Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ jobs:
- name: Build bundle
run: bun run build

- name: Minimal Linux npm artifact check
if: matrix.name == 'Linux x64 (ubuntu-24.04)'
env:
LETTA_CODE_MINIMAL_LINUX_ARTIFACT_SKIP_BUILD: "1"
run: bun run check:minimal-linux-npm-artifact

- name: CLI help smoke test
run: ./letta.js --help

Expand Down Expand Up @@ -230,6 +236,13 @@ jobs:
npm install -g (Get-Item letta-ai-letta-code-*.tgz).FullName
letta --help

- name: Windows node-pty artifact smoke test
if: runner.os == 'Windows'
shell: pwsh
run: |
$env:LETTA_CODE_GLOBAL_NODE_MODULES = npm root -g
bun run check:windows-node-pty-artifact

- name: Test npm install flow (Unix)
if: runner.os != 'Windows'
shell: sh
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
"cron-parser": "^5.6.1",
"glob": "^13.0.0",
"ink-link": "^5.0.0",
"node-pty": "^1.1.0",
"node-pty": "1.2.0-beta.14",
"open": "^10.2.0",
"react": "18.2.0",
"sharp": "^0.34.5",
Expand Down Expand Up @@ -169,6 +169,8 @@
"check:test-coverage": "node scripts/check-test-coverage.cjs",
"check:skill-frontmatter": "node scripts/check-skill-frontmatter.js",
"check:bundled-skill-scripts": "node scripts/check-bundled-skill-scripts.js",
"check:minimal-linux-npm-artifact": "node scripts/check-minimal-linux-npm-artifact.js",
"check:windows-node-pty-artifact": "node scripts/check-windows-node-pty-artifact.js",
"check": "bun run scripts/check.js",
"dev": "node scripts/dev.cjs",
"build": "node scripts/postinstall-patches.js && bun run build.js",
Expand Down
174 changes: 174 additions & 0 deletions scripts/check-minimal-linux-npm-artifact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env node

import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const tempDir = mkdtempSync(join(tmpdir(), "letta-code-minimal-linux-npm-"));
const imageTag = `letta-code-minimal-linux-npm:${process.pid}`;
const platform = "linux/amd64";
const dockerNodeVersion = "$" + "{NODE_VERSION}";
const dockerNodeArch = "$" + "{node_arch}";
const skipBuild =
process.argv.includes("--skip-build") ||
process.env.LETTA_CODE_MINIMAL_LINUX_ARTIFACT_SKIP_BUILD === "1";

function run(command, args, options = {}) {
console.log(`$ ${[command, ...args].join(" ")}`);
execFileSync(command, args, {
cwd: repoRoot,
stdio: "inherit",
timeout: 10 * 60 * 1000,
...options,
});
}

function capture(command, args, options = {}) {
console.log(`$ ${[command, ...args].join(" ")}`);
return execFileSync(command, args, {
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "inherit"],
timeout: 10 * 60 * 1000,
...options,
});
}

const dockerfile = String.raw`
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION=22.19.0

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl xz-utils \
&& rm -rf /var/lib/apt/lists/*

RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
amd64) node_arch="x64" ;; \
arm64) node_arch="arm64" ;; \
*) echo "unsupported architecture: $arch" >&2; exit 1 ;; \
esac; \
curl -fsSL "https://nodejs.org/dist/v${dockerNodeVersion}/node-v${dockerNodeVersion}-linux-${dockerNodeArch}.tar.xz" -o /tmp/node.tar.xz; \
tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1; \
rm /tmp/node.tar.xz; \
node --version; \
npm --version; \
if command -v make >/dev/null 2>&1; then \
echo "make unexpectedly present in minimal image" >&2; \
exit 1; \
fi

WORKDIR /workspace
`;

