@akanjs/devkit 1.0.19 → 2.1.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aiEditor.ts +304 -0
- package/akanApp/akanApp.host.ts +393 -0
- package/akanApp/index.ts +1 -0
- package/akanConfig/akanConfig.test.ts +236 -0
- package/akanConfig/akanConfig.ts +384 -0
- package/akanConfig/index.ts +2 -0
- package/akanConfig/types.ts +23 -0
- package/applicationBuildReporter.ts +69 -0
- package/applicationBuildRunner.ts +302 -0
- package/applicationReleasePackager.ts +206 -0
- package/artifact/implicitRootLayout.ts +155 -0
- package/artifact/index.ts +1 -0
- package/artifact/routeSeedIndex.test.ts +98 -0
- package/artifact/routeSeedIndex.ts +130 -0
- package/auth.ts +41 -0
- package/builder.ts +164 -0
- package/capacitor.base.config.ts +88 -0
- package/capacitorApp.ts +440 -0
- package/commandDecorators/argMeta.ts +102 -0
- package/commandDecorators/command.ts +343 -0
- package/commandDecorators/commandBuilder.ts +224 -0
- package/commandDecorators/commandDecorators.test.ts +212 -0
- package/commandDecorators/commandMeta.ts +7 -0
- package/commandDecorators/dependencyBuilder.ts +100 -0
- package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
- package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
- package/commandDecorators/targetMeta.ts +31 -0
- package/commandDecorators/types.ts +10 -0
- package/constants.ts +25 -0
- package/createTunnel.ts +36 -0
- package/dependencyScanner.ts +357 -0
- package/devkitUtils.test.ts +259 -0
- package/executors.test.ts +315 -0
- package/executors.ts +1390 -0
- package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
- package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
- package/fileSys.ts +39 -0
- package/frontendBuild/allRoutesBuilder.ts +103 -0
- package/frontendBuild/buildRouteClient.test.ts +190 -0
- package/frontendBuild/clientBuildTypes.ts +114 -0
- package/frontendBuild/clientEntriesBundler.ts +303 -0
- package/frontendBuild/clientEntryDiscovery.ts +199 -0
- package/frontendBuild/csrArtifactBuilder.ts +237 -0
- package/frontendBuild/cssCompiler.ts +286 -0
- package/frontendBuild/cssImportResolver.ts +116 -0
- package/frontendBuild/fontOptimizer.ts +427 -0
- package/frontendBuild/frontendBuild.test.ts +204 -0
- package/frontendBuild/hmrChangeClassifier.ts +28 -0
- package/frontendBuild/hmrWatcher.ts +102 -0
- package/frontendBuild/index.ts +18 -0
- package/frontendBuild/pagesBundleBuilder.ts +137 -0
- package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
- package/frontendBuild/precompressArtifacts.ts +59 -0
- package/frontendBuild/routeClientBuilder.ts +290 -0
- package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
- package/frontendBuild/vendorSpecifiers.ts +16 -0
- package/frontendBuild/watchRootResolver.ts +28 -0
- package/getCredentials.ts +19 -0
- package/getDirname.ts +3 -0
- package/getModelFileData.ts +59 -0
- package/getRelatedCnsts.ts +313 -0
- package/guideline.ts +19 -0
- package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
- package/incrementalBuilder/index.ts +1 -0
- package/{esm/src/index.js → index.ts} +28 -15
- package/lint/no-deep-internal-import.grit +25 -0
- package/lint/no-import-client-functions.grit +32 -0
- package/lint/no-import-external-library.grit +21 -0
- package/lint/no-js-private-class-method.grit +42 -0
- package/lint/no-use-client-in-server.grit +7 -0
- package/lint/non-scalar-props-restricted.grit +13 -0
- package/linter.ts +271 -0
- package/mobile/index.ts +1 -0
- package/mobile/mobileTarget.test.ts +53 -0
- package/mobile/mobileTarget.ts +88 -0
- package/package.json +48 -31
- package/prompter.ts +72 -0
- package/scanInfo.ts +606 -0
- package/selectModel.ts +11 -0
- package/{esm/src/spinner.js → spinner.ts} +22 -28
- package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
- package/sshTunnel.ts +152 -0
- package/{esm/src/streamAi.js → streamAi.ts} +18 -12
- package/transforms/barrelAnalyzer.ts +278 -0
- package/transforms/barrelImportsPlugin.ts +504 -0
- package/transforms/externalizeFrameworkPlugin.ts +185 -0
- package/transforms/index.ts +5 -0
- package/transforms/rscUseClientTransform.ts +59 -0
- package/transforms/transforms.test.ts +208 -0
- package/transforms/useClientBundlePlugin.ts +47 -0
- package/tsconfig.json +37 -0
- package/typeChecker.ts +264 -0
- package/types.ts +44 -0
- package/ui/MultiScrollList.tsx +242 -0
- package/ui/ScrollList.tsx +107 -0
- package/ui/index.ts +2 -0
- package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
- package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
- package/README.md +0 -1
- package/cjs/index.js +0 -21
- package/cjs/src/aiEditor.js +0 -311
- package/cjs/src/auth.js +0 -72
- package/cjs/src/builder.js +0 -114
- package/cjs/src/capacitorApp.js +0 -313
- package/cjs/src/commandDecorators/argMeta.js +0 -88
- package/cjs/src/commandDecorators/command.js +0 -324
- package/cjs/src/commandDecorators/commandMeta.js +0 -30
- package/cjs/src/commandDecorators/helpFormatter.js +0 -211
- package/cjs/src/commandDecorators/index.js +0 -31
- package/cjs/src/commandDecorators/targetMeta.js +0 -57
- package/cjs/src/commandDecorators/types.js +0 -15
- package/cjs/src/constants.js +0 -46
- package/cjs/src/createTunnel.js +0 -49
- package/cjs/src/dependencyScanner.js +0 -220
- package/cjs/src/executors.js +0 -964
- package/cjs/src/extractDeps.js +0 -103
- package/cjs/src/fileEditor.js +0 -120
- package/cjs/src/getCredentials.js +0 -44
- package/cjs/src/getDirname.js +0 -38
- package/cjs/src/getModelFileData.js +0 -66
- package/cjs/src/getRelatedCnsts.js +0 -260
- package/cjs/src/guideline.js +0 -15
- package/cjs/src/index.js +0 -65
- package/cjs/src/linter.js +0 -238
- package/cjs/src/prompter.js +0 -85
- package/cjs/src/scanInfo.js +0 -491
- package/cjs/src/selectModel.js +0 -46
- package/cjs/src/spinner.js +0 -93
- package/cjs/src/streamAi.js +0 -62
- package/cjs/src/typeChecker.js +0 -207
- package/cjs/src/types.js +0 -15
- package/cjs/src/uploadRelease.js +0 -112
- package/cjs/src/useStdoutDimensions.js +0 -43
- package/esm/index.js +0 -1
- package/esm/src/aiEditor.js +0 -282
- package/esm/src/auth.js +0 -42
- package/esm/src/builder.js +0 -81
- package/esm/src/commandDecorators/argMeta.js +0 -54
- package/esm/src/commandDecorators/command.js +0 -290
- package/esm/src/commandDecorators/commandMeta.js +0 -7
- package/esm/src/commandDecorators/targetMeta.js +0 -33
- package/esm/src/commandDecorators/types.js +0 -0
- package/esm/src/constants.js +0 -17
- package/esm/src/createTunnel.js +0 -26
- package/esm/src/dependencyScanner.js +0 -187
- package/esm/src/executors.js +0 -928
- package/esm/src/getCredentials.js +0 -11
- package/esm/src/getDirname.js +0 -5
- package/esm/src/getModelFileData.js +0 -33
- package/esm/src/getRelatedCnsts.js +0 -221
- package/esm/src/guideline.js +0 -0
- package/esm/src/linter.js +0 -205
- package/esm/src/prompter.js +0 -51
- package/esm/src/scanInfo.js +0 -455
- package/esm/src/selectModel.js +0 -13
- package/esm/src/typeChecker.js +0 -174
- package/esm/src/types.js +0 -0
- package/index.d.ts +0 -1
- package/src/aiEditor.d.ts +0 -50
- package/src/auth.d.ts +0 -9
- package/src/builder.d.ts +0 -18
- package/src/capacitorApp.d.ts +0 -39
- package/src/commandDecorators/argMeta.d.ts +0 -67
- package/src/commandDecorators/command.d.ts +0 -2
- package/src/commandDecorators/commandMeta.d.ts +0 -2
- package/src/commandDecorators/helpFormatter.d.ts +0 -3
- package/src/commandDecorators/index.d.ts +0 -6
- package/src/commandDecorators/targetMeta.d.ts +0 -19
- package/src/commandDecorators/types.d.ts +0 -1
- package/src/constants.d.ts +0 -26
- package/src/createTunnel.d.ts +0 -8
- package/src/dependencyScanner.d.ts +0 -23
- package/src/executors.d.ts +0 -296
- package/src/extractDeps.d.ts +0 -7
- package/src/fileEditor.d.ts +0 -16
- package/src/getCredentials.d.ts +0 -12
- package/src/getDirname.d.ts +0 -1
- package/src/getModelFileData.d.ts +0 -16
- package/src/getRelatedCnsts.d.ts +0 -53
- package/src/guideline.d.ts +0 -19
- package/src/index.d.ts +0 -23
- package/src/linter.d.ts +0 -109
- package/src/prompter.d.ts +0 -14
- package/src/scanInfo.d.ts +0 -82
- package/src/selectModel.d.ts +0 -1
- package/src/spinner.d.ts +0 -20
- package/src/streamAi.d.ts +0 -6
- package/src/typeChecker.d.ts +0 -52
- package/src/types.d.ts +0 -31
- package/src/uploadRelease.d.ts +0 -10
- package/src/useStdoutDimensions.d.ts +0 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { App, getArgMetas, Lib, Module, Pkg, Sys, Workspace } from "./argMeta";
|
|
3
|
+
import { command } from "./commandBuilder";
|
|
4
|
+
import {
|
|
5
|
+
assertUniqueDependencies,
|
|
6
|
+
CommandContainer,
|
|
7
|
+
getDependencyKey,
|
|
8
|
+
injectDependencies,
|
|
9
|
+
runner,
|
|
10
|
+
script,
|
|
11
|
+
} from "./dependencyBuilder";
|
|
12
|
+
import { formatCommandHelp, formatHelp } from "./helpFormatter";
|
|
13
|
+
import { getTargetMetas } from "./targetMeta";
|
|
14
|
+
|
|
15
|
+
const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
16
|
+
const stripAnsi = (value: string) => value.replace(ansiPattern, "");
|
|
17
|
+
|
|
18
|
+
describe("command helper metadata", () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
CommandContainer.clear();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("records targets, args, options, and internal executor tokens", () => {
|
|
24
|
+
class ExampleRunner extends runner("example") {
|
|
25
|
+
run() {
|
|
26
|
+
return "ok";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class ExampleScript extends script("example", [ExampleRunner]) {
|
|
31
|
+
callRunner() {
|
|
32
|
+
return this.exampleRunner.run();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class ExampleCommand extends command("example", [ExampleScript], ({ public: publicTarget, dev }) => ({
|
|
37
|
+
createModule: publicTarget({ short: true, desc: "Create a module" })
|
|
38
|
+
.arg("moduleName", String, { desc: "module name", example: "user" })
|
|
39
|
+
.option("count", Number, { desc: "count", default: 1 })
|
|
40
|
+
.option("force", Boolean, { desc: "force", default: false })
|
|
41
|
+
.with(Workspace, App, Lib, Sys, Pkg, Module)
|
|
42
|
+
.exec(function (moduleName, count, force, workspace, app, lib, sys, pkg, module) {
|
|
43
|
+
expect(this.exampleScript.callRunner()).toBe("ok");
|
|
44
|
+
return { moduleName, count, force, workspace, app, lib, sys, pkg, module };
|
|
45
|
+
}),
|
|
46
|
+
devOnlyTask: dev({ devOnly: true, desc: "Hidden task" })
|
|
47
|
+
.arg("targetName", String)
|
|
48
|
+
.exec((targetName) => targetName),
|
|
49
|
+
})) {}
|
|
50
|
+
|
|
51
|
+
const targetMetas = getTargetMetas(ExampleCommand);
|
|
52
|
+
expect(targetMetas.map((target) => target.key)).toEqual(["createModule", "devOnlyTask"]);
|
|
53
|
+
expect(targetMetas[0]?.targetOption).toEqual({
|
|
54
|
+
runsOnWorkspaceRoot: true,
|
|
55
|
+
short: true,
|
|
56
|
+
desc: "Create a module",
|
|
57
|
+
type: "public",
|
|
58
|
+
});
|
|
59
|
+
expect(targetMetas[1]?.targetOption).toEqual({
|
|
60
|
+
runsOnWorkspaceRoot: true,
|
|
61
|
+
devOnly: true,
|
|
62
|
+
desc: "Hidden task",
|
|
63
|
+
type: "dev",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const [allArgMetas, optionMetas, internalArgMetas] = getArgMetas(ExampleCommand, "createModule");
|
|
67
|
+
expect(allArgMetas.map((arg) => [arg.type, arg.idx])).toEqual([
|
|
68
|
+
["Argument", 0],
|
|
69
|
+
["Option", 1],
|
|
70
|
+
["Option", 2],
|
|
71
|
+
["Workspace", 3],
|
|
72
|
+
["App", 4],
|
|
73
|
+
["Lib", 5],
|
|
74
|
+
["Sys", 6],
|
|
75
|
+
["Pkg", 7],
|
|
76
|
+
["Module", 8],
|
|
77
|
+
]);
|
|
78
|
+
expect(optionMetas.map((arg) => arg.name)).toEqual(["count", "force"]);
|
|
79
|
+
const internalArgTypes = internalArgMetas.map((arg) => arg.type as string);
|
|
80
|
+
expect(internalArgTypes).toEqual(["Argument", "Workspace", "App", "Lib", "Sys", "Pkg", "Module"]);
|
|
81
|
+
|
|
82
|
+
const commandInstance = CommandContainer.get(ExampleCommand);
|
|
83
|
+
const result = targetMetas[0]?.handler.call(
|
|
84
|
+
commandInstance,
|
|
85
|
+
"profile",
|
|
86
|
+
2,
|
|
87
|
+
true,
|
|
88
|
+
{ name: "workspace" },
|
|
89
|
+
{ name: "app" },
|
|
90
|
+
{ name: "lib" },
|
|
91
|
+
{ name: "sys" },
|
|
92
|
+
{ name: "pkg" },
|
|
93
|
+
{ name: "module" },
|
|
94
|
+
);
|
|
95
|
+
expect(result).toMatchObject({ moduleName: "profile", count: 2, force: true });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("formats global and command help from helper metadata", () => {
|
|
99
|
+
class HelpCommand extends command("help", ({ public: publicTarget, dev }) => ({
|
|
100
|
+
buildApp: publicTarget({ desc: "Build app" })
|
|
101
|
+
.with(App)
|
|
102
|
+
.option("write", Boolean, { desc: "write generated files", default: true })
|
|
103
|
+
.option("mode", String, {
|
|
104
|
+
desc: "build mode",
|
|
105
|
+
enum: [
|
|
106
|
+
{ label: "Fast", value: "fast" },
|
|
107
|
+
{ label: "Full", value: "full" },
|
|
108
|
+
],
|
|
109
|
+
})
|
|
110
|
+
.exec(() => undefined),
|
|
111
|
+
generateModule: publicTarget({ desc: "Generate module" })
|
|
112
|
+
.with(Module)
|
|
113
|
+
.arg("modelName", String, { desc: "model name" })
|
|
114
|
+
.exec(() => undefined),
|
|
115
|
+
hiddenTask: dev({ devOnly: true, desc: "Hidden task" }).exec(() => undefined),
|
|
116
|
+
})) {}
|
|
117
|
+
|
|
118
|
+
const globalHelp = stripAnsi(formatHelp([HelpCommand], "1.2.3"));
|
|
119
|
+
expect(globalHelp).toContain("Version: 1.2.3");
|
|
120
|
+
expect(globalHelp).toContain("build-app [app]");
|
|
121
|
+
expect(globalHelp).toContain("generate-module [sys:module] [modelName]");
|
|
122
|
+
expect(globalHelp).not.toContain("hidden-task");
|
|
123
|
+
|
|
124
|
+
const commandHelp = stripAnsi(formatCommandHelp(HelpCommand, "buildApp"));
|
|
125
|
+
expect(commandHelp).toContain("akan build-app [app]");
|
|
126
|
+
expect(commandHelp).toContain("--write");
|
|
127
|
+
expect(commandHelp).toContain("[default: true]");
|
|
128
|
+
expect(commandHelp).toContain("Fast, Full");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("command helper dependency injection", () => {
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
CommandContainer.clear();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("creates stable dependency keys and singleton injected instances", () => {
|
|
138
|
+
class StableRunner extends runner("stable") {
|
|
139
|
+
readonly id = Math.random();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
class StableScript extends script("stable", [StableRunner]) {}
|
|
143
|
+
|
|
144
|
+
expect(StableRunner.refName).toBe("stable");
|
|
145
|
+
expect(StableRunner.dependencyKind).toBe("runner");
|
|
146
|
+
expect(StableRunner.dependencyKey).toBe("stableRunner");
|
|
147
|
+
expect(StableScript.dependencyKind).toBe("script");
|
|
148
|
+
expect(StableScript.dependencyKey).toBe("stableScript");
|
|
149
|
+
expect(getDependencyKey(StableScript)).toBe("stableScript");
|
|
150
|
+
|
|
151
|
+
const first = CommandContainer.get(StableScript);
|
|
152
|
+
const second = CommandContainer.get(StableScript);
|
|
153
|
+
expect(first).toBe(second);
|
|
154
|
+
expect(first.stableRunner).toBe(CommandContainer.get(StableRunner));
|
|
155
|
+
expect(Object.keys(first)).not.toContain("stableRunner");
|
|
156
|
+
|
|
157
|
+
expect(() => {
|
|
158
|
+
first.stableRunner = new StableRunner();
|
|
159
|
+
}).toThrow();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("rejects duplicate dependencies by class or generated key", () => {
|
|
163
|
+
class OneRunner extends runner("duplicate") {}
|
|
164
|
+
class AnotherRunner extends runner("duplicate") {}
|
|
165
|
+
|
|
166
|
+
expect(() => assertUniqueDependencies([OneRunner, OneRunner])).toThrow("Duplicate command dependency class");
|
|
167
|
+
expect(() => assertUniqueDependencies([OneRunner, AnotherRunner])).toThrow(
|
|
168
|
+
'Duplicate command dependency key "duplicateRunner"',
|
|
169
|
+
);
|
|
170
|
+
expect(() => script("broken", [OneRunner, AnotherRunner])).toThrow(
|
|
171
|
+
'Duplicate command dependency key "duplicateRunner"',
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("injectDependencies detects circular command dependencies", () => {
|
|
176
|
+
class CircularA {
|
|
177
|
+
static readonly refName = "circularA";
|
|
178
|
+
static readonly dependencyKind = "script";
|
|
179
|
+
static readonly dependencyKey = "circularAScript";
|
|
180
|
+
|
|
181
|
+
constructor() {
|
|
182
|
+
CommandContainer.get(CircularB);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class CircularB {
|
|
187
|
+
static readonly refName = "circularB";
|
|
188
|
+
static readonly dependencyKind = "script";
|
|
189
|
+
static readonly dependencyKey = "circularBScript";
|
|
190
|
+
|
|
191
|
+
constructor() {
|
|
192
|
+
CommandContainer.get(CircularA);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
expect(() => CommandContainer.get(CircularA)).toThrow("Circular command dependency");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("injects explicit dependency lists into arbitrary objects", () => {
|
|
200
|
+
class DirectRunner extends runner("direct") {
|
|
201
|
+
value = "runner";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const target = {};
|
|
205
|
+
const injected = injectDependencies(target, [DirectRunner]);
|
|
206
|
+
expect(injected.directRunner.value).toBe("runner");
|
|
207
|
+
|
|
208
|
+
const descriptor = Object.getOwnPropertyDescriptor(injected, "directRunner");
|
|
209
|
+
expect(descriptor?.enumerable).toBe(false);
|
|
210
|
+
expect(descriptor?.writable).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { DependencyCls, DependencyKey, DependencyKind } from "./types";
|
|
2
|
+
|
|
3
|
+
type UnionToIntersection<Union> = (Union extends unknown ? (value: Union) => void : never) extends (
|
|
4
|
+
value: infer Intersection,
|
|
5
|
+
) => void
|
|
6
|
+
? Intersection
|
|
7
|
+
: never;
|
|
8
|
+
|
|
9
|
+
type DependencyToField<Dep> = Dep extends DependencyCls<infer Instance, infer Key> ? { [K in Key]: Instance } : never;
|
|
10
|
+
|
|
11
|
+
export type DependencyInstanceMap<Deps extends readonly DependencyCls[]> = [Deps[number]] extends [never]
|
|
12
|
+
? Record<PropertyKey, never>
|
|
13
|
+
: UnionToIntersection<DependencyToField<Deps[number]>>;
|
|
14
|
+
|
|
15
|
+
const capitalize = (value: string) => value.slice(0, 1).toUpperCase() + value.slice(1);
|
|
16
|
+
|
|
17
|
+
const createDependencyKey = <RefName extends string, Kind extends DependencyKind>(
|
|
18
|
+
refName: RefName,
|
|
19
|
+
kind: Kind,
|
|
20
|
+
): DependencyKey<RefName, Kind> => `${refName}${capitalize(kind)}` as DependencyKey<RefName, Kind>;
|
|
21
|
+
|
|
22
|
+
export class CommandContainer {
|
|
23
|
+
static #instances = new Map<DependencyCls, unknown>();
|
|
24
|
+
static #resolving = new Set<DependencyCls>();
|
|
25
|
+
|
|
26
|
+
static get<T>(dep: DependencyCls<T>): T {
|
|
27
|
+
const instance = CommandContainer.#instances.get(dep);
|
|
28
|
+
if (instance) return instance as T;
|
|
29
|
+
if (CommandContainer.#resolving.has(dep)) throw new Error(`Circular command dependency: ${dep.name}`);
|
|
30
|
+
CommandContainer.#resolving.add(dep);
|
|
31
|
+
try {
|
|
32
|
+
const nextInstance = new dep();
|
|
33
|
+
CommandContainer.#instances.set(dep, nextInstance);
|
|
34
|
+
return nextInstance;
|
|
35
|
+
} finally {
|
|
36
|
+
CommandContainer.#resolving.delete(dep);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static clear() {
|
|
41
|
+
CommandContainer.#instances.clear();
|
|
42
|
+
CommandContainer.#resolving.clear();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const getDependencyKey = (dep: DependencyCls) => dep.dependencyKey;
|
|
47
|
+
|
|
48
|
+
export const assertUniqueDependencies = (deps: readonly DependencyCls[]) => {
|
|
49
|
+
const keys = new Map<string, DependencyCls>();
|
|
50
|
+
const classes = new Set<DependencyCls>();
|
|
51
|
+
for (const dep of deps) {
|
|
52
|
+
if (classes.has(dep)) throw new Error(`Duplicate command dependency class: ${dep.name}`);
|
|
53
|
+
classes.add(dep);
|
|
54
|
+
const key = getDependencyKey(dep);
|
|
55
|
+
const existing = keys.get(key);
|
|
56
|
+
if (existing) throw new Error(`Duplicate command dependency key "${key}": ${existing.name}, ${dep.name}`);
|
|
57
|
+
keys.set(key, dep);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const injectDependencies = <Deps extends readonly DependencyCls[]>(target: object, deps: Deps) => {
|
|
62
|
+
assertUniqueDependencies(deps);
|
|
63
|
+
for (const dep of deps) {
|
|
64
|
+
Object.defineProperty(target, getDependencyKey(dep), {
|
|
65
|
+
configurable: true,
|
|
66
|
+
enumerable: false,
|
|
67
|
+
value: CommandContainer.get(dep),
|
|
68
|
+
writable: false,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return target as typeof target & DependencyInstanceMap<Deps>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const runner = <RefName extends string>(refName: RefName) => {
|
|
75
|
+
class RunnerBase {
|
|
76
|
+
static readonly refName = refName;
|
|
77
|
+
static readonly dependencyKind = "runner";
|
|
78
|
+
static readonly dependencyKey = createDependencyKey(refName, "runner");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return RunnerBase as unknown as DependencyCls<InstanceType<typeof RunnerBase>, DependencyKey<RefName, "runner">>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const script = <RefName extends string, Deps extends readonly DependencyCls[]>(
|
|
85
|
+
refName: RefName,
|
|
86
|
+
deps: Deps = [] as unknown as Deps,
|
|
87
|
+
) => {
|
|
88
|
+
assertUniqueDependencies(deps);
|
|
89
|
+
class ScriptBase {
|
|
90
|
+
static readonly refName = refName;
|
|
91
|
+
static readonly dependencyKind = "script";
|
|
92
|
+
static readonly dependencyKey = createDependencyKey(refName, "script");
|
|
93
|
+
|
|
94
|
+
constructor() {
|
|
95
|
+
injectDependencies(this, deps);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return ScriptBase as unknown as DependencyCls<DependencyInstanceMap<Deps>, DependencyKey<RefName, "script">>;
|
|
100
|
+
};
|
|
@@ -1,75 +1,103 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
|
|
3
|
+
import { type EnumChoice, getArgMetas } from "./argMeta";
|
|
4
|
+
import { type CommandCls, getTargetMetas } from "./targetMeta";
|
|
5
|
+
|
|
6
|
+
const camelToKebabCase = (str: string) => str.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
7
|
+
const formatChoice = (choice: EnumChoice) => (typeof choice === "object" ? choice.label : choice.toString());
|
|
8
|
+
|
|
9
|
+
const groupCommands = (commands: CommandCls[]) => {
|
|
10
|
+
const groups = new Map<string, { name: string; commands: { key: string; args: string[]; desc?: string }[] }>();
|
|
11
|
+
|
|
7
12
|
for (const command of commands) {
|
|
8
13
|
const className = command.name.replace("Command", "");
|
|
9
14
|
const groupName = className;
|
|
15
|
+
|
|
10
16
|
if (!groups.has(groupName)) {
|
|
11
17
|
groups.set(groupName, { name: groupName, commands: [] });
|
|
12
18
|
}
|
|
19
|
+
|
|
13
20
|
const targetMetas = getTargetMetas(command);
|
|
14
21
|
for (const targetMeta of targetMetas) {
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
// Skip devOnly commands in help
|
|
23
|
+
if (targetMeta.targetOption.devOnly) continue;
|
|
24
|
+
|
|
17
25
|
const [allArgMetas] = getArgMetas(command, targetMeta.key);
|
|
18
|
-
const args = allArgMetas
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return "[sys:module]";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const args = allArgMetas
|
|
27
|
+
.filter((arg) => arg.type !== "Option")
|
|
28
|
+
.map((arg) => {
|
|
29
|
+
if (arg.type === "Workspace") return "";
|
|
30
|
+
if (arg.type === "Module") return "[sys:module]";
|
|
31
|
+
if (arg.type === "Argument") {
|
|
32
|
+
return `[${arg.name}]`;
|
|
33
|
+
}
|
|
34
|
+
return `[${arg.type.toLowerCase()}]`;
|
|
35
|
+
})
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
|
|
28
38
|
const group = groups.get(groupName);
|
|
29
39
|
if (group) {
|
|
30
40
|
group.commands.push({
|
|
31
41
|
key: camelToKebabCase(targetMeta.key),
|
|
32
42
|
args,
|
|
33
|
-
desc: targetMeta.targetOption.desc
|
|
43
|
+
desc: targetMeta.targetOption.desc,
|
|
34
44
|
});
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
47
|
}
|
|
48
|
+
|
|
38
49
|
return groups;
|
|
39
50
|
};
|
|
40
|
-
|
|
51
|
+
|
|
52
|
+
export const formatHelp = (commands: CommandCls[], version: string) => {
|
|
41
53
|
const groups = groupCommands(commands);
|
|
42
|
-
const lines = [];
|
|
54
|
+
const lines: string[] = [];
|
|
55
|
+
|
|
56
|
+
// Header
|
|
43
57
|
lines.push("");
|
|
44
|
-
lines.push(chalk.bold.cyan("
|
|
58
|
+
lines.push(chalk.bold.cyan(" ╔═══════════════════════════════════════════════════╗"));
|
|
45
59
|
lines.push(
|
|
46
|
-
chalk.bold.cyan("
|
|
60
|
+
chalk.bold.cyan(" ║") +
|
|
61
|
+
chalk.bold.white(" Akan.js Framework CLI ") +
|
|
62
|
+
chalk.bold.cyan(" ║"),
|
|
47
63
|
);
|
|
48
|
-
lines.push(chalk.bold.cyan("
|
|
64
|
+
lines.push(chalk.bold.cyan(" ╚═══════════════════════════════════════════════════╝"));
|
|
49
65
|
lines.push("");
|
|
50
66
|
lines.push(chalk.gray(` Version: ${version}`));
|
|
51
67
|
lines.push("");
|
|
68
|
+
|
|
69
|
+
// Usage
|
|
52
70
|
lines.push(chalk.bold.yellow(" USAGE"));
|
|
53
71
|
lines.push("");
|
|
54
72
|
lines.push(chalk.gray(" $ ") + chalk.white("akan") + chalk.gray(" <command> [options]"));
|
|
55
73
|
lines.push("");
|
|
74
|
+
|
|
75
|
+
// Commands by category
|
|
56
76
|
lines.push(chalk.bold.yellow(" COMMANDS"));
|
|
57
77
|
lines.push("");
|
|
78
|
+
|
|
58
79
|
for (const [groupName, group] of groups) {
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
// Skip empty groups (all commands are devOnly)
|
|
81
|
+
if (group.commands.length === 0) continue;
|
|
82
|
+
|
|
61
83
|
lines.push(chalk.bold.magenta(` ${groupName}`));
|
|
62
84
|
lines.push("");
|
|
85
|
+
|
|
63
86
|
for (const cmd of group.commands) {
|
|
64
87
|
const cmdName = chalk.green(cmd.key);
|
|
65
88
|
const cmdArgs = cmd.args.length > 0 ? chalk.gray(` ${cmd.args.join(" ")}`) : "";
|
|
89
|
+
|
|
90
|
+
// Format description: wrap if too long
|
|
66
91
|
if (cmd.desc) {
|
|
67
92
|
const maxLineLength = 70;
|
|
68
93
|
const cmdPrefix = ` ${cmdName}${cmdArgs}`;
|
|
69
94
|
const indent = " ";
|
|
95
|
+
|
|
70
96
|
if (cmdPrefix.length + cmd.desc.length + 3 < maxLineLength) {
|
|
97
|
+
// Single line
|
|
71
98
|
lines.push(`${cmdPrefix} ${chalk.gray(cmd.desc)}`);
|
|
72
99
|
} else {
|
|
100
|
+
// Multi-line
|
|
73
101
|
lines.push(cmdPrefix);
|
|
74
102
|
lines.push(`${indent}${chalk.gray(cmd.desc)}`);
|
|
75
103
|
}
|
|
@@ -79,12 +107,16 @@ const formatHelp = (commands, version) => {
|
|
|
79
107
|
}
|
|
80
108
|
lines.push("");
|
|
81
109
|
}
|
|
110
|
+
|
|
111
|
+
// Global Options
|
|
82
112
|
lines.push(chalk.bold.yellow(" OPTIONS"));
|
|
83
113
|
lines.push("");
|
|
84
114
|
lines.push(` ${chalk.green("-v, --verbose")} ${chalk.gray("Enable verbose output")}`);
|
|
85
115
|
lines.push(` ${chalk.green("-h, --help")} ${chalk.gray("Display this help message")}`);
|
|
86
116
|
lines.push(` ${chalk.green("-V, --version")} ${chalk.gray("Output version number")}`);
|
|
87
117
|
lines.push("");
|
|
118
|
+
|
|
119
|
+
// Examples
|
|
88
120
|
lines.push(chalk.bold.yellow(" EXAMPLES"));
|
|
89
121
|
lines.push("");
|
|
90
122
|
lines.push(chalk.gray(" # Create a new workspace"));
|
|
@@ -96,48 +128,63 @@ const formatHelp = (commands, version) => {
|
|
|
96
128
|
lines.push(chalk.gray(" # Create a new module"));
|
|
97
129
|
lines.push(chalk.white(" $ akan create-module userProfile"));
|
|
98
130
|
lines.push("");
|
|
131
|
+
|
|
132
|
+
// Footer
|
|
99
133
|
lines.push(chalk.gray(" Documentation: ") + chalk.cyan("https://akanjs.com/docs"));
|
|
100
134
|
lines.push(chalk.gray(" Report issues: ") + chalk.cyan("https://github.com/akan-team/akanjs/issues"));
|
|
101
135
|
lines.push("");
|
|
136
|
+
|
|
102
137
|
return lines.join("\n");
|
|
103
138
|
};
|
|
104
|
-
|
|
139
|
+
|
|
140
|
+
export const formatCommandHelp = (command: CommandCls, key: string) => {
|
|
105
141
|
const [allArgMetas, argMetas] = getArgMetas(command, key);
|
|
106
142
|
const kebabKey = camelToKebabCase(key);
|
|
107
|
-
const lines = [];
|
|
143
|
+
const lines: string[] = [];
|
|
144
|
+
|
|
108
145
|
const targetMetas = getTargetMetas(command);
|
|
109
146
|
const targetMeta = targetMetas.find((t) => t.key === key);
|
|
110
147
|
const commandDesc = targetMeta?.targetOption.desc;
|
|
148
|
+
|
|
111
149
|
lines.push("");
|
|
112
150
|
lines.push(chalk.bold.cyan(` Command: ${kebabKey}`));
|
|
113
151
|
if (commandDesc) {
|
|
114
152
|
lines.push(chalk.gray(` ${commandDesc}`));
|
|
115
153
|
}
|
|
116
154
|
lines.push("");
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
155
|
+
|
|
156
|
+
// Usage
|
|
157
|
+
const args = allArgMetas
|
|
158
|
+
.filter((arg) => arg.type !== "Option")
|
|
159
|
+
.map((arg) => {
|
|
160
|
+
if (arg.type === "Workspace") return "";
|
|
161
|
+
if (arg.type === "Module") return "[sys:module]";
|
|
162
|
+
if (arg.type === "Argument") {
|
|
163
|
+
return `[${camelToKebabCase(arg.name)}]`;
|
|
164
|
+
}
|
|
165
|
+
return `[${arg.type.toLowerCase()}]`;
|
|
166
|
+
})
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join(" ");
|
|
169
|
+
|
|
127
170
|
lines.push(chalk.bold.yellow(" USAGE"));
|
|
128
171
|
lines.push("");
|
|
129
172
|
lines.push(chalk.gray(" $ ") + chalk.white(`akan ${kebabKey}`) + (args ? chalk.gray(` ${args}`) : ""));
|
|
130
173
|
lines.push("");
|
|
174
|
+
|
|
175
|
+
// Arguments
|
|
131
176
|
const nonOptionArgs = allArgMetas.filter((arg) => arg.type !== "Option");
|
|
132
177
|
if (nonOptionArgs.length > 0) {
|
|
133
178
|
lines.push(chalk.bold.yellow(" ARGUMENTS"));
|
|
134
179
|
lines.push("");
|
|
180
|
+
|
|
135
181
|
for (const arg of nonOptionArgs) {
|
|
136
|
-
if (arg.type === "Workspace")
|
|
137
|
-
|
|
138
|
-
let argName;
|
|
139
|
-
let argDesc;
|
|
182
|
+
if (arg.type === "Workspace") continue;
|
|
183
|
+
|
|
184
|
+
let argName: string;
|
|
185
|
+
let argDesc: string;
|
|
140
186
|
let example = "";
|
|
187
|
+
|
|
141
188
|
if (arg.type === "Argument") {
|
|
142
189
|
argName = camelToKebabCase(arg.name);
|
|
143
190
|
argDesc = arg.argsOption.desc ?? "";
|
|
@@ -149,29 +196,35 @@ const formatCommandHelp = (command, key) => {
|
|
|
149
196
|
argName = arg.type.toLowerCase();
|
|
150
197
|
argDesc = `${arg.type} name in this workspace`;
|
|
151
198
|
}
|
|
199
|
+
|
|
152
200
|
lines.push(` ${chalk.green(argName)} ${chalk.gray(argDesc)}${example}`);
|
|
153
201
|
}
|
|
202
|
+
|
|
154
203
|
lines.push("");
|
|
155
204
|
}
|
|
205
|
+
|
|
206
|
+
// Options
|
|
156
207
|
const optionArgs = argMetas.filter((a) => a.type === "Option");
|
|
157
208
|
if (optionArgs.length > 0) {
|
|
158
209
|
lines.push(chalk.bold.yellow(" OPTIONS"));
|
|
159
210
|
lines.push("");
|
|
211
|
+
|
|
160
212
|
for (const arg of optionArgs) {
|
|
161
213
|
const opt = arg.argsOption;
|
|
162
214
|
const flag = opt.flag ? `-${opt.flag}, ` : "";
|
|
163
215
|
const kebabName = camelToKebabCase(arg.name);
|
|
164
216
|
const optName = `${flag}--${kebabName}`;
|
|
165
217
|
const optDesc = opt.desc ?? "";
|
|
166
|
-
const defaultVal = opt.default !==
|
|
167
|
-
const choices = opt.enum
|
|
218
|
+
const defaultVal = opt.default !== undefined ? chalk.gray(` [default: ${String(opt.default)}]`) : "";
|
|
219
|
+
const choices = opt.enum
|
|
220
|
+
? chalk.gray(
|
|
221
|
+
typeof opt.enum === "function" ? " ([dynamic choices])" : ` (${opt.enum.map(formatChoice).join(", ")})`,
|
|
222
|
+
)
|
|
223
|
+
: "";
|
|
168
224
|
lines.push(` ${chalk.green(optName)} ${chalk.gray(optDesc)}${defaultVal}${choices}`);
|
|
169
225
|
}
|
|
170
226
|
lines.push("");
|
|
171
227
|
}
|
|
228
|
+
|
|
172
229
|
return lines.join("\n");
|
|
173
230
|
};
|
|
174
|
-
export {
|
|
175
|
-
formatCommandHelp,
|
|
176
|
-
formatHelp
|
|
177
|
-
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from "./argMeta";
|
|
2
|
+
export * from "./command";
|
|
3
|
+
export * from "./commandBuilder";
|
|
2
4
|
export * from "./commandMeta";
|
|
5
|
+
export * from "./dependencyBuilder";
|
|
6
|
+
export * from "./helpFormatter";
|
|
3
7
|
export * from "./targetMeta";
|
|
4
8
|
export * from "./types";
|
|
5
|
-
export * from "./helpFormatter";
|
|
6
|
-
export * from "./command";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ArgMeta, InternalArgMeta } from "./argMeta";
|
|
2
|
+
import type { DependencyCls } from "./types";
|
|
3
|
+
|
|
4
|
+
export const COMMAND_META: unique symbol = Symbol("akan.command.meta");
|
|
5
|
+
|
|
6
|
+
export interface TargetMeta {
|
|
7
|
+
key: string;
|
|
8
|
+
targetOption: TargetOption;
|
|
9
|
+
args: (ArgMeta | InternalArgMeta)[];
|
|
10
|
+
handler: (this: unknown, ...args: unknown[]) => unknown | Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CommandStatics {
|
|
14
|
+
[COMMAND_META]: Map<string, TargetMeta>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CommandCls<Instance = unknown, Key extends string = string> = DependencyCls<Instance, Key> & CommandStatics;
|
|
18
|
+
|
|
19
|
+
export const getTargetMetas = (command: CommandCls): TargetMeta[] => {
|
|
20
|
+
const targetMetaMap = command[COMMAND_META];
|
|
21
|
+
if (!targetMetaMap) throw new Error(`TargetMeta is not defined for ${command.name}`);
|
|
22
|
+
return [...targetMetaMap.values()];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface TargetOption {
|
|
26
|
+
type: "public" | "cloud" | "dev";
|
|
27
|
+
short?: string | true;
|
|
28
|
+
devOnly?: boolean;
|
|
29
|
+
desc?: string;
|
|
30
|
+
runsOnWorkspaceRoot?: boolean;
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Cls<T = unknown> = new (...args: unknown[]) => T;
|
|
2
|
+
|
|
3
|
+
export type DependencyKind = "command" | "script" | "runner";
|
|
4
|
+
export type DependencyKey<RefName extends string, Kind extends DependencyKind> = `${RefName}${Capitalize<Kind>}`;
|
|
5
|
+
|
|
6
|
+
export type DependencyCls<T = unknown, Key extends string = string> = (new () => T) & {
|
|
7
|
+
readonly dependencyKey: Key;
|
|
8
|
+
readonly dependencyKind: DependencyKind;
|
|
9
|
+
readonly refName: string;
|
|
10
|
+
};
|
package/constants.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SupportedLlmModel } from "./aiEditor";
|
|
2
|
+
|
|
3
|
+
export const basePath = `${Bun.env.HOME ?? Bun.env.USERPROFILE}/.akan`;
|
|
4
|
+
export const configPath = `${basePath}/config.json`;
|
|
5
|
+
export const akanCloudHost =
|
|
6
|
+
process.env.AKAN_PUBLIC_OPERATION_MODE === "local" ? "http://localhost" : "https://cloud.akanjs.com";
|
|
7
|
+
export const akanCloudUrl = `${akanCloudHost}${process.env.AKAN_PUBLIC_OPERATION_MODE === "local" ? ":8282" : ""}/메ㅑ`;
|
|
8
|
+
|
|
9
|
+
export interface HostConfig {
|
|
10
|
+
auth?: {
|
|
11
|
+
token: string;
|
|
12
|
+
self: { id: string; nickname: string };
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export const defaultHostConfig: HostConfig = {};
|
|
16
|
+
export interface AkanGlobalConfig {
|
|
17
|
+
cloudHost: {
|
|
18
|
+
[key: string]: HostConfig;
|
|
19
|
+
};
|
|
20
|
+
llm: {
|
|
21
|
+
model: SupportedLlmModel;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
25
|
+
export const defaultAkanGlobalConfig: AkanGlobalConfig = { cloudHost: {}, llm: null };
|