@letta-ai/letta-code 0.25.10 → 0.26.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/README.md +108 -28
- package/dist/types/protocol.d.ts +2 -2
- package/docs/nix.md +105 -0
- package/docs/plans/discord-channel-policy-consolidation.md +35 -0
- package/letta.js +250244 -247360
- package/package.json +11 -4
- package/scripts/check-exported-functions.js +58 -0
- package/scripts/check-filename-casing.js +70 -0
- package/scripts/check-layer-boundaries.js +113 -0
- package/scripts/check-test-mock-isolation.js +19 -19
- package/scripts/check.js +64 -44
- package/scripts/codex-watch/check-release.ts +426 -0
- package/scripts/codex-watch/diff-models-json.test.ts +151 -0
- package/scripts/codex-watch/diff-models-json.ts +207 -0
- package/scripts/codex-watch/render-issue.ts +273 -0
- package/scripts/rename-to-kebab.js +59 -0
- package/scripts/run-unit-tests.cjs +78 -0
- package/scripts/update-kebab-imports.js +93 -0
- package/skills/initializing-memory/SKILL.md +1 -2
- package/skills/working-in-parallel/SKILL.md +0 -90
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letta-ai/letta-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "bun@1.3.0",
|
|
6
7
|
"bin": {
|
|
7
8
|
"letta": "letta.js"
|
|
8
9
|
},
|
|
@@ -13,7 +14,8 @@
|
|
|
13
14
|
"scripts",
|
|
14
15
|
"skills",
|
|
15
16
|
"vendor",
|
|
16
|
-
"dist/types"
|
|
17
|
+
"dist/types",
|
|
18
|
+
"docs"
|
|
17
19
|
],
|
|
18
20
|
"exports": {
|
|
19
21
|
".": "./letta.js",
|
|
@@ -61,6 +63,7 @@
|
|
|
61
63
|
"ink-spinner": "^5.0.0",
|
|
62
64
|
"ink-text-input": "^5.0.0",
|
|
63
65
|
"lint-staged": "16.2.4",
|
|
66
|
+
"madge": "^8.0.0",
|
|
64
67
|
"minimatch": "^10.0.3",
|
|
65
68
|
"picomatch": "^2.3.1",
|
|
66
69
|
"react": "18.2.0",
|
|
@@ -71,12 +74,16 @@
|
|
|
71
74
|
"lint": "bunx --bun @biomejs/biome@2.2.5 check src",
|
|
72
75
|
"fix": "bunx --bun @biomejs/biome@2.2.5 check --write src",
|
|
73
76
|
"typecheck": "tsc --noEmit",
|
|
77
|
+
"check:cycles": "madge --circular --extensions ts,tsx src/",
|
|
78
|
+
"check:boundaries": "node scripts/check-layer-boundaries.js",
|
|
79
|
+
"check:exported-functions": "node scripts/check-exported-functions.js",
|
|
80
|
+
"check:filename-casing": "node scripts/check-filename-casing.js",
|
|
74
81
|
"check:test-mock-isolation": "bun run scripts/check-test-mock-isolation.js",
|
|
75
82
|
"check": "bun run scripts/check.js",
|
|
76
83
|
"dev": "LETTA_DEBUG=${LETTA_DEBUG:-1} LETTA_RESPONSES_WS=${LETTA_RESPONSES_WS:-1} bun --loader=.md:text --loader=.mdx:text --loader=.txt:text run src/index.ts",
|
|
77
84
|
"build": "node scripts/postinstall-patches.js && bun run build.js",
|
|
78
|
-
"test:update-chain:manual": "bun run src/
|
|
79
|
-
"test:update-chain:startup": "bun run src/
|
|
85
|
+
"test:update-chain:manual": "bun run src/test-utils/update-chain-smoke.ts --mode manual",
|
|
86
|
+
"test:update-chain:startup": "bun run src/test-utils/update-chain-smoke.ts --mode startup",
|
|
80
87
|
"prepublishOnly": "bun run build",
|
|
81
88
|
"postinstall": "node scripts/postinstall-patches.js || echo letta: vendor patches skipped && node -e \"try{require('fs').chmodSync(require('path').join(require.resolve('node-pty/package.json'),'../prebuilds/darwin-arm64/spawn-helper'),0o755)}catch(e){}\" || true"
|
|
82
89
|
},
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Enforces that exported functions use `export function` declarations
|
|
4
|
+
* rather than `export const fn = () =>` or `export const fn = async () =>`.
|
|
5
|
+
*
|
|
6
|
+
* Rationale: `export function` declarations are greppable (`grep "export function"`
|
|
7
|
+
* reliably finds all exported functions), hoisted, and easier for agents to locate.
|
|
8
|
+
*
|
|
9
|
+
* Scope: .ts files only — NOT .tsx. React components in .tsx legitimately use
|
|
10
|
+
* `export const Foo = memo(...)` which cannot be a function declaration.
|
|
11
|
+
*
|
|
12
|
+
* Value exports (singletons, data, re-exports) are not flagged.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { glob } from "glob";
|
|
18
|
+
|
|
19
|
+
const rootDir = process.cwd();
|
|
20
|
+
|
|
21
|
+
// Matches: export const name = (...) => or export const name = async (...) =>
|
|
22
|
+
// Also catches: export const name = () => and export const name = async () =>
|
|
23
|
+
const ARROW_EXPORT = /^export const \w+ = (async )?\(/;
|
|
24
|
+
|
|
25
|
+
const files = await glob("src/**/*.ts", {
|
|
26
|
+
cwd: rootDir,
|
|
27
|
+
ignore: ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let violations = 0;
|
|
31
|
+
|
|
32
|
+
for (const file of files.sort()) {
|
|
33
|
+
const content = readFileSync(join(rootDir, file), "utf-8");
|
|
34
|
+
const lines = content.split("\n");
|
|
35
|
+
|
|
36
|
+
lines.forEach((line, i) => {
|
|
37
|
+
if (ARROW_EXPORT.test(line)) {
|
|
38
|
+
if (violations === 0) {
|
|
39
|
+
console.error("\n❌ Exported arrow functions found:\n");
|
|
40
|
+
}
|
|
41
|
+
console.error(` ${file}:${i + 1}`);
|
|
42
|
+
console.error(` ${line.trim()}`);
|
|
43
|
+
console.error(
|
|
44
|
+
` ↳ Use 'export function name() {}' instead of 'export const name = () =>'\n`,
|
|
45
|
+
);
|
|
46
|
+
violations++;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (violations > 0) {
|
|
52
|
+
console.error(
|
|
53
|
+
`Found ${violations} exported arrow function${violations === 1 ? "" : "s"}.`,
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
} else {
|
|
57
|
+
console.log("✅ No exported arrow functions found.");
|
|
58
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Enforces kebab-case filenames for .ts files in src/.
|
|
4
|
+
* .tsx files are exempt — PascalCase is the React component convention.
|
|
5
|
+
* .d.ts files are exempt.
|
|
6
|
+
*
|
|
7
|
+
* A filename is a violation if its stem (before the first dot) contains
|
|
8
|
+
* any uppercase letter, e.g. bootstrapHandler.ts or LocalStore.ts.
|
|
9
|
+
*
|
|
10
|
+
* Pass --staged to check only git-staged files (used in pre-commit hook).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
import { readdirSync, statSync } from "node:fs";
|
|
15
|
+
import { basename, join } from "node:path";
|
|
16
|
+
|
|
17
|
+
const stagedOnly = process.argv.includes("--staged");
|
|
18
|
+
|
|
19
|
+
function isViolation(filename) {
|
|
20
|
+
if (!filename.endsWith(".ts")) return false;
|
|
21
|
+
if (filename.endsWith(".d.ts")) return false;
|
|
22
|
+
const stem = filename.slice(0, filename.indexOf("."));
|
|
23
|
+
return /[A-Z]/.test(stem);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function* walkTs(dir) {
|
|
27
|
+
for (const entry of readdirSync(dir)) {
|
|
28
|
+
const full = join(dir, entry);
|
|
29
|
+
if (statSync(full).isDirectory()) yield* walkTs(full);
|
|
30
|
+
else if (isViolation(entry)) yield full;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let files;
|
|
35
|
+
|
|
36
|
+
if (stagedOnly) {
|
|
37
|
+
const staged = execSync("git diff --cached --name-only --diff-filter=A", {
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
})
|
|
40
|
+
.trim()
|
|
41
|
+
.split("\n")
|
|
42
|
+
.filter((f) => f.startsWith("src/") && isViolation(basename(f)));
|
|
43
|
+
files = staged;
|
|
44
|
+
} else {
|
|
45
|
+
files = [...walkTs("src")];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (files.length === 0) {
|
|
49
|
+
console.log("✅ No filename casing violations found.");
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.error("\n❌ Non-kebab-case .ts filenames found:\n");
|
|
54
|
+
for (const f of files) {
|
|
55
|
+
const stem = basename(f).slice(0, basename(f).indexOf("."));
|
|
56
|
+
const kebab = stem
|
|
57
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
58
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
59
|
+
.toLowerCase();
|
|
60
|
+
console.error(` ${f}`);
|
|
61
|
+
console.error(` ↳ rename to: ${basename(f).replace(stem, kebab)}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.error(
|
|
65
|
+
`Found ${files.length} violation${files.length === 1 ? "" : "s"}.`,
|
|
66
|
+
);
|
|
67
|
+
console.error(
|
|
68
|
+
".tsx files are exempt (PascalCase is correct for React components).\n",
|
|
69
|
+
);
|
|
70
|
+
process.exit(1);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Enforces architectural import boundaries between top-level modules.
|
|
4
|
+
*
|
|
5
|
+
* Rules:
|
|
6
|
+
* tools/ must not import from cli/
|
|
7
|
+
* backend/ must not import from cli/ or websocket/
|
|
8
|
+
* providers/ must not import from agent/ or cli/
|
|
9
|
+
* websocket/listener/ must not import from backend/api/client or backend/api/conversations
|
|
10
|
+
* telemetry/ must not import from cli/ agent/ websocket/ or tools/
|
|
11
|
+
*
|
|
12
|
+
* These are currently violation-free. Adding a rule here means you must
|
|
13
|
+
* also ensure no existing code violates it.
|
|
14
|
+
*
|
|
15
|
+
* To add a new rule, append an entry to RULES below.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { glob } from "glob";
|
|
21
|
+
|
|
22
|
+
const rootDir = process.cwd();
|
|
23
|
+
const srcDir = join(rootDir, "src");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Each rule: files under `layer` must not contain an import from any of `forbidden`.
|
|
27
|
+
* `description` is shown on violation.
|
|
28
|
+
*/
|
|
29
|
+
const RULES = [
|
|
30
|
+
{
|
|
31
|
+
layer: "tools",
|
|
32
|
+
forbidden: ["cli"],
|
|
33
|
+
description:
|
|
34
|
+
"tools/ run in headless and agent contexts — they must not import from cli/",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
layer: "backend",
|
|
38
|
+
forbidden: ["cli", "websocket"],
|
|
39
|
+
description:
|
|
40
|
+
"backend/ is a low-level abstraction — it must not import from cli/ or websocket/",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
layer: "providers",
|
|
44
|
+
forbidden: ["agent", "cli"],
|
|
45
|
+
description:
|
|
46
|
+
"providers/ are pure LLM adapters — they must not import from agent/ or cli/",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
layer: "websocket/listener",
|
|
50
|
+
forbidden: ["backend/api/client", "backend/api/conversations"],
|
|
51
|
+
description:
|
|
52
|
+
"websocket/listener/ uses the getBackend() abstraction — it must not import the raw API client or conversations module directly",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
layer: "cli/app",
|
|
56
|
+
forbidden: ["backend/api/conversations"],
|
|
57
|
+
description:
|
|
58
|
+
"cli/app/ uses the getBackend() abstraction for conversation operations — it must not import the raw conversations module directly",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
layer: "telemetry",
|
|
62
|
+
forbidden: ["cli", "agent", "websocket", "tools"],
|
|
63
|
+
description:
|
|
64
|
+
"telemetry/ is a leaf observer — it must not import from cli/, agent/, websocket/, or tools/ (only backend/api/ for submitting data is permitted)",
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Matches: import ... from "@/forbidden/..." (static imports only)
|
|
69
|
+
function buildPattern(forbidden) {
|
|
70
|
+
return new RegExp(`from\\s+["'\`]@/(${forbidden.join("|")})/`, "g");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let violations = 0;
|
|
74
|
+
|
|
75
|
+
for (const rule of RULES) {
|
|
76
|
+
const files = await glob(`src/${rule.layer}/**/*.{ts,tsx}`, {
|
|
77
|
+
cwd: rootDir,
|
|
78
|
+
ignore: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts"],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const pattern = buildPattern(rule.forbidden);
|
|
82
|
+
|
|
83
|
+
for (const file of files.sort()) {
|
|
84
|
+
const content = readFileSync(join(rootDir, file), "utf-8");
|
|
85
|
+
const lines = content.split("\n");
|
|
86
|
+
|
|
87
|
+
lines.forEach((line, i) => {
|
|
88
|
+
// Reset lastIndex for global regex reuse
|
|
89
|
+
pattern.lastIndex = 0;
|
|
90
|
+
if (pattern.test(line)) {
|
|
91
|
+
if (violations === 0) {
|
|
92
|
+
console.error("\n❌ Layer boundary violations found:\n");
|
|
93
|
+
}
|
|
94
|
+
console.error(` ${file}:${i + 1}`);
|
|
95
|
+
console.error(` ${line.trim()}`);
|
|
96
|
+
console.error(` ↳ ${rule.description}\n`);
|
|
97
|
+
violations++;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (violations > 0) {
|
|
104
|
+
console.error(
|
|
105
|
+
`Found ${violations} boundary violation${violations === 1 ? "" : "s"}.`,
|
|
106
|
+
);
|
|
107
|
+
console.error(
|
|
108
|
+
"Fix by moving the helper to a shared layer (utils/, types/) or inverting the dependency.\n",
|
|
109
|
+
);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
} else {
|
|
112
|
+
console.log("✅ No layer boundary violations found.");
|
|
113
|
+
}
|
|
@@ -8,7 +8,7 @@ const TESTS_DIR_ENV = "LETTA_MOCK_ISOLATION_TESTS_DIR";
|
|
|
8
8
|
const rootDir = process.cwd();
|
|
9
9
|
const testsDir = process.env[TESTS_DIR_ENV]
|
|
10
10
|
? resolve(process.env[TESTS_DIR_ENV])
|
|
11
|
-
: join(rootDir, "src"
|
|
11
|
+
: join(rootDir, "src");
|
|
12
12
|
|
|
13
13
|
const FORBIDDEN_MOCK_MODULES = new Map([
|
|
14
14
|
[
|
|
@@ -41,24 +41,24 @@ const COMPLETE_EXPORT_MOCK_MODULES = new Set([
|
|
|
41
41
|
// explicit test override helper. If a new top-level mock is truly unavoidable,
|
|
42
42
|
// add a file+module entry here in the same PR with a clear explanation.
|
|
43
43
|
const ALLOWED_TOP_LEVEL_MOCKS = new Set([
|
|
44
|
-
"src/
|
|
45
|
-
"src/
|
|
46
|
-
"src/
|
|
47
|
-
"src/
|
|
48
|
-
"src/
|
|
49
|
-
"src/
|
|
50
|
-
"src/
|
|
51
|
-
"src/
|
|
52
|
-
"src/
|
|
53
|
-
"src/
|
|
54
|
-
"src/
|
|
55
|
-
"src/
|
|
56
|
-
"src/
|
|
57
|
-
"src/
|
|
58
|
-
"src/
|
|
59
|
-
"src/
|
|
60
|
-
"src/
|
|
61
|
-
"src/
|
|
44
|
+
"src/channels/discord-registry.test.ts::../backend/api/client",
|
|
45
|
+
"src/channels/slack-adapter-interop.test.ts::./slack/media",
|
|
46
|
+
"src/channels/slack-adapter-interop.test.ts::./slack/runtime",
|
|
47
|
+
"src/channels/slack-adapter.test.ts::./slack/media",
|
|
48
|
+
"src/channels/slack-adapter.test.ts::./slack/runtime",
|
|
49
|
+
"src/channels/telegram-adapter.test.ts::./telegram/runtime",
|
|
50
|
+
"src/cli/message-search-cache-warm.test.ts::../backend/api/search",
|
|
51
|
+
"src/hooks/prompt-executor.test.ts::../backend/api/generate",
|
|
52
|
+
"src/tools/memory-apply-patch.test.ts::../backend/api/client",
|
|
53
|
+
"src/tools/memory-tool.test.ts::../backend/api/client",
|
|
54
|
+
"src/tools/toolset-client-tool-rule-cleanup.test.ts::../backend/api/client",
|
|
55
|
+
"src/tools/toolset-memfs-detach.test.ts::../backend/api/client",
|
|
56
|
+
"src/websocket/listen-client-concurrency.test.ts::../agent/approval-execution",
|
|
57
|
+
"src/websocket/listen-client-concurrency.test.ts::../agent/approval-recovery",
|
|
58
|
+
"src/websocket/listen-client-concurrency.test.ts::../agent/message",
|
|
59
|
+
"src/websocket/listen-client-concurrency.test.ts::../backend/api/client",
|
|
60
|
+
"src/websocket/listen-client-concurrency.test.ts::../cli/helpers/approvalClassification",
|
|
61
|
+
"src/websocket/listen-client-concurrency.test.ts::../cli/helpers/stream",
|
|
62
62
|
]);
|
|
63
63
|
|
|
64
64
|
const mockModulePattern = /\bmock\.module\s*\(\s*(["'`])([^"'`]+)\1/g;
|
package/scripts/check.js
CHANGED
|
@@ -1,56 +1,76 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// Script to run linting and type checking with helpful error messages
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
const LABEL_WIDTH = 32;
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
function formatLabel(name) {
|
|
6
|
+
const dots = ".".repeat(Math.max(3, LABEL_WIDTH - name.length - 1));
|
|
7
|
+
return `${name} ${dots}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseFileCount(output) {
|
|
11
|
+
const m = output.match(/(?:Processed|Checked) ([\d,]+) files/);
|
|
12
|
+
return m ? Number(m[1].replace(/,/g, "")) : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const checks = [
|
|
16
|
+
{ name: "circular dependencies", script: ["check:cycles", "--no-spinner"] },
|
|
17
|
+
{ name: "layer boundaries", script: ["check:boundaries"] },
|
|
18
|
+
{ name: "exported function style", script: ["check:exported-functions"] },
|
|
19
|
+
{ name: "filename casing", script: ["check:filename-casing"] },
|
|
20
|
+
{ name: "test mock isolation", script: ["check:test-mock-isolation"] },
|
|
21
|
+
{ name: "biome", script: ["lint"] },
|
|
22
|
+
{ name: "typescript", script: ["typecheck"] },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const N = checks.length;
|
|
26
|
+
let failed = 0;
|
|
27
|
+
const wallStart = performance.now();
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < N; i++) {
|
|
30
|
+
const { name, script } = checks[i];
|
|
31
|
+
const t0 = performance.now();
|
|
7
32
|
|
|
8
|
-
|
|
33
|
+
const proc = Bun.spawn(["bun", "run", ...script], {
|
|
34
|
+
cwd: process.cwd(),
|
|
35
|
+
stdout: "pipe",
|
|
36
|
+
stderr: "pipe",
|
|
37
|
+
});
|
|
9
38
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
40
|
+
new Response(proc.stdout).text(),
|
|
41
|
+
new Response(proc.stderr).text(),
|
|
42
|
+
proc.exited,
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
|
|
46
|
+
const ok = exitCode === 0;
|
|
47
|
+
const combined = stdout + stderr;
|
|
48
|
+
const count = parseFileCount(combined);
|
|
49
|
+
const countStr = count ? ` (${count.toLocaleString()} files)` : "";
|
|
50
|
+
|
|
51
|
+
process.stdout.write(
|
|
52
|
+
`[${i + 1}/${N}] ${formatLabel(name)} ${ok ? "PASS" : "FAIL"} ${elapsed}s${countStr}\n`,
|
|
19
53
|
);
|
|
20
|
-
failed = true;
|
|
21
|
-
}
|
|
22
54
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
if (!ok) {
|
|
56
|
+
failed++;
|
|
57
|
+
const lines = combined
|
|
58
|
+
.trim()
|
|
59
|
+
.split("\n")
|
|
60
|
+
.filter((l) => l.trim());
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
process.stderr.write(` ${line}\n`);
|
|
63
|
+
}
|
|
64
|
+
process.stderr.write("\n");
|
|
65
|
+
}
|
|
33
66
|
}
|
|
34
67
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
await $`bun run typecheck`;
|
|
39
|
-
console.log("✅ Type checking passed\n");
|
|
40
|
-
} catch (error) {
|
|
41
|
-
console.error("❌ Type checking failed\n");
|
|
42
|
-
console.error("Fix the type errors shown above, then run:");
|
|
43
|
-
console.error(" bun run typecheck\n");
|
|
44
|
-
failed = true;
|
|
45
|
-
}
|
|
68
|
+
const total = ((performance.now() - wallStart) / 1000).toFixed(1);
|
|
69
|
+
process.stdout.write("\n");
|
|
46
70
|
|
|
47
|
-
if (failed) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
console.error(" bun run typecheck # Check types only");
|
|
52
|
-
console.error(" bun run check # Run both checks");
|
|
71
|
+
if (failed === 0) {
|
|
72
|
+
process.stdout.write(`✓ ${N} checks passed in ${total}s\n`);
|
|
73
|
+
} else {
|
|
74
|
+
process.stderr.write(`✗ ${failed} of ${N} checks failed in ${total}s\n`);
|
|
53
75
|
process.exit(1);
|
|
54
76
|
}
|
|
55
|
-
|
|
56
|
-
console.log("✅ All checks passed!");
|