const ptySmoke = `
set -eux
if command -v make >/dev/null 2>&1; then
echo "make unexpectedly present before npm install" >&2
exit 1
fi
mkdir -p /workspace/app
cd /workspace/app
npm init -y >/dev/null
npm install --omit=dev --foreground-scripts /tmp/letta-code.tgz
./node_modules/.bin/letta --help >/tmp/letta-help.txt
node <<'NODE'
const { createRequire } = require("node:module");
const requireFromLetta = createRequire(require.resolve("@letta-ai/letta-code"));
const pty = requireFromLetta("node-pty");
const term = pty.spawn("/bin/bash", ["-lc", "test -t 0 && printf tty-ok"], {
name: "xterm-256color",
cols: 80,
rows: 24,
cwd: process.cwd(),
env: { ...process.env, TERM: "xterm-256color" },
});
let output = "";
let settled = false;
const timeout = setTimeout(() => {
if (settled) return;
settled = true;
try { term.kill(); } catch {}
console.error("PTY smoke timed out; output=" + JSON.stringify(output));
process.exit(1);
}, 5000);
term.onData((data) => {
output += data;
});
term.onExit(({ exitCode }) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
if (exitCode !== 0) {
console.error(
"PTY smoke exited with " + exitCode + "; output=" + JSON.stringify(output),
);
process.exit(1);
}
if (!output.includes("tty-ok")) {
console.error(
"PTY smoke did not allocate a tty; output=" + JSON.stringify(output),
);
process.exit(1);
}
console.log("PTY smoke passed");
});
NODE
`;

try {
run("docker", ["version"], { stdio: "ignore" });
if (skipBuild) {
console.log("$ bun run build (skipped; using existing build output)");
} else {
run("bun", ["run", "build"]);
}

const packOutput = capture("npm", [
"pack",
"--pack-destination",
tempDir,
"--json",
]);
const packed = JSON.parse(packOutput);
const filename = packed?.[0]?.filename;
if (!filename) {
throw new Error(`npm pack did not report a filename: ${packOutput}`);
}
const tarball = join(tempDir, filename);

writeFileSync(join(tempDir, "Dockerfile"), dockerfile);
run("docker", [
"build",
"--pull",
"--platform",
platform,
"-t",
imageTag,
tempDir,
]);
run("docker", [
"run",
"--rm",
"--platform",
platform,
"-v",
`${tarball}:/tmp/letta-code.tgz:ro`,
imageTag,
"sh",
"-c",
ptySmoke,
]);
console.log("Minimal Linux packed artifact check passed");
} finally {
try {
execFileSync("docker", ["rmi", "-f", imageTag], { stdio: "ignore" });
} catch {}
rmSync(tempDir, { recursive: true, force: true });
}
82 changes: 82 additions & 0 deletions scripts/check-windows-node-pty-artifact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env node

import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";

const marker = "letta-node-pty-windows-ok";

if (process.platform !== "win32") {
console.log(
"Windows node-pty artifact check skipped on non-Windows platform",
);
process.exit(0);
}

const globalRoot = (
process.env.LETTA_CODE_GLOBAL_NODE_MODULES ||
execSync("npm root -g", { encoding: "utf8" })
).trim();
const packageJson = join(globalRoot, "@letta-ai", "letta-code", "package.json");

if (!existsSync(packageJson)) {
throw new Error(
`Expected global @letta-ai/letta-code package at ${packageJson}. Run the npm install flow before this check.`,
);
}

const requireFromLetta = createRequire(packageJson);
const pty = requireFromLetta("node-pty");
const shell = process.env.ComSpec || "cmd.exe";
const term = pty.spawn(shell, ["/d", "/q"], {
name: "xterm-256color",
cols: 80,
rows: 24,
cwd: process.cwd(),
env: { ...process.env, TERM: "xterm-256color" },
});

let output = "";
let settled = false;

const timeout = setTimeout(() => {
if (settled) return;
settled = true;
try {
term.kill();
} catch {}
console.error(
`Windows node-pty smoke timed out; output=${JSON.stringify(output)}`,
);
process.exit(1);
}, 5000);

term.onData((data) => {
output += data;
});

term.onExit(({ exitCode }) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
if (exitCode !== 0) {
console.error(
`Windows node-pty smoke exited with ${exitCode}; output=${JSON.stringify(output)}`,
);
process.exit(1);
}
if (!output.includes(marker)) {
console.error(
`Windows node-pty smoke did not see marker; output=${JSON.stringify(output)}`,
);
process.exit(1);
}
// ConPTY can retain a native handle after the child exits. This is a one-shot
// check, so exit explicitly once the PTY proof is complete.
console.log("Windows node-pty PTY smoke passed");
process.exit(0);
});

term.write(`echo ${marker}\r\n`);
term.write("exit\r\n");
Loading