@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.20
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.test.ts +6 -15
- package/abstractDoc.ts +1 -4
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +1 -3
- package/akanApp/akanApp.host.ts +64 -250
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +30 -39
- package/akanConfig/akanConfig.ts +40 -136
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.ts +25 -59
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +3 -19
- package/artifact/implicitRootLayout.ts +34 -91
- package/artifact/routeSeedIndex.ts +11 -44
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +4 -14
- package/capacitorApp.ts +86 -235
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +33 -57
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +3 -2
- package/cloud/globalConfig.ts +64 -31
- package/codeAgent/agent/AkanCodeServices.ts +15 -57
- package/codeAgent/agent/AkanContextFiles.ts +5 -28
- package/codeAgent/agent/CodeAgent.ts +33 -108
- package/codeAgent/agent/CodeAgentAsks.ts +7 -30
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
- package/codeAgent/agent/CodeAgentGate.ts +8 -34
- package/codeAgent/agent/CodeAgentProxy.ts +4 -13
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
- package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
- package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
- package/codeAgent/agent/CodeAgentUi.ts +2 -7
- package/codeAgent/agent/CodeMailbox.ts +7 -34
- package/codeAgent/agent/CodeSessionFork.ts +4 -19
- package/codeAgent/agent/CodeSessionIndex.ts +5 -31
- package/codeAgent/agent/SubagentPool.ts +48 -58
- package/codeAgent/agent/akanCodeModel.ts +5 -39
- package/codeAgent/agent/akanCodePaths.ts +4 -24
- package/codeAgent/agent/akanContextFiles.test.ts +0 -2
- package/codeAgent/agent/akanSystemPrompt.ts +1 -9
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +5 -28
- package/codeAgent/agent/codeMailbox.test.ts +0 -4
- package/codeAgent/agent/codeSessionFork.test.ts +0 -2
- package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
- package/codeAgent/agent/consoleToStderr.ts +2 -12
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +3 -18
- package/codeAgent/feedback/PreviewChrome.ts +5 -29
- package/codeAgent/feedback/PreviewFeedback.ts +2 -9
- package/codeAgent/feedback/PreviewView.ts +3 -15
- package/codeAgent/feedback/TurnFeedback.ts +2 -17
- package/codeAgent/feedback/VerifyFeedback.ts +1 -7
- package/codeAgent/feedback/previewChrome.test.ts +1 -1
- package/codeAgent/index.ts +2 -10
- package/codeAgent/skills/akan-code/SKILL.md +12 -4
- package/codeAgent/tools/AkanCli.ts +4 -16
- package/codeAgent/tools/AkanCodePlugins.ts +21 -43
- package/codeAgent/tools/AkanEditScope.ts +2 -12
- package/codeAgent/tools/AkanEnvKeys.ts +3 -11
- package/codeAgent/tools/AkanToolPack.ts +6 -28
- package/codeAgent/tools/AkanVerifier.ts +1 -8
- package/codeAgent/tools/AskToolPack.ts +3 -15
- package/codeAgent/tools/MailToolPack.ts +0 -9
- package/codeAgent/tools/McpClient.ts +3 -24
- package/codeAgent/tools/McpOAuth.ts +6 -41
- package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
- package/codeAgent/tools/McpServerConfig.ts +7 -26
- package/codeAgent/tools/McpSignIn.ts +46 -49
- package/codeAgent/tools/McpTokenStore.ts +6 -17
- package/codeAgent/tools/McpToolPack.ts +107 -35
- package/codeAgent/tools/SessionToolPack.ts +1 -11
- package/codeAgent/tools/WebToolPack.ts +1 -7
- package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
- package/codeAgent/tools/askToolPack.test.ts +0 -3
- package/codeAgent/tools/mcpOAuth.test.ts +315 -27
- package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +44 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +1 -9
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +66 -144
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +27 -78
- package/devkitUtils.test.ts +47 -19
- package/executors.test.ts +114 -60
- package/executors.ts +165 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +1 -1
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +14 -38
- package/frontendBuild/autoImportSync.ts +15 -67
- package/frontendBuild/buildRouteClient.test.ts +10 -36
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +5 -18
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +5 -13
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +27 -52
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +7 -31
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +7 -47
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/devWatchBatch.test.ts +14 -18
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
- package/index.ts +3 -15
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +9 -29
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/gritRules.test.ts +6 -23
- package/linter.test.ts +1 -2
- package/linter.ts +22 -115
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +2 -2
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +3 -14
- package/platformTest/IdleWatchedProcess.test.ts +5 -13
- package/platformTest/IdleWatchedProcess.ts +1 -5
- package/platformTest/LinuxDockerTarget.ts +20 -35
- package/platformTest/PlatformTestRun.test.ts +5 -9
- package/platformTest/PlatformTestTarget.ts +24 -2
- package/platformTest/TestSnapshot.test.ts +6 -14
- package/platformTest/TestSnapshot.ts +1 -5
- package/platformTest/WindowsSshTarget.ts +6 -22
- package/prompter.ts +3 -6
- package/qualityScanner.test.ts +6 -24
- package/qualityScanner.ts +196 -284
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.ts +7 -30
- package/scanInfo.ts +82 -214
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/subspace.test.ts +6 -25
- package/subspace.ts +31 -109
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +49 -34
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +85 -146
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/source.ts +33 -56
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
package/linter.ts
CHANGED
|
@@ -58,16 +58,8 @@ interface LintResponse {
|
|
|
58
58
|
warnings: LintMessage[];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
* Biome mixes configuration and IO diagnostics into the same stream as the report, so the output is not
|
|
65
|
-
* always JSON alone. Slicing from the first `{` to the last `}` mis-parses as soon as that leading text
|
|
66
|
-
* carries a brace of its own, which is not hypothetical: a config error printed ahead of the report made
|
|
67
|
-
* one unchanged command alternate between 2 and 10 diagnostics. So every `{` is tried as a start, its
|
|
68
|
-
* match is found by depth while string literals are skipped, and the first candidate that both parses and
|
|
69
|
-
* carries a report field wins — a brace in prose parses as nothing, and `{"a":1}` in prose is not a report.
|
|
70
|
-
*/
|
|
61
|
+
// Biome mixes config and IO diagnostics into the report stream, and that text can carry braces of its own, so every
|
|
62
|
+
// `{` is tried as a start (string literals skipped) and the first candidate that parses and carries a report field wins.
|
|
71
63
|
export const parseBiomeReport = (output: string): BiomeReport => {
|
|
72
64
|
for (let start = output.indexOf("{"); start !== -1; start = output.indexOf("{", start + 1)) {
|
|
73
65
|
const end = objectEndAt(output, start);
|
|
@@ -111,6 +103,15 @@ const asBiomeReport = (candidate: string): BiomeReport | null => {
|
|
|
111
103
|
/** Biome reads `biome.json` first and `biome.jsonc` second; only the latter may carry comments. */
|
|
112
104
|
const BIOME_CONFIG_FILES = ["biome.json", "biome.jsonc"] as const;
|
|
113
105
|
|
|
106
|
+
const emptyResult = (filePath: string): LintResult => ({
|
|
107
|
+
filePath,
|
|
108
|
+
messages: [],
|
|
109
|
+
errorCount: 0,
|
|
110
|
+
warningCount: 0,
|
|
111
|
+
fixableErrorCount: 0,
|
|
112
|
+
fixableWarningCount: 0,
|
|
113
|
+
});
|
|
114
|
+
|
|
114
115
|
export class Linter {
|
|
115
116
|
//? Bun links a package bin as `<name>.exe` (plus a `.bunx` stub) on Windows, and a bare `<name>` elsewhere.
|
|
116
117
|
static readonly biomeBinName = process.platform === "win32" ? "biome.exe" : "biome";
|
|
@@ -202,16 +203,7 @@ export class Linter {
|
|
|
202
203
|
for (const diagnostic of report.diagnostics ?? []) {
|
|
203
204
|
if (diagnostic.severity !== "error" && diagnostic.severity !== "warning") continue;
|
|
204
205
|
const diagnosticFilePath = this.#diagnosticFilePath(diagnostic, filePath);
|
|
205
|
-
const result =
|
|
206
|
-
resultsByPath.get(diagnosticFilePath) ??
|
|
207
|
-
({
|
|
208
|
-
filePath: diagnosticFilePath,
|
|
209
|
-
messages: [],
|
|
210
|
-
errorCount: 0,
|
|
211
|
-
warningCount: 0,
|
|
212
|
-
fixableErrorCount: 0,
|
|
213
|
-
fixableWarningCount: 0,
|
|
214
|
-
} satisfies LintResult);
|
|
206
|
+
const result = resultsByPath.get(diagnosticFilePath) ?? emptyResult(diagnosticFilePath);
|
|
215
207
|
const message = this.#createLintMessage(diagnostic);
|
|
216
208
|
result.messages.push(message);
|
|
217
209
|
if (message.severity === 2) result.errorCount += 1;
|
|
@@ -220,15 +212,7 @@ export class Linter {
|
|
|
220
212
|
}
|
|
221
213
|
|
|
222
214
|
return [
|
|
223
|
-
resultsByPath.get(filePath) ??
|
|
224
|
-
({
|
|
225
|
-
filePath,
|
|
226
|
-
messages: [],
|
|
227
|
-
errorCount: 0,
|
|
228
|
-
warningCount: 0,
|
|
229
|
-
fixableErrorCount: 0,
|
|
230
|
-
fixableWarningCount: 0,
|
|
231
|
-
} satisfies LintResult),
|
|
215
|
+
resultsByPath.get(filePath) ?? emptyResult(filePath),
|
|
232
216
|
...[...resultsByPath.entries()].filter(([resultPath]) => resultPath !== filePath).map(([, result]) => result),
|
|
233
217
|
];
|
|
234
218
|
}
|
|
@@ -272,22 +256,12 @@ export class Linter {
|
|
|
272
256
|
return await this.lintFile(filePath);
|
|
273
257
|
}
|
|
274
258
|
|
|
275
|
-
/**
|
|
276
|
-
* Lint a single file using Biome.
|
|
277
|
-
* @param filePath - Path to the file to lint
|
|
278
|
-
* @returns Array of Biome results in the legacy lint result shape
|
|
279
|
-
*/
|
|
280
259
|
async lintFile(filePath: string): Promise<LintResponse> {
|
|
281
260
|
const resolvedFilePath = this.#resolveFilePath(filePath);
|
|
282
261
|
if (!existsSync(resolvedFilePath)) throw new Error(`File not found: ${filePath}`);
|
|
283
262
|
return await this.#checkFile(resolvedFilePath);
|
|
284
263
|
}
|
|
285
264
|
|
|
286
|
-
/**
|
|
287
|
-
* Format lint results for console output
|
|
288
|
-
* @param results - Array of Biome results
|
|
289
|
-
* @returns Formatted string
|
|
290
|
-
*/
|
|
291
265
|
formatLintResults(results: LintResult[]): string {
|
|
292
266
|
if (results.length === 0) return "No files to lint";
|
|
293
267
|
|
|
@@ -308,7 +282,7 @@ export class Linter {
|
|
|
308
282
|
const sourceContent = readFileSync(result.filePath, "utf8");
|
|
309
283
|
sourceLines = sourceContent.split("\n");
|
|
310
284
|
} catch {
|
|
311
|
-
|
|
285
|
+
//? An unreadable source only loses its code frame.
|
|
312
286
|
}
|
|
313
287
|
}
|
|
314
288
|
|
|
@@ -321,14 +295,12 @@ export class Linter {
|
|
|
321
295
|
output.push(`\n ${icon} ${typeColor(type)}: ${message.message}${ruleInfo}`);
|
|
322
296
|
output.push(` ${chalk.gray("at")} ${result.filePath}:${chalk.bold(`${message.line}:${message.column}`)}`);
|
|
323
297
|
|
|
324
|
-
// Show source line with underline
|
|
325
298
|
if (sourceLines.length > 0 && message.line <= sourceLines.length) {
|
|
326
299
|
const sourceLine = sourceLines[message.line - 1];
|
|
327
300
|
const lineNumber = message.line.toString().padStart(5, " ");
|
|
328
301
|
|
|
329
302
|
output.push(`\n${chalk.dim(`${lineNumber} |`)} ${sourceLine}`);
|
|
330
303
|
|
|
331
|
-
// Create underline
|
|
332
304
|
const underlinePrefix = " ".repeat(message.column - 1);
|
|
333
305
|
const underlineLength = message.endColumn ? message.endColumn - message.column : 1;
|
|
334
306
|
const underline = "^".repeat(Math.max(1, underlineLength));
|
|
@@ -348,27 +320,7 @@ export class Linter {
|
|
|
348
320
|
return summary.concat(output).join("\n");
|
|
349
321
|
}
|
|
350
322
|
|
|
351
|
-
|
|
352
|
-
* Get detailed lint information
|
|
353
|
-
* @param filePath - Path to the file to lint
|
|
354
|
-
* @returns Object containing detailed lint information
|
|
355
|
-
*/
|
|
356
|
-
async getDetailedLintInfo(filePath: string): Promise<{
|
|
357
|
-
results: LintResult[];
|
|
358
|
-
details: {
|
|
359
|
-
line: number;
|
|
360
|
-
column: number;
|
|
361
|
-
message: string;
|
|
362
|
-
ruleId: string | null;
|
|
363
|
-
severity: "error" | "warning";
|
|
364
|
-
}[];
|
|
365
|
-
stats: {
|
|
366
|
-
errorCount: number;
|
|
367
|
-
warningCount: number;
|
|
368
|
-
fixableErrorCount: number;
|
|
369
|
-
fixableWarningCount: number;
|
|
370
|
-
};
|
|
371
|
-
}> {
|
|
323
|
+
async getDetailedLintInfo(filePath: string) {
|
|
372
324
|
const { results } = await this.lintFile(filePath);
|
|
373
325
|
|
|
374
326
|
const details = results.flatMap((result) =>
|
|
@@ -388,22 +340,13 @@ export class Linter {
|
|
|
388
340
|
fixableErrorCount: acc.fixableErrorCount + result.fixableErrorCount,
|
|
389
341
|
fixableWarningCount: acc.fixableWarningCount + result.fixableWarningCount,
|
|
390
342
|
}),
|
|
391
|
-
{
|
|
392
|
-
errorCount: 0,
|
|
393
|
-
warningCount: 0,
|
|
394
|
-
fixableErrorCount: 0,
|
|
395
|
-
fixableWarningCount: 0,
|
|
396
|
-
},
|
|
343
|
+
{ errorCount: 0, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0 },
|
|
397
344
|
);
|
|
398
345
|
|
|
399
346
|
return { results, details, stats };
|
|
400
347
|
}
|
|
401
348
|
|
|
402
|
-
/**
|
|
403
|
-
* Check if a file has lint errors
|
|
404
|
-
* @param filePath - Path to the file to check
|
|
405
|
-
* @returns true if there are no errors, false otherwise
|
|
406
|
-
*/
|
|
349
|
+
/** Also false when the file cannot be linted at all. */
|
|
407
350
|
async hasNoLintErrors(filePath: string): Promise<boolean> {
|
|
408
351
|
try {
|
|
409
352
|
const { results } = await this.lintFile(filePath);
|
|
@@ -413,32 +356,17 @@ export class Linter {
|
|
|
413
356
|
}
|
|
414
357
|
}
|
|
415
358
|
|
|
416
|
-
/**
|
|
417
|
-
* Get only error messages (excluding warnings)
|
|
418
|
-
* @param filePath - Path to the file to lint
|
|
419
|
-
* @returns Array of error messages
|
|
420
|
-
*/
|
|
421
359
|
async getErrors(filePath: string): Promise<LintMessage[]> {
|
|
422
360
|
const { results } = await this.lintFile(filePath);
|
|
423
361
|
return results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
|
|
424
362
|
}
|
|
425
363
|
|
|
426
|
-
/**
|
|
427
|
-
* Get only warning messages
|
|
428
|
-
* @param filePath - Path to the file to lint
|
|
429
|
-
* @returns Array of warning messages
|
|
430
|
-
*/
|
|
431
364
|
async getWarnings(filePath: string): Promise<LintMessage[]> {
|
|
432
365
|
const { results } = await this.lintFile(filePath);
|
|
433
366
|
return results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
|
|
434
367
|
}
|
|
435
368
|
|
|
436
|
-
/**
|
|
437
|
-
* Fix lint errors automatically
|
|
438
|
-
* @param filePath - Path to the file to fix
|
|
439
|
-
* @param dryRun - If true, returns the fixed content without writing to file
|
|
440
|
-
* @returns Fixed content and remaining issues
|
|
441
|
-
*/
|
|
369
|
+
/** `dryRun` returns the fixed content in `output` without writing the file. */
|
|
442
370
|
async fixFile(filePath: string, dryRun = false): Promise<LintResponse> {
|
|
443
371
|
const resolvedFilePath = this.#resolveFilePath(filePath);
|
|
444
372
|
if (!existsSync(resolvedFilePath)) throw new Error(`File not found: ${filePath}`);
|
|
@@ -454,19 +382,8 @@ export class Linter {
|
|
|
454
382
|
return { ...lintResult, fixed: stdout !== source, output: stdout };
|
|
455
383
|
}
|
|
456
384
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
* @param filePath - Path to the file
|
|
460
|
-
* @returns Biome configuration object
|
|
461
|
-
*/
|
|
462
|
-
/**
|
|
463
|
-
* Applies Biome's safe fixes to many files in one process.
|
|
464
|
-
*
|
|
465
|
-
* One spawn, not one per file: Biome's startup dominates a small file, so the per-file path turns a
|
|
466
|
-
* fifteen-file scaffold into three seconds of process launches. Paths that do not exist are dropped
|
|
467
|
-
* rather than thrown on, because the caller is usually reporting what a template just wrote and a
|
|
468
|
-
* template is allowed to decline a file.
|
|
469
|
-
*/
|
|
385
|
+
// One spawn for all files: Biome's startup dominates a small file. Missing paths are dropped (a template may
|
|
386
|
+
// decline a file).
|
|
470
387
|
async fixFiles(filePaths: string[]): Promise<{ fixed: string[] }> {
|
|
471
388
|
const resolved = filePaths.map((filePath) => this.#resolveFilePath(filePath)).filter(existsSync);
|
|
472
389
|
if (resolved.length === 0) return { fixed: [] };
|
|
@@ -492,21 +409,11 @@ export class Linter {
|
|
|
492
409
|
return JSON.parse(readFileSync(this.configPath, "utf8")) as unknown;
|
|
493
410
|
}
|
|
494
411
|
|
|
495
|
-
/**
|
|
496
|
-
* Get rules that are causing errors in a file
|
|
497
|
-
* @param filePath - Path to the file to check
|
|
498
|
-
* @returns Object mapping rule IDs to their error counts
|
|
499
|
-
*/
|
|
500
412
|
async getProblematicRules(filePath: string): Promise<Record<string, number>> {
|
|
501
413
|
const { results } = await this.lintFile(filePath);
|
|
502
414
|
const ruleCounts: Record<string, number> = {};
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
result.messages.forEach((message) => {
|
|
506
|
-
if (message.ruleId) ruleCounts[message.ruleId] = (ruleCounts[message.ruleId] || 0) + 1;
|
|
507
|
-
});
|
|
508
|
-
});
|
|
509
|
-
|
|
415
|
+
for (const message of results.flatMap((result) => result.messages))
|
|
416
|
+
if (message.ruleId) ruleCounts[message.ruleId] = (ruleCounts[message.ruleId] || 0) + 1;
|
|
510
417
|
return ruleCounts;
|
|
511
418
|
}
|
|
512
419
|
}
|
package/mobile/mobileTarget.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ResolvedMobileTarget {
|
|
|
11
11
|
|
|
12
12
|
export const MOBILE_ENVS = ["local", "debug", "develop", "main"] as const satisfies readonly MobileEnv[];
|
|
13
13
|
|
|
14
|
+
const trimSlashes = (value: string) => value.replace(/^\/+|\/+$/g, "");
|
|
15
|
+
|
|
14
16
|
export const getMobileTargets = async (app: App): Promise<ResolvedMobileTarget[]> => {
|
|
15
17
|
const config = await app.getConfig();
|
|
16
18
|
return Object.entries(config.mobile.targets).map(([name, target]) => ({ name, config: target }));
|
|
@@ -20,26 +22,23 @@ export const getMobileTargetChoices = async (app: App): Promise<string[]> => {
|
|
|
20
22
|
const config = await app.getConfig();
|
|
21
23
|
const targetNames = Object.keys(config.mobile.targets);
|
|
22
24
|
if (targetNames.length > 0) return targetNames;
|
|
23
|
-
|
|
24
|
-
return basePaths;
|
|
25
|
+
return [...config.basePaths];
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
const resolveMobileTargetByBasePath = (
|
|
28
29
|
targets: ResolvedMobileTarget[],
|
|
29
30
|
basePath: string,
|
|
30
31
|
): ResolvedMobileTarget | undefined => {
|
|
31
|
-
const normalizedBasePath = basePath
|
|
32
|
-
const byBasePath = targets.find(
|
|
32
|
+
const normalizedBasePath = trimSlashes(basePath);
|
|
33
|
+
const byBasePath = targets.find(
|
|
34
|
+
({ config }) => config.basePath !== undefined && trimSlashes(config.basePath) === normalizedBasePath,
|
|
35
|
+
);
|
|
33
36
|
if (byBasePath) return byBasePath;
|
|
34
37
|
const [template] = targets;
|
|
35
38
|
if (!template) return undefined;
|
|
36
39
|
return {
|
|
37
40
|
name: normalizedBasePath,
|
|
38
|
-
config: {
|
|
39
|
-
...template.config,
|
|
40
|
-
name: normalizedBasePath,
|
|
41
|
-
basePath: normalizedBasePath,
|
|
42
|
-
},
|
|
41
|
+
config: { ...template.config, name: normalizedBasePath, basePath: normalizedBasePath },
|
|
43
42
|
};
|
|
44
43
|
};
|
|
45
44
|
|
|
@@ -70,18 +69,20 @@ export const resolveMobileTargets = async (
|
|
|
70
69
|
const target = targets.find((candidate) => candidate.name === selection);
|
|
71
70
|
if (target) return [target];
|
|
72
71
|
const basePathTarget = resolveMobileTargetByBasePath(targets, selection);
|
|
73
|
-
if (basePathTarget && config.basePaths.has(selection
|
|
72
|
+
if (basePathTarget && config.basePaths.has(trimSlashes(selection))) return [basePathTarget];
|
|
74
73
|
const choices = await getMobileTargetChoices(app);
|
|
75
74
|
throw new Error(`Mobile target '${selection}' was not found. Available: ${choices.join(", ")}`);
|
|
76
75
|
};
|
|
77
76
|
|
|
78
77
|
export const resolveMobilePath = (target: AkanMobileTargetConfig, pathname: string) => {
|
|
79
|
-
const basePath = target.basePath
|
|
78
|
+
const basePath = trimSlashes(target.basePath ?? "");
|
|
80
79
|
const normalizedPath = `/${pathname.replace(/^\/+/, "")}`;
|
|
81
80
|
if (!basePath) return normalizedPath;
|
|
82
81
|
if (normalizedPath === `/${basePath}` || normalizedPath.startsWith(`/${basePath}/`)) return normalizedPath;
|
|
83
82
|
return `/${basePath}${normalizedPath === "/" ? "" : normalizedPath}`;
|
|
84
83
|
};
|
|
85
84
|
|
|
86
|
-
export const targetHtmlFilename = (target: AkanMobileTargetConfig) =>
|
|
87
|
-
|
|
85
|
+
export const targetHtmlFilename = (target: AkanMobileTargetConfig) => {
|
|
86
|
+
const basePath = trimSlashes(target.basePath ?? "");
|
|
87
|
+
return basePath ? `${basePath}.html` : "index.html";
|
|
88
|
+
};
|
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.20",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@inquirer/prompts": "^8.4.3",
|
|
49
49
|
"@tailwindcss/node": "^4.3.0",
|
|
50
50
|
"@trapezedev/project": "^7.1.4",
|
|
51
|
-
"akanjs": "3.0.0-beta.
|
|
51
|
+
"akanjs": "3.0.0-beta.20",
|
|
52
52
|
"chalk": "^5.6.2",
|
|
53
53
|
"commander": "^14.0.3",
|
|
54
54
|
"dayjs": "^1.11.20",
|
package/packageExports.test.ts
CHANGED
|
@@ -3,30 +3,17 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { PackageExportsMap } from "@akanjs/devkit/packageExportsMap";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// Inside the monorepo Bun resolves `@akanjs/devkit/executors` through the root tsconfig `paths`
|
|
9
|
-
// (`@akanjs/devkit/*` -> `pkgs/@akanjs/devkit/*`), and that resolver probes extensions and directory
|
|
10
|
-
// indexes. A published consumer has no such mapping: it goes through this package's `exports` map,
|
|
11
|
-
// whose targets are matched *exactly*. So `"./*": "./*"` type-checked, built, and passed every test
|
|
12
|
-
// here while every subpath import failed at runtime for anyone installing the tarball:
|
|
13
|
-
//
|
|
14
|
-
// error: Cannot find module '@akanjs/devkit/executors' from
|
|
15
|
-
// '<consumer>/node_modules/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts'
|
|
16
|
-
//
|
|
17
|
-
// `PackageRunner.verifyDistPackage` runs the same check against the built dist tree of every
|
|
18
|
-
// publishable package at release time; these tests keep this package honest on every run.
|
|
6
|
+
//? The monorepo resolves subpaths through tsconfig `paths`, which probe extensions; a published consumer goes through
|
|
7
|
+
//? the exports map, matched exactly — so `"./*": "./*"` passed every test here and broke every installed subpath.
|
|
19
8
|
|
|
20
9
|
const packageDir = import.meta.dir;
|
|
21
10
|
const exportsMap = await PackageExportsMap.from(packageDir);
|
|
22
11
|
|
|
23
|
-
/** Every facet the root barrel re-exports, as the subpath a consumer would import. */
|
|
24
12
|
const barrelFacets = async (): Promise<string[]> => {
|
|
25
13
|
const barrel = await Bun.file(path.join(packageDir, "index.ts")).text();
|
|
26
14
|
return [...barrel.matchAll(/^export (?:type )?\* from "\.\/([^"]+)";$/gm)].map((match) => `./${match[1]}`);
|
|
27
15
|
};
|
|
28
16
|
|
|
29
|
-
/** Every `@akanjs/devkit/<subpath>` specifier written anywhere in the two packages that use them. */
|
|
30
17
|
const importedSubpaths = async (): Promise<string[]> => {
|
|
31
18
|
const repoRoot = path.resolve(packageDir, "../../..");
|
|
32
19
|
const glob = new Bun.Glob("pkgs/@akanjs/{cli,devkit}/**/*.{ts,tsx}");
|
|
@@ -53,9 +40,7 @@ describe("published exports map", () => {
|
|
|
53
40
|
});
|
|
54
41
|
|
|
55
42
|
test("covers both facet shapes and keeps explicit extensions intact", () => {
|
|
56
|
-
//
|
|
57
|
-
// facets need their own literal entry, and `./*.ts` -> `./*.ts` keeps an already-suffixed
|
|
58
|
-
// specifier from becoming `./cloud/cloudApi.ts.ts`.
|
|
43
|
+
// One wildcard cannot serve bare files, directory facets and already-suffixed specifiers (no `.ts.ts`) at once.
|
|
59
44
|
expect(exportsMap.resolve("./executors")).toBe("./executors.ts");
|
|
60
45
|
expect(exportsMap.resolve("./frontendBuild")).toBe("./frontendBuild/index.ts");
|
|
61
46
|
expect(exportsMap.resolve("./cloud/cloudApi.ts")).toBe("./cloud/cloudApi.ts");
|
package/packageExportsMap.ts
CHANGED
|
@@ -7,17 +7,9 @@ export interface UnreachableSubpath {
|
|
|
7
7
|
target: string | null;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
* Exports targets are matched **exactly**: no extension is appended and no `index.ts` is probed. That
|
|
14
|
-
* is invisible inside this monorepo, where `@akanjs/devkit/*` and `akanjs/*` resolve through the root
|
|
15
|
-
* tsconfig `paths` instead — a resolver that *does* probe both. A map of `{"./*": "./*"}` therefore
|
|
16
|
-
* type-checks, builds, and passes every test while every subpath import fails for anyone who installs
|
|
17
|
-
* the tarball. This class exists so that gap can be asserted against before publishing.
|
|
18
|
-
*/
|
|
10
|
+
// Bun matches exports targets exactly (no extension, no index.ts), while this monorepo resolves `akanjs/*` through
|
|
11
|
+
// tsconfig paths, which probe both — so a broken map passes every in-repo test and fails for every installer.
|
|
19
12
|
export class PackageExportsMap {
|
|
20
|
-
/** Reads `<packageDir>/package.json` and builds the map from its `exports` field. */
|
|
21
13
|
static async from(packageDir: string) {
|
|
22
14
|
const manifest = (await Bun.file(path.join(packageDir, "package.json")).json()) as { exports?: unknown };
|
|
23
15
|
return new PackageExportsMap(packageDir, manifest.exports);
|
|
@@ -60,7 +52,6 @@ export class PackageExportsMap {
|
|
|
60
52
|
// wins over `./*`, which is what keeps an already-suffixed specifier from gaining a second `.ts`.
|
|
61
53
|
this.#patterns.sort((a, b) => b.prefix.length - a.prefix.length || b.suffix.length - a.suffix.length);
|
|
62
54
|
}
|
|
63
|
-
/** Returns the target an `exports` lookup yields, or null when the subpath is unexported. */
|
|
64
55
|
resolve(subpath: string): string | null {
|
|
65
56
|
const literal = this.#literals.get(subpath);
|
|
66
57
|
if (literal) return literal;
|
|
@@ -71,20 +62,13 @@ export class PackageExportsMap {
|
|
|
71
62
|
}
|
|
72
63
|
return null;
|
|
73
64
|
}
|
|
74
|
-
|
|
75
|
-
* Resolves a subpath and reports whether the target it yields is a readable file.
|
|
76
|
-
*
|
|
77
|
-
* A directory does not count. `{"./*": "./*"}` maps `./commandDecorators` onto the directory of
|
|
78
|
-
* that name, which exists but is not a module — an `existsSync` check here reports such a subpath
|
|
79
|
-
* as reachable while the import still fails.
|
|
80
|
-
*/
|
|
65
|
+
// A directory does not count: `{"./*": "./*"}` maps `./commandDecorators` onto a folder, which is not importable.
|
|
81
66
|
resolveToFile(subpath: string): { target: string | null; exists: boolean } {
|
|
82
67
|
const target = this.resolve(subpath);
|
|
83
68
|
if (!target) return { target: null, exists: false };
|
|
84
69
|
const stat = statSync(path.join(this.#packageDir, target), { throwIfNoEntry: false });
|
|
85
70
|
return { target, exists: !!stat?.isFile() };
|
|
86
71
|
}
|
|
87
|
-
/** Returns the given subpaths that no consumer could import, in input order. */
|
|
88
72
|
findUnreachable(subpaths: Iterable<string>): UnreachableSubpath[] {
|
|
89
73
|
const unreachable: UnreachableSubpath[] = [];
|
|
90
74
|
for (const subpath of subpaths) {
|
package/piDependencyPins.test.ts
CHANGED
|
@@ -2,18 +2,8 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { PackageJson } from "@akanjs/devkit/types";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// `@earendil-works/pi-coding-agent` asks for `^0.80.6` of its three sibling packages and ships an
|
|
8
|
-
// `npm-shrinkwrap.json` pinning them to the exact version it was built against. Bun reads neither that file nor
|
|
9
|
-
// this workspace's `overrides` when somebody installs the published CLI, so `^0.80.6` resolved to the newest
|
|
10
|
-
// 0.80.x — 0.80.10, which dropped `getOAuthApiKey` from `pi-ai/oauth` while the pinned coding agent still
|
|
11
|
-
// imports it. A global install died on its first import:
|
|
12
|
-
//
|
|
13
|
-
// Export named 'getOAuthApiKey' not found in module '.../@earendil-works/pi-ai/dist/oauth.js'.
|
|
14
|
-
//
|
|
15
|
-
// `PackageRunner` therefore re-declares pi's own pins as dependencies of what we publish; this keeps the two
|
|
16
|
-
// equal, so bumping the coding agent without moving the pins fails here rather than at a user's first run.
|
|
5
|
+
//? A published install reads neither pi's npm-shrinkwrap.json nor our overrides, so PackageRunner re-declares pi's
|
|
6
|
+
//? sibling pins (a floating 0.80.x once broke every global install); this keeps them equal to the shrinkwrap.
|
|
17
7
|
|
|
18
8
|
const repoRoot = path.resolve(import.meta.dir, "../../..");
|
|
19
9
|
const piName = "@earendil-works/pi-coding-agent";
|
|
@@ -28,8 +18,7 @@ const readJson = async <T>(file: string) => (await Bun.file(file).json()) as T;
|
|
|
28
18
|
|
|
29
19
|
const shrinkwrappedPins = async () => {
|
|
30
20
|
const { packages } = await readJson<Shrinkwrap>(`${piDir}/npm-shrinkwrap.json`);
|
|
31
|
-
// Top-level entries only: a
|
|
32
|
-
// package's dependency, which a consumer's resolver reaches on its own and we have no business pinning.
|
|
21
|
+
// Top-level entries only: a nested `node_modules/` key is another package's placement, not ours to pin.
|
|
33
22
|
const entries = Object.entries(packages)
|
|
34
23
|
.filter(([key]) => /^node_modules\/@earendil-works\/[^/]+$/.test(key))
|
|
35
24
|
.map(([key, entry]) => [key.replace(/^node_modules\//, ""), entry.version ?? ""] as const);
|
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { tempDirs } from "../testHelpers";
|
|
5
5
|
import { IdleWatchedProcess } from "./IdleWatchedProcess";
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
const makeLogPath = async () =>
|
|
9
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "akan-idle-watch-"));
|
|
10
|
-
roots.push(root);
|
|
11
|
-
return path.join(root, "nested", "run.log");
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
afterAll(async () => {
|
|
15
|
-
await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })));
|
|
16
|
-
});
|
|
7
|
+
const makeRoot = tempDirs("akan-idle-watch-");
|
|
8
|
+
const makeLogPath = async () => path.join(await makeRoot(), "nested", "run.log");
|
|
17
9
|
|
|
18
10
|
describe("IdleWatchedProcess", () => {
|
|
19
11
|
test("tees the output of a command that finishes into the log and reports its exit code", async () => {
|
|
@@ -19,11 +19,7 @@ interface IdleWatchedOptions {
|
|
|
19
19
|
onOutput?: (text: string) => void;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
* Runs a command with its output teed into a log, and gives up on it once the output goes quiet.
|
|
24
|
-
* Silence is the only reliable hang signal: a runner stuck in a synchronous loop (Bun 1.4.2's Windows
|
|
25
|
-
* websocket crash spun at 100% CPU) never lets bun's own per-test timeout fire.
|
|
26
|
-
*/
|
|
22
|
+
// Silence is the only reliable hang signal: a runner stuck in a synchronous loop never lets bun's test timeout fire.
|
|
27
23
|
export class IdleWatchedProcess {
|
|
28
24
|
readonly #command: string[];
|
|
29
25
|
readonly #options: IdleWatchedOptions;
|
|
@@ -18,8 +18,7 @@ export class LinuxDockerTarget extends PlatformTestTarget {
|
|
|
18
18
|
this.policy = config.policy ?? "gate";
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
//* No compiler in the image
|
|
22
|
-
//* addon, and Bun then panics loading it (`uv_version_string`, oven-sh/bun#18546).
|
|
21
|
+
//* No compiler in the image: with one, ssh2 builds its native addon and Bun panics loading it (oven-sh/bun#18546).
|
|
23
22
|
static readonly dockerfile = [
|
|
24
23
|
`FROM oven/bun:${Bun.version}-slim`,
|
|
25
24
|
"RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates procps lsof && rm -rf /var/lib/apt/lists/*",
|
|
@@ -30,12 +29,13 @@ export class LinuxDockerTarget extends PlatformTestTarget {
|
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
protected async prepare(context: PlatformRunContext) {
|
|
33
|
-
if ((await LinuxDockerTarget
|
|
32
|
+
if ((await LinuxDockerTarget.spawnText(["docker", "info"])).exitCode !== 0)
|
|
34
33
|
throw new Error("Docker is not running — start Docker Desktop, or leave linux out of --platforms");
|
|
35
34
|
await this.#ensureImage();
|
|
36
35
|
const container = `akan-test-${context.runId}`;
|
|
37
36
|
const cpus = this.#config.cpus ?? Math.max(2, Math.floor(os.availableParallelism() / 2));
|
|
38
|
-
const started = await LinuxDockerTarget
|
|
37
|
+
const started = await LinuxDockerTarget.spawnText([
|
|
38
|
+
"docker",
|
|
39
39
|
"run",
|
|
40
40
|
"-d",
|
|
41
41
|
"--rm",
|
|
@@ -70,51 +70,36 @@ export class LinuxDockerTarget extends PlatformTestTarget {
|
|
|
70
70
|
const container = this.#container;
|
|
71
71
|
if (!container) throw new Error("the linux container is not running");
|
|
72
72
|
return await new IdleWatchedProcess(
|
|
73
|
-
[
|
|
73
|
+
[
|
|
74
|
+
"docker",
|
|
75
|
+
"exec",
|
|
76
|
+
...Object.entries(PlatformTestTarget.gitIdentityEnv).flatMap(([key, value]) => ["-e", `${key}=${value}`]),
|
|
77
|
+
container,
|
|
78
|
+
"sh",
|
|
79
|
+
"-c",
|
|
80
|
+
command,
|
|
81
|
+
],
|
|
74
82
|
{
|
|
75
83
|
logPath,
|
|
76
84
|
idleMs,
|
|
77
85
|
onIdle: async () => {
|
|
78
|
-
await LinuxDockerTarget
|
|
86
|
+
await LinuxDockerTarget.spawnText(["docker", "exec", container, "pkill", "-9", "bun"]);
|
|
79
87
|
},
|
|
80
88
|
},
|
|
81
89
|
).run();
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
protected async cleanup() {
|
|
85
|
-
if (this.#container) await LinuxDockerTarget
|
|
93
|
+
if (this.#container) await LinuxDockerTarget.spawnText(["docker", "rm", "-f", this.#container]);
|
|
86
94
|
this.#container = null;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
async #ensureImage() {
|
|
90
|
-
if ((await LinuxDockerTarget
|
|
91
|
-
const built = await LinuxDockerTarget
|
|
98
|
+
if ((await LinuxDockerTarget.spawnText(["docker", "image", "inspect", this.image])).exitCode === 0) return;
|
|
99
|
+
const built = await LinuxDockerTarget.spawnText(
|
|
100
|
+
["docker", "build", "-t", this.image, "-"],
|
|
101
|
+
LinuxDockerTarget.dockerfile,
|
|
102
|
+
);
|
|
92
103
|
if (built.exitCode !== 0) throw new Error(`building ${this.image} failed: ${built.stderr.trim().slice(-400)}`);
|
|
93
104
|
}
|
|
94
|
-
|
|
95
|
-
static #gitIdentityEnv() {
|
|
96
|
-
return Object.entries({
|
|
97
|
-
GIT_AUTHOR_NAME: "akan-test",
|
|
98
|
-
GIT_AUTHOR_EMAIL: "akan-test@localhost",
|
|
99
|
-
GIT_COMMITTER_NAME: "akan-test",
|
|
100
|
-
GIT_COMMITTER_EMAIL: "akan-test@localhost",
|
|
101
|
-
GIT_CONFIG_COUNT: "1",
|
|
102
|
-
GIT_CONFIG_KEY_0: "init.defaultBranch",
|
|
103
|
-
GIT_CONFIG_VALUE_0: "main",
|
|
104
|
-
}).flatMap(([key, value]) => ["-e", `${key}=${value}`]);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
static async #docker(args: string[], stdin?: string) {
|
|
108
|
-
const proc = Bun.spawn(["docker", ...args], {
|
|
109
|
-
stdin: stdin === undefined ? "ignore" : new Response(stdin),
|
|
110
|
-
stdout: "pipe",
|
|
111
|
-
stderr: "pipe",
|
|
112
|
-
});
|
|
113
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
114
|
-
new Response(proc.stdout).text(),
|
|
115
|
-
new Response(proc.stderr).text(),
|
|
116
|
-
proc.exited,
|
|
117
|
-
]);
|
|
118
|
-
return { stdout, stderr, exitCode };
|
|
119
|
-
}
|
|
120
105
|
}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { tempDirs } from "../testHelpers";
|
|
5
5
|
import { BunTestReport } from "./BunTestReport";
|
|
6
6
|
import { PlatformTestRun } from "./PlatformTestRun";
|
|
7
7
|
import type { PackageTestResult, PlatformTestResult } from "./PlatformTestTarget";
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
afterAll(async () => {
|
|
11
|
-
await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })));
|
|
12
|
-
});
|
|
9
|
+
const makeRoot = tempDirs("akan-platform-env-");
|
|
13
10
|
|
|
14
11
|
const pkg = (status: PackageTestResult["status"]): PackageTestResult => ({
|
|
15
12
|
pkg: "akanjs",
|
|
@@ -42,8 +39,7 @@ describe("PlatformTestRun", () => {
|
|
|
42
39
|
});
|
|
43
40
|
|
|
44
41
|
test("hands a test host only the env keys the CLI needs, never the secrets beside them", async () => {
|
|
45
|
-
const root = await
|
|
46
|
-
roots.push(root);
|
|
42
|
+
const root = await makeRoot();
|
|
47
43
|
await writeFile(
|
|
48
44
|
path.join(root, ".env"),
|
|
49
45
|
["USE_AKANJS_PKGS=true", "OPENAI_API_KEY=sk-secret", 'AKAN_PUBLIC_SERVE_DOMAIN="akanjs.com"', "JEV_KEY=x"].join(
|