@akanjs/devkit 3.0.0-alpha.71 → 3.0.0-alpha.72
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/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +65 -562
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +412 -0
- package/akanApp/index.ts +2 -0
- package/akanContext.ts +17 -3
- package/akanMcpContract.ts +25 -4
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/commandDecorators/command.ts +30 -6
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +45 -0
- package/executors.ts +29 -5
- package/fileEditor.ts +19 -19
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +4 -0
- package/frontendBuild/buildRouteClient.test.ts +19 -22
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/index.ts +0 -1
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.proc.ts +6 -2
- package/index.ts +0 -5
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +2 -2
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +82 -13
- package/package.json +3 -3
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +64 -0
- package/qualityScanner.ts +43 -3
- package/recipeScanner.ts +4 -1
- package/scanInfo.ts +3 -3
- package/ssrScanner.test.ts +301 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +5 -1
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/ui/ScrollList.tsx +6 -8
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -180
- package/frontendBuild/styleGuard.ts +0 -339
- package/getCredentials.ts +0 -17
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type {
|
|
3
|
+
BuilderMessage,
|
|
4
|
+
BuildPhase,
|
|
5
|
+
ChangeBatch,
|
|
6
|
+
DevBuildStatus,
|
|
7
|
+
DevChangePlan,
|
|
8
|
+
DevChangeRole,
|
|
9
|
+
} from "akanjs/server";
|
|
10
|
+
import type { IncrementalBuilderStatus } from "../incrementalBuilder";
|
|
11
|
+
|
|
12
|
+
const BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How many requests may wait for a builder that is coming back. Generous — a page load asks for
|
|
16
|
+
* several routes — but finite, so a builder that never returns cannot grow this without bound. Past
|
|
17
|
+
* it, requests are failed as they were before, which is the behaviour this limit falls back to.
|
|
18
|
+
*/
|
|
19
|
+
const HELD_BUILDER_REQUEST_LIMIT = 64;
|
|
20
|
+
|
|
21
|
+
export const BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS = 30_000;
|
|
22
|
+
|
|
23
|
+
const BUILDER_TIGHT_RSS_REPORT_LIMIT = 2;
|
|
24
|
+
|
|
25
|
+
// Far enough above the ceiling that no purge would rescue it; recycle without waiting.
|
|
26
|
+
const BUILDER_RSS_HARD_MULTIPLE = 1.5;
|
|
27
|
+
|
|
28
|
+
// A sandbox between user turns pays for a watcher that is watching nothing change. Suspending build
|
|
29
|
+
// capacity after this long returns the builder's residency until the next edit or route request.
|
|
30
|
+
const DEV_IDLE_SUSPEND_MS = 300_000;
|
|
31
|
+
|
|
32
|
+
// A wake that immediately suspends again would flap around whatever woke it.
|
|
33
|
+
const DEV_IDLE_MIN_UPTIME_MS = 30_000;
|
|
34
|
+
|
|
35
|
+
export const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
36
|
+
|
|
37
|
+
/\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav|html)$/i;
|
|
38
|
+
const SERVER_SUFFIXES = [".service.ts", ".document.ts"];
|
|
39
|
+
|
|
40
|
+
const SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
|
|
41
|
+
|
|
42
|
+
const RUNTIME_METADATA_BASENAMES = new Set(["dict.ts", "sig.ts", "useClient.ts", "useServer.ts"]);
|
|
43
|
+
|
|
44
|
+
export const shouldRestartBackendByDevPlan = (
|
|
45
|
+
message: Extract<BuilderMessage, { type: "invalidate" }>,
|
|
46
|
+
): boolean | null => {
|
|
47
|
+
if (!message.devPlan) return null;
|
|
48
|
+
if (message.devPlan.actions.includes("report-error")) return false;
|
|
49
|
+
if (message.devPlan.actions.includes("restart-builder")) return false;
|
|
50
|
+
return message.devPlan.actions.includes("restart-backend");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const shouldRestartBuilderByDevPlan = (message: Extract<BuilderMessage, { type: "invalidate" }>): boolean =>
|
|
54
|
+
message.devPlan?.actions.includes("restart-builder") ?? false;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A backend that keeps dying isn't going to heal by retrying the same code; after this many
|
|
58
|
+
* consecutive attempts the host idles and the next server-side edit triggers a fresh restart.
|
|
59
|
+
*/
|
|
60
|
+
export const shouldAbandonBackendRecovery = (attempts: number, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS): boolean =>
|
|
61
|
+
attempts >= maxAttempts;
|
|
62
|
+
|
|
63
|
+
/** The gateway reports backend failures with `generation: -1`; the host assigns its own counter then. */
|
|
64
|
+
export const normalizeBackendReportedGeneration = (generation: number): number | undefined =>
|
|
65
|
+
generation >= 0 ? generation : undefined;
|
|
66
|
+
|
|
67
|
+
export const shouldRestartDevHostByDevPlan = (message: Extract<BuilderMessage, { type: "invalidate" }>): boolean =>
|
|
68
|
+
message.devPlan?.actions.includes("restart-dev-host") ?? message.kinds.includes("config");
|
|
69
|
+
|
|
70
|
+
export type BackendLifecycleState = "starting" | "ready" | "restart-pending" | "stopping" | "recovering" | "stopped";
|
|
71
|
+
|
|
72
|
+
export interface BackendRestartReason {
|
|
73
|
+
generation?: number;
|
|
74
|
+
files: string[];
|
|
75
|
+
roles: Extract<DevChangeRole, "server" | "shared" | "barrel" | "config">[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const RESTART_ROLE_ORDER: BackendRestartReason["roles"] = ["server", "shared", "barrel", "config"];
|
|
79
|
+
|
|
80
|
+
const generationValue = (generation: number | undefined): number => generation ?? -1;
|
|
81
|
+
|
|
82
|
+
export const isLegacyBackendFallbackFile = (file: string, workspaceRoot: string): boolean => {
|
|
83
|
+
const abs = path.resolve(file);
|
|
84
|
+
const ext = path.extname(abs).toLowerCase();
|
|
85
|
+
if (!SOURCE_EXTS.has(ext)) return false;
|
|
86
|
+
const rel = path.relative(path.resolve(workspaceRoot), abs);
|
|
87
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return false;
|
|
88
|
+
const parts = rel.split(path.sep).filter(Boolean);
|
|
89
|
+
const [scope] = parts;
|
|
90
|
+
if (scope !== "apps" && scope !== "libs" && scope !== "pkgs") return false;
|
|
91
|
+
|
|
92
|
+
const base = path.basename(abs);
|
|
93
|
+
return (
|
|
94
|
+
parts.includes("srvkit") ||
|
|
95
|
+
parts.includes("common") ||
|
|
96
|
+
SERVER_SUFFIXES.some((suffix) => base.endsWith(suffix)) ||
|
|
97
|
+
SHARED_SUFFIXES.some((suffix) => base.endsWith(suffix)) ||
|
|
98
|
+
RUNTIME_METADATA_BASENAMES.has(base) ||
|
|
99
|
+
base === "main.ts" ||
|
|
100
|
+
base === "server.ts"
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const shouldMarkBuildPhaseRecovered = (
|
|
105
|
+
previousByPhase: ReadonlyMap<BuildPhase, DevBuildStatus>,
|
|
106
|
+
status: DevBuildStatus,
|
|
107
|
+
): boolean => {
|
|
108
|
+
const previous = previousByPhase.get(status.phase);
|
|
109
|
+
return Boolean(previous && status.ok && !previous.ok && generationValue(status.generation) >= previous.generation);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const createBackendBuildStatus = ({
|
|
113
|
+
generation,
|
|
114
|
+
ok,
|
|
115
|
+
files = [],
|
|
116
|
+
message,
|
|
117
|
+
}: {
|
|
118
|
+
generation: number;
|
|
119
|
+
ok: boolean;
|
|
120
|
+
files?: string[];
|
|
121
|
+
message?: string;
|
|
122
|
+
}): DevBuildStatus => ({
|
|
123
|
+
generation,
|
|
124
|
+
phase: "backend",
|
|
125
|
+
ok,
|
|
126
|
+
files,
|
|
127
|
+
message,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export const backendRestartReasonFromMessage = (
|
|
131
|
+
message: Extract<BuilderMessage, { type: "invalidate" }>,
|
|
132
|
+
): BackendRestartReason => {
|
|
133
|
+
const roleSet = new Set<BackendRestartReason["roles"][number]>();
|
|
134
|
+
for (const role of message.devPlan?.roles ?? []) {
|
|
135
|
+
if (role === "server" || role === "shared" || role === "barrel" || role === "config") roleSet.add(role);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
generation: message.devPlan?.generation ?? message.generation,
|
|
139
|
+
files: [...new Set(message.files)].sort(),
|
|
140
|
+
roles: RESTART_ROLE_ORDER.filter((role) => roleSet.has(role)),
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const mergeBackendRestartReasons = (
|
|
145
|
+
current: BackendRestartReason | null,
|
|
146
|
+
next: BackendRestartReason,
|
|
147
|
+
): BackendRestartReason => ({
|
|
148
|
+
generation:
|
|
149
|
+
generationValue(next.generation) >= generationValue(current?.generation) ? next.generation : current?.generation,
|
|
150
|
+
files: [...new Set([...(current?.files ?? []), ...next.files])].sort(),
|
|
151
|
+
roles: RESTART_ROLE_ORDER.filter((role) => current?.roles.includes(role) || next.roles.includes(role)),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export const shouldReplaceLastGoodMessage = (
|
|
155
|
+
current:
|
|
156
|
+
| Extract<BuilderMessage, { type: "pages-updated" }>
|
|
157
|
+
| Extract<BuilderMessage, { type: "css-updated" }>
|
|
158
|
+
| undefined,
|
|
159
|
+
next: Extract<BuilderMessage, { type: "pages-updated" }> | Extract<BuilderMessage, { type: "css-updated" }>,
|
|
160
|
+
): boolean => !current || generationValue(next.data.generation) >= generationValue(current.data.generation);
|
|
161
|
+
|
|
162
|
+
export const shouldQueueBuildStatusReplay = (backendReady: boolean, pendingReplayCount: number): boolean =>
|
|
163
|
+
!backendReady || pendingReplayCount > 0;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Recycling the builder/backend on a generation whose build already failed is guaranteed to strand
|
|
167
|
+
* the dev server: the rebooted builder hits the same compile error and exits before builder-ready.
|
|
168
|
+
* Failing phase statuses for a generation arrive over IPC before that generation's invalidate, so
|
|
169
|
+
* the host can check them here and defer the recycle until a healthy batch lands.
|
|
170
|
+
*/
|
|
171
|
+
export const hasBuildFailureForGeneration = (
|
|
172
|
+
statusByPhase: ReadonlyMap<BuildPhase, DevBuildStatus>,
|
|
173
|
+
generation: number | undefined,
|
|
174
|
+
): boolean => {
|
|
175
|
+
if (typeof generation !== "number") return false;
|
|
176
|
+
for (const status of statusByPhase.values()) {
|
|
177
|
+
if (!status.ok && status.generation === generation) return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export type BuilderRssRecycleDecision = "unbounded" | "below-ceiling" | "build-failed" | "too-soon" | "recycle";
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Whether an over-ceiling builder should be replaced now.
|
|
186
|
+
*
|
|
187
|
+
* `Bun.build` never returns its native arenas, so the builder's RSS only comes back when the process
|
|
188
|
+
* exits. Recycling it is therefore the only bound available — but it costs a boot build, so the two
|
|
189
|
+
* cases where a recycle cannot help are excluded: a generation whose build already failed (the
|
|
190
|
+
* replacement would hit the same compile error), and a recycle so soon after the last one that the
|
|
191
|
+
* ceiling is evidently unreachable for this app.
|
|
192
|
+
*/
|
|
193
|
+
export const decideBuilderRssRecycle = ({
|
|
194
|
+
rssBytes,
|
|
195
|
+
ceilingBytes,
|
|
196
|
+
buildFailed,
|
|
197
|
+
msSinceLastRecycle,
|
|
198
|
+
minIntervalMs = BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS,
|
|
199
|
+
}: {
|
|
200
|
+
rssBytes: number;
|
|
201
|
+
ceilingBytes: number | null;
|
|
202
|
+
buildFailed: boolean;
|
|
203
|
+
msSinceLastRecycle: number | null;
|
|
204
|
+
minIntervalMs?: number;
|
|
205
|
+
}): BuilderRssRecycleDecision => {
|
|
206
|
+
if (!ceilingBytes) return "unbounded";
|
|
207
|
+
if (rssBytes < ceilingBytes) return "below-ceiling";
|
|
208
|
+
if (buildFailed) return "build-failed";
|
|
209
|
+
if (msSinceLastRecycle !== null && msSinceLastRecycle < minIntervalMs) return "too-soon";
|
|
210
|
+
return "recycle";
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export type BuilderRssSettleDecision = "recycle-now" | "wait-and-recheck";
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Whether an armed recycle should wait out the allocator's purge window before committing. A builder
|
|
217
|
+
* far enough over the ceiling is not going to be rescued by a purge, so waiting there only delays a
|
|
218
|
+
* recycle that has to happen.
|
|
219
|
+
*/
|
|
220
|
+
export const decideBuilderRssSettle = ({
|
|
221
|
+
rssBytes,
|
|
222
|
+
ceilingBytes,
|
|
223
|
+
hardMultiple = BUILDER_RSS_HARD_MULTIPLE,
|
|
224
|
+
}: {
|
|
225
|
+
rssBytes: number;
|
|
226
|
+
ceilingBytes: number;
|
|
227
|
+
hardMultiple?: number;
|
|
228
|
+
}): BuilderRssSettleDecision => (rssBytes >= ceilingBytes * hardMultiple ? "recycle-now" : "wait-and-recheck");
|
|
229
|
+
|
|
230
|
+
export type IdleSuspendDecision =
|
|
231
|
+
| "disabled"
|
|
232
|
+
| "already-suspended"
|
|
233
|
+
| "builder-not-ready"
|
|
234
|
+
| "backend-not-ready"
|
|
235
|
+
| "build-failed"
|
|
236
|
+
| "restart-pending"
|
|
237
|
+
| "too-soon"
|
|
238
|
+
| "suspend";
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Whether the dev host may drop its build capacity now. Every "no" here is a case where suspending
|
|
242
|
+
* would either lose work or produce a wake that immediately re-suspends:
|
|
243
|
+
*
|
|
244
|
+
* - a red build means the developer is mid-fix and about to save again, and a wake would boot straight
|
|
245
|
+
* back into the same error via the degraded-boot path
|
|
246
|
+
* - a pending restart/recovery already has its own plan for the builder
|
|
247
|
+
* - `too-soon` keeps a wake from flapping around whatever triggered it
|
|
248
|
+
*/
|
|
249
|
+
export const decideIdleSuspend = ({
|
|
250
|
+
enabled,
|
|
251
|
+
suspended,
|
|
252
|
+
builderReady,
|
|
253
|
+
backendReady,
|
|
254
|
+
buildFailed,
|
|
255
|
+
restartPending,
|
|
256
|
+
msSinceWake,
|
|
257
|
+
minUptimeMs = DEV_IDLE_MIN_UPTIME_MS,
|
|
258
|
+
}: {
|
|
259
|
+
enabled: boolean;
|
|
260
|
+
suspended: boolean;
|
|
261
|
+
builderReady: boolean;
|
|
262
|
+
backendReady: boolean;
|
|
263
|
+
buildFailed: boolean;
|
|
264
|
+
restartPending: boolean;
|
|
265
|
+
msSinceWake: number | null;
|
|
266
|
+
minUptimeMs?: number;
|
|
267
|
+
}): IdleSuspendDecision => {
|
|
268
|
+
if (!enabled) return "disabled";
|
|
269
|
+
if (suspended) return "already-suspended";
|
|
270
|
+
if (!builderReady) return "builder-not-ready";
|
|
271
|
+
if (!backendReady) return "backend-not-ready";
|
|
272
|
+
if (buildFailed) return "build-failed";
|
|
273
|
+
if (restartPending) return "restart-pending";
|
|
274
|
+
if (msSinceWake !== null && msSinceWake < minUptimeMs) return "too-soon";
|
|
275
|
+
return "suspend";
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/** `undefined` env means the default is on; any non-positive value turns idle suspend off. */
|
|
279
|
+
export const resolveIdleSuspendMs = (raw: string | undefined): number | null => {
|
|
280
|
+
if (raw === undefined || raw === "") return DEV_IDLE_SUSPEND_MS;
|
|
281
|
+
const parsed = Number(raw);
|
|
282
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
|
283
|
+
return Math.round(parsed);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/** Any red phase blocks a suspend, unlike the rss recycle which only cares about one generation. */
|
|
287
|
+
export const hasAnyBuildFailure = (statusByPhase: ReadonlyMap<BuildPhase, DevBuildStatus>): boolean =>
|
|
288
|
+
[...statusByPhase.values()].some((status) => !status.ok);
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* A config change while suspended cannot be applied by restarting the builder alone — the dev host
|
|
292
|
+
* itself has to re-read the config, which is the same path an ordinary config save takes.
|
|
293
|
+
*/
|
|
294
|
+
export const shouldRefreshConfigOnIdleWake = (batch: ChangeBatch | null): boolean =>
|
|
295
|
+
!!batch && batch.kinds.has("config");
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Whether a request that arrived while the builder was away should wait for the one coming back.
|
|
299
|
+
*
|
|
300
|
+
* A recycle or a crash-restart is a gap, not a failure — the request that lands in it is the page a
|
|
301
|
+
* developer is waiting on. A stopped builder is a different thing: nothing is bringing it back, so
|
|
302
|
+
* waiting would only delay the error.
|
|
303
|
+
*
|
|
304
|
+
* `recycling` is in here for the same reason as `restarting`, and was the hole this decision shipped
|
|
305
|
+
* with: a draining builder is still alive, so the request reached it and came back refused while the
|
|
306
|
+
* host still thought there was nothing to wait for.
|
|
307
|
+
*/
|
|
308
|
+
const RETURNING_BUILDER_STATUSES = new Set<IncrementalBuilderStatus>(["starting", "recycling", "restarting"]);
|
|
309
|
+
export const shouldHoldForReturningBuilder = ({
|
|
310
|
+
status,
|
|
311
|
+
heldCount,
|
|
312
|
+
limit = HELD_BUILDER_REQUEST_LIMIT,
|
|
313
|
+
}: {
|
|
314
|
+
status: IncrementalBuilderStatus;
|
|
315
|
+
heldCount: number;
|
|
316
|
+
limit?: number;
|
|
317
|
+
}): boolean => RETURNING_BUILDER_STATUSES.has(status) && heldCount < limit;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Whether a builder that is over the ceiling again this soon after being replaced is worth saying so
|
|
321
|
+
* about, once. Not a reason to stop enforcing the ceiling: the minimum interval already bounds what
|
|
322
|
+
* this costs at one recycle per interval, and dropping the bound is how a container gets OOM-killed.
|
|
323
|
+
*
|
|
324
|
+
* This used to disable the ceiling for the session, on a count that a single page load reaches — two
|
|
325
|
+
* route builds, two reports, both inside the interval. That is normal work on any app whose builds sit
|
|
326
|
+
* above the ceiling, which is the same app the ceiling was derived for.
|
|
327
|
+
*/
|
|
328
|
+
export const shouldWarnBuilderRssCeilingTight = (
|
|
329
|
+
reportsSinceRecycle: number,
|
|
330
|
+
limit = BUILDER_TIGHT_RSS_REPORT_LIMIT,
|
|
331
|
+
): boolean => reportsSinceRecycle >= limit;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Whether recycling can ever bring this builder under the ceiling.
|
|
335
|
+
*
|
|
336
|
+
* Measured on a replacement the moment it is ready, before it has built anything on demand: that is
|
|
337
|
+
* the floor every future replacement lands on, so a floor already over the ceiling is the one case
|
|
338
|
+
* where the recycle loop is pure cost. It is also the case the escape hatch was always described as
|
|
339
|
+
* being for — the previous rule inferred it from report timing and caught ordinary work instead.
|
|
340
|
+
*/
|
|
341
|
+
export const isRssCeilingUnreachable = (freshRssBytes: number | null, ceilingBytes: number | null): boolean =>
|
|
342
|
+
freshRssBytes !== null && ceilingBytes !== null && freshRssBytes >= ceilingBytes;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Whether a recycled builder's re-announced boot artifact actually differs from what the backend
|
|
346
|
+
* already has. Both payload identities are content hashes — `pages-[hash].js` and
|
|
347
|
+
* `<name>-[hash].css` — so an unchanged recycle produces identical ones and needs no reload. Only a
|
|
348
|
+
* save that raced the recycle moves them, and that is the case worth pushing.
|
|
349
|
+
*/
|
|
350
|
+
export const shouldRelayRecycledFrontendState = (
|
|
351
|
+
current:
|
|
352
|
+
| Extract<BuilderMessage, { type: "pages-updated" }>
|
|
353
|
+
| Extract<BuilderMessage, { type: "css-updated" }>
|
|
354
|
+
| undefined,
|
|
355
|
+
next: Extract<BuilderMessage, { type: "pages-updated" }> | Extract<BuilderMessage, { type: "css-updated" }>,
|
|
356
|
+
): boolean => {
|
|
357
|
+
if (!current || current.type !== next.type) return true;
|
|
358
|
+
if (current.type === "pages-updated" && next.type === "pages-updated")
|
|
359
|
+
return current.data.bundlePath !== next.data.bundlePath;
|
|
360
|
+
if (current.type === "css-updated" && next.type === "css-updated")
|
|
361
|
+
return JSON.stringify(current.data.cssAssets) !== JSON.stringify(next.data.cssAssets);
|
|
362
|
+
return true;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const mergeDevPlans = (current?: DevChangePlan, next?: DevChangePlan): DevChangePlan | undefined => {
|
|
366
|
+
if (!current) return next;
|
|
367
|
+
if (!next) return current;
|
|
368
|
+
const reasonByFile: Record<string, string[]> = { ...current.reasonByFile };
|
|
369
|
+
for (const [file, reasons] of Object.entries(next.reasonByFile)) {
|
|
370
|
+
reasonByFile[file] = [...new Set([...(reasonByFile[file] ?? []), ...reasons])].sort();
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
generation: Math.max(current.generation, next.generation),
|
|
374
|
+
files: [...new Set([...current.files, ...next.files])].sort(),
|
|
375
|
+
generatedFiles: [...new Set([...current.generatedFiles, ...next.generatedFiles])].sort(),
|
|
376
|
+
roles: [...new Set([...current.roles, ...next.roles])].sort(),
|
|
377
|
+
actions: [...new Set([...current.actions, ...next.actions])].sort(),
|
|
378
|
+
reasonByFile,
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/** A deferred recycle accumulates every batch it skipped so the eventual restart covers them all. */
|
|
383
|
+
export const mergeInvalidateMessages = (
|
|
384
|
+
current: Extract<BuilderMessage, { type: "invalidate" }>,
|
|
385
|
+
next: Extract<BuilderMessage, { type: "invalidate" }>,
|
|
386
|
+
): Extract<BuilderMessage, { type: "invalidate" }> => {
|
|
387
|
+
const generation = Math.max(generationValue(current.generation), generationValue(next.generation));
|
|
388
|
+
return {
|
|
389
|
+
type: "invalidate",
|
|
390
|
+
kinds: [...new Set([...current.kinds, ...next.kinds])].sort(),
|
|
391
|
+
files: [...new Set([...current.files, ...next.files])].sort(),
|
|
392
|
+
generation: generation >= 0 ? generation : undefined,
|
|
393
|
+
devPlan: mergeDevPlans(current.devPlan, next.devPlan),
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
export const buildStatusReplaySequence = (
|
|
398
|
+
pendingReplay: readonly DevBuildStatus[],
|
|
399
|
+
latestByPhase: ReadonlyMap<BuildPhase, DevBuildStatus>,
|
|
400
|
+
): DevBuildStatus[] => [...pendingReplay, ...latestByPhase.values()];
|
|
401
|
+
|
|
402
|
+
/** `(mtimeMs, size)` per file — what a save moves, and what a rebuild of identical content does not. */
|
|
403
|
+
export type SourceFingerprints = ReadonlyMap<string, string>;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Which of the files in `before` are no longer stamped the way they were.
|
|
407
|
+
*
|
|
408
|
+
* Only files present in `before` are compared. The question this answers is which *running* code went
|
|
409
|
+
* stale while nothing was watching, and a file that did not exist then is not running anywhere.
|
|
410
|
+
*/
|
|
411
|
+
export const filesChangedSince = (before: SourceFingerprints, after: SourceFingerprints): string[] =>
|
|
412
|
+
[...before].filter(([file, stamp]) => after.get(file) !== stamp).map(([file]) => file);
|
package/akanApp/index.ts
CHANGED
package/akanContext.ts
CHANGED
|
@@ -130,15 +130,29 @@ export type CursorMcpConfig = {
|
|
|
130
130
|
mcpServers?: Record<string, unknown>;
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
-
export const
|
|
133
|
+
export const guidelineResourceUri = (name: string) => `akan://guidelines/${name}`;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resources that exist whatever the workspace holds. The guideline entries are **not** here: they are
|
|
137
|
+
* one per directory under `cli/guidelines/`, and a hardcoded pair listed 2 of 31 while `readResource`
|
|
138
|
+
* threw on the other 29. `buildResourceList` is the only place the two halves meet.
|
|
139
|
+
*/
|
|
140
|
+
const staticResourceList = [
|
|
134
141
|
{ uri: "akan://docs/framework", name: "Akan framework guide", mimeType: "text/markdown" },
|
|
135
|
-
{ uri: "akan://guidelines/framework", name: "Framework guideline", mimeType: "text/markdown" },
|
|
136
|
-
{ uri: "akan://guidelines/modelSignal", name: "Model signal guideline", mimeType: "text/markdown" },
|
|
137
142
|
{ uri: "akan://workspace/summary", name: "Workspace summary", mimeType: "application/json" },
|
|
138
143
|
{ uri: "akan://workspace/apps", name: "Workspace apps", mimeType: "application/json" },
|
|
139
144
|
{ uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" },
|
|
140
145
|
];
|
|
141
146
|
|
|
147
|
+
export const buildResourceList = (guidelineNames: readonly string[]) => [
|
|
148
|
+
...staticResourceList,
|
|
149
|
+
...guidelineNames.map((name) => ({
|
|
150
|
+
uri: guidelineResourceUri(name),
|
|
151
|
+
name: `${name} guideline`,
|
|
152
|
+
mimeType: "text/markdown",
|
|
153
|
+
})),
|
|
154
|
+
];
|
|
155
|
+
|
|
142
156
|
export const cursorMcpConfigPath = ".cursor/mcp.json";
|
|
143
157
|
// Claude Code reads project-scoped MCP servers from `.mcp.json` at the workspace root.
|
|
144
158
|
export const claudeMcpConfigPath = ".mcp.json";
|
package/akanMcpContract.ts
CHANGED
|
@@ -391,10 +391,31 @@ export const applyMcpTools: McpToolDefinition[] = [
|
|
|
391
391
|
},
|
|
392
392
|
];
|
|
393
393
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
394
|
+
/**
|
|
395
|
+
* `guidelineNames` fills the `get_guideline` enum. Without it the argument is a bare string and a model
|
|
396
|
+
* has to guess one of the ~30 directory names, which it cannot see from here — the names are a
|
|
397
|
+
* filesystem listing, so they can only arrive from a caller that already read it.
|
|
398
|
+
*/
|
|
399
|
+
export const listAkanMcpTools = (
|
|
400
|
+
mode: "readonly" | "plan" | "apply" = "readonly",
|
|
401
|
+
{ guidelineNames = [] }: { guidelineNames?: readonly string[] } = {},
|
|
402
|
+
) => {
|
|
403
|
+
const tools =
|
|
404
|
+
mode === "readonly"
|
|
405
|
+
? readonlyMcpTools
|
|
406
|
+
: mode === "plan"
|
|
407
|
+
? [...readonlyMcpTools, ...planMcpTools]
|
|
408
|
+
: [...readonlyMcpTools, ...planMcpTools, ...applyMcpTools];
|
|
409
|
+
if (guidelineNames.length === 0) return tools;
|
|
410
|
+
return tools.map((tool) =>
|
|
411
|
+
tool.name === "get_guideline"
|
|
412
|
+
? {
|
|
413
|
+
...tool,
|
|
414
|
+
description: `${tool.description ?? ""} One of: ${guidelineNames.join(", ")}.`.trim(),
|
|
415
|
+
inputSchema: { ...tool.inputSchema, properties: { name: { type: "string", enum: [...guidelineNames] } } },
|
|
416
|
+
}
|
|
417
|
+
: tool,
|
|
418
|
+
);
|
|
398
419
|
};
|
|
399
420
|
|
|
400
421
|
export const createAkanValidationContract = (listTools = listAkanMcpTools) => ({
|
package/capacitorApp.ts
CHANGED
|
@@ -123,6 +123,16 @@ interface MaterializeCapacitorConfigOptions {
|
|
|
123
123
|
}
|
|
124
124
|
type MobilePlatform = "ios" | "android";
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* `AppExecutor.spawn`'s own options plus the two the Capacitor config is written from before the spawn:
|
|
128
|
+
* the platform the command targets, and — for iOS — whether it targets a device or a simulator, which
|
|
129
|
+
* decide the `capacitor.config.json` that command reads.
|
|
130
|
+
*/
|
|
131
|
+
type SpawnMobileOptions = Parameters<AppExecutor["spawn"]>[2] & {
|
|
132
|
+
platform?: MobilePlatform;
|
|
133
|
+
iosRunTargetKind?: IosRunTargetKind;
|
|
134
|
+
};
|
|
135
|
+
|
|
126
136
|
export interface LocalDevHostResolution {
|
|
127
137
|
host: string;
|
|
128
138
|
source: "override" | "detected" | "loopback" | "platform";
|
package/cloud/globalConfig.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
1
|
+
import { chmod, mkdir } from "node:fs/promises";
|
|
2
2
|
import dayjs from "dayjs";
|
|
3
3
|
import { FileSys } from "../fileSys";
|
|
4
4
|
import {
|
|
@@ -29,9 +29,16 @@ export class GlobalConfig {
|
|
|
29
29
|
remoteEnvServers: akanConfig.remoteEnvServers ?? defaultAkanGlobalConfig.remoteEnvServers,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* This file holds the cloud jwt, a refresh token that does not expire, and the LLM api key, so it is
|
|
34
|
+
* written owner-only — the same `0600` the runtime gives its control socket. `Bun.write` takes no mode
|
|
35
|
+
* and lands on `0666 & ~umask` (0644 on a default shell), so the mode is applied after the write; an
|
|
36
|
+
* existing world-readable file is tightened by the next write rather than left as it was found.
|
|
37
|
+
*/
|
|
32
38
|
static async #setAkanGlobalConfig(akanConfig: AkanGlobalConfig) {
|
|
33
|
-
await mkdir(basePath, { recursive: true });
|
|
39
|
+
await mkdir(basePath, { recursive: true, mode: 0o700 });
|
|
34
40
|
await Bun.write(configPath, JSON.stringify(akanConfig, null, 2));
|
|
41
|
+
await chmod(configPath, 0o600);
|
|
35
42
|
}
|
|
36
43
|
static async getHostConfig(host = GlobalConfig.akanCloudHost): Promise<HostConfig> {
|
|
37
44
|
const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
|
|
@@ -104,6 +104,26 @@ const select = ((config, context) => prompts().then((m) => m.select(config, cont
|
|
|
104
104
|
const confirm = ((config, context) => prompts().then((m) => m.confirm(config, context))) as typeof inquirerConfirm;
|
|
105
105
|
const input = ((config, context) => prompts().then((m) => m.input(config, context))) as typeof inquirerInput;
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Rejects a value that is not one of the declared choices, in commander's own wording.
|
|
109
|
+
*
|
|
110
|
+
* Only a static choice list can be checked: a `DynamicEnum` resolves against the command context, which
|
|
111
|
+
* is not populated until the internal args are resolved, and it is the interactive `select` that consumes
|
|
112
|
+
* it. Comparison is stringly on purpose — the value still carries commander's raw string here, while a
|
|
113
|
+
* numeric choice list holds numbers.
|
|
114
|
+
*/
|
|
115
|
+
const assertEnumChoice = (argMeta: ArgMeta, value: unknown) => {
|
|
116
|
+
const enumChoices = argMeta.argsOption.enum;
|
|
117
|
+
if (!enumChoices || typeof enumChoices === "function") return;
|
|
118
|
+
const choices = normalizeEnumChoices(enumChoices);
|
|
119
|
+
if (choices.some((choice) => String(choice.value) === String(value))) return;
|
|
120
|
+
const label =
|
|
121
|
+
argMeta.type === "Option" ? `option '--${camelToKebabCase(argMeta.name)}'` : `argument '${argMeta.name}'`;
|
|
122
|
+
throw new Error(
|
|
123
|
+
`${label} argument '${String(value)}' is invalid. Allowed choices are ${choices.map((choice) => choice.name).join(", ")}.`,
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
107
127
|
const resolveEnumChoices = async (argMeta: ArgMeta, context: CommandContext) => {
|
|
108
128
|
const enumChoices = argMeta.argsOption.enum;
|
|
109
129
|
if (!enumChoices) return null;
|
|
@@ -111,13 +131,15 @@ const resolveEnumChoices = async (argMeta: ArgMeta, context: CommandContext) =>
|
|
|
111
131
|
return enumChoices;
|
|
112
132
|
};
|
|
113
133
|
|
|
114
|
-
const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unknown>, context: CommandContext) => {
|
|
134
|
+
export const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unknown>, context: CommandContext) => {
|
|
115
135
|
const {
|
|
116
136
|
name,
|
|
117
137
|
argsOption: { enum: enumChoices, default: defaultValue, type, desc, nullable, example, ask },
|
|
118
138
|
} = argMeta;
|
|
119
|
-
if (opt[argMeta.name] !== undefined)
|
|
120
|
-
|
|
139
|
+
if (opt[argMeta.name] !== undefined) {
|
|
140
|
+
assertEnumChoice(argMeta, opt[argMeta.name]);
|
|
141
|
+
return convertArgValue(opt[argMeta.name] as string, type ?? "string");
|
|
142
|
+
} else if (defaultValue !== undefined) return defaultValue;
|
|
121
143
|
|
|
122
144
|
if (enumChoices) {
|
|
123
145
|
const choices = normalizeEnumChoices((await resolveEnumChoices(argMeta, context)) ?? []);
|
|
@@ -139,13 +161,15 @@ const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unknown>, co
|
|
|
139
161
|
}
|
|
140
162
|
};
|
|
141
163
|
|
|
142
|
-
const getArgumentValue = async (argMeta: ArgMeta, value: string | undefined) => {
|
|
164
|
+
export const getArgumentValue = async (argMeta: ArgMeta, value: string | undefined) => {
|
|
143
165
|
const {
|
|
144
166
|
name,
|
|
145
167
|
argsOption: { default: defaultValue, type, desc, nullable, example, ask },
|
|
146
168
|
} = argMeta;
|
|
147
|
-
if (value !== undefined)
|
|
148
|
-
|
|
169
|
+
if (value !== undefined) {
|
|
170
|
+
assertEnumChoice(argMeta, value);
|
|
171
|
+
return convertArgValue(value, type ?? "string");
|
|
172
|
+
} else if (defaultValue !== undefined) return defaultValue;
|
|
149
173
|
else if (nullable) return null;
|
|
150
174
|
|
|
151
175
|
const message = ask
|
|
@@ -23,9 +23,24 @@ type PrimitiveValue<T extends PrimitiveArgType> = T extends StringConstructor
|
|
|
23
23
|
? number
|
|
24
24
|
: boolean;
|
|
25
25
|
type MaybeNullable<Value, Option> = Option extends { nullable: true } ? Value | null : Value;
|
|
26
|
+
/**
|
|
27
|
+
* The literal union a static `enum` declares, or `never` for a `DynamicEnum` — a function does not extend
|
|
28
|
+
* a readonly array, so a runtime-resolved choice list falls back to the primitive type. `{ label, value }`
|
|
29
|
+
* choices contribute their `value`.
|
|
30
|
+
*/
|
|
31
|
+
type EnumValue<Option> = Option extends { enum: infer Choices }
|
|
32
|
+
? Choices extends readonly (infer Choice)[]
|
|
33
|
+
? Choice extends { value: infer Value }
|
|
34
|
+
? Value
|
|
35
|
+
: Choice
|
|
36
|
+
: never
|
|
37
|
+
: never;
|
|
38
|
+
type ArgValue<Type extends PrimitiveArgType, Option> = [EnumValue<Option>] extends [never]
|
|
39
|
+
? PrimitiveValue<Type>
|
|
40
|
+
: EnumValue<Option>;
|
|
26
41
|
type AddArg<Params extends unknown[], Type extends PrimitiveArgType, Option> = [
|
|
27
42
|
...Params,
|
|
28
|
-
MaybeNullable<
|
|
43
|
+
MaybeNullable<ArgValue<Type, Option>, Option>,
|
|
29
44
|
];
|
|
30
45
|
type AddInternalArg<Params extends unknown[], Token extends InternalArgToken> = [...Params, Token["_value"]];
|
|
31
46
|
type AddInternalArgs<Params extends unknown[], Tokens extends readonly InternalArgToken[]> = Tokens extends readonly [
|
|
@@ -68,7 +83,7 @@ class TargetBuilder<Deps extends readonly DependencyCls[], Params extends unknow
|
|
|
68
83
|
this.#args = args;
|
|
69
84
|
}
|
|
70
85
|
|
|
71
|
-
arg<Type extends PrimitiveArgType, Option extends ArgsOption<Context> = ArgsOption<Context>>(
|
|
86
|
+
arg<Type extends PrimitiveArgType, const Option extends ArgsOption<Context> = ArgsOption<Context>>(
|
|
72
87
|
name: string,
|
|
73
88
|
type: Type,
|
|
74
89
|
argsOption: Option = {} as Option,
|
|
@@ -85,7 +100,7 @@ class TargetBuilder<Deps extends readonly DependencyCls[], Params extends unknow
|
|
|
85
100
|
]);
|
|
86
101
|
}
|
|
87
102
|
|
|
88
|
-
option<Type extends PrimitiveArgType, Option extends ArgsOption<Context> = ArgsOption<Context>>(
|
|
103
|
+
option<Type extends PrimitiveArgType, const Option extends ArgsOption<Context> = ArgsOption<Context>>(
|
|
89
104
|
name: string,
|
|
90
105
|
type: Type,
|
|
91
106
|
argsOption: Option = {} as Option,
|
|
@@ -135,12 +150,12 @@ type CommandBuilderContext<Deps extends readonly DependencyCls[]> = {
|
|
|
135
150
|
public: (targetOption?: Omit<TargetOption, "type">) => TargetBuilder<Deps>;
|
|
136
151
|
cloud: (targetOption?: Omit<TargetOption, "type">) => TargetBuilder<Deps>;
|
|
137
152
|
dev: (targetOption?: Omit<TargetOption, "type">) => TargetBuilder<Deps>;
|
|
138
|
-
arg: <Type extends PrimitiveArgType, Option extends ArgsOption<CommandContext> = ArgsOption<CommandContext>>(
|
|
153
|
+
arg: <Type extends PrimitiveArgType, const Option extends ArgsOption<CommandContext> = ArgsOption<CommandContext>>(
|
|
139
154
|
name: string,
|
|
140
155
|
type: Type,
|
|
141
156
|
argsOption?: Option,
|
|
142
157
|
) => ArgMeta;
|
|
143
|
-
option: <Type extends PrimitiveArgType, Option extends ArgsOption<CommandContext> = ArgsOption<CommandContext>>(
|
|
158
|
+
option: <Type extends PrimitiveArgType, const Option extends ArgsOption<CommandContext> = ArgsOption<CommandContext>>(
|
|
144
159
|
name: string,
|
|
145
160
|
type: Type,
|
|
146
161
|
argsOption?: Option,
|