Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
160 changes: 160 additions & 0 deletions .github/workflows/code-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
name: Code Package

on:
pull_request:
paths:
- '.github/workflows/code-package.yml'
- 'packages/code/**'
- 'scripts/code-package-release*'
- 'scripts/smoke-code-package.mjs'
- 'LICENSE'
- 'NOTICE'
push:
tags: ['code-v*']
workflow_dispatch:
inputs:
version:
description: 'Committed worker version, e.g. 0.1.0-rc.1. Dispatch from main.'
required: true
type: string
publish:
description: 'Publish the verified artifact to npm (requires the npm environment).'
type: boolean
default: false

permissions:
contents: read

concurrency:
group: code-package-${{ github.event_name == 'pull_request' && github.ref || 'release' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
pack:
name: Pack Worker
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
actions: read
outputs:
sha: ${{ steps.plan.outputs.sha }}
filename: ${{ steps.pack.outputs.filename }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24.22.1
cache: npm
cache-dependency-path: packages/code/package-lock.json
- name: Validate release source and channel
id: plan
env:
EXPECTED_VERSION: ${{ inputs.version }}
run: node scripts/code-package-release.mjs plan
- name: Require successful main CI for releases
if: github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ steps.plan.outputs.sha }}
run: |
set -euo pipefail
count="$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs?head_sha=$SOURCE_SHA&event=push&status=success" \
--jq '[.workflow_runs[] | select(.head_branch == "main")] | length')"
test "$count" -gt 0 || { echo 'Successful main CI is required for this exact commit'; exit 1; }
- name: Install worker build dependencies
working-directory: packages/code
run: npm ci
- name: Test release guards and typecheck
run: |
node --test scripts/code-package-release.test.mjs
npm exec --prefix packages/code -- tsc --noEmit -p packages/code/tsconfig.json
- name: Build the candidate once
id: pack
run: node scripts/code-package-release.mjs pack "$RUNNER_TEMP/code-package"
- name: Focused protocol and enrollment regressions
working-directory: packages/code
run: >-
node --test dist/identity.test.js dist/pairing.test.js
dist/protocol.test.js dist/credential-recovery.test.js
dist/browser-setup.test.js dist/browser-protection.test.js
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: code-package
path: ${{ runner.temp }}/code-package/
if-no-files-found: error
retention-days: 7

verify:
name: Packed Worker (${{ matrix.runner }}, Node ${{ matrix.node }})
needs: pack
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
node: 22.23.4
- runner: ubuntu-latest
node: 24.22.1
- runner: ubuntu-24.04-arm
node: 24.22.1
- runner: macos-14
node: 24.22.1
- runner: macos-15-intel
node: 24.22.1
runs-on: ${{ matrix.runner }}
timeout-minutes: 10
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: ${{ matrix.node }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: code-package
path: ${{ runner.temp }}/code-package
- name: Verify the exact candidate bytes
run: node scripts/code-package-release.mjs verify "$RUNNER_TEMP/code-package"
- name: Install and smoke-test outside the source checkout
env:
PACKAGE_FILENAME: ${{ needs.pack.outputs.filename }}
run: node scripts/smoke-code-package.mjs "$RUNNER_TEMP/code-package/$PACKAGE_FILENAME"

publish:
name: Publish Verified Worker
needs: [pack, verify]
if: >-
github.repository == 'LibreChat-AI/code-interpreter' &&
(github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' && inputs.publish == true))
runs-on: ubuntu-latest
timeout-minutes: 5
environment: npm
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.pack.outputs.sha }}
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24.22.1
- name: Install the trusted-publishing npm CLI
run: npm install --global npm@11.13.0 --ignore-scripts
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: code-package
path: ${{ runner.temp }}/code-package
- name: Publish without rebuilding, or verify an identical prior publication
env:
EXPECTED_VERSION: ${{ inputs.version }}
CODE_PACKAGE_PUBLISH: 'true'
run: node scripts/code-package-release.mjs publish "$RUNNER_TEMP/code-package"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ data/

# Editor config
.vscode/

# Staged by the worker package prepack hook from the repository legal files.
/packages/code/LICENSE
/packages/code/NOTICE
5 changes: 3 additions & 2 deletions packages/code/package-lock.json

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

20 changes: 18 additions & 2 deletions packages/code/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@librechat/code",
"version": "0.1.0",
"version": "0.1.0-rc.1",
"description": "LibreChat stateful code environment protocol and worker CLI",
"license": "Apache-2.0",
"type": "module",
Expand Down Expand Up @@ -53,16 +53,19 @@
}
},
"bin": {
"lc-code": "./dist/cli.js",
"librechat-code": "./dist/cli.js"
},
"files": [
"dist",
"LICENSE",
"NOTICE",
"!dist/*.test.*"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "npm run build && node --test dist/*.test.js",
"prepack": "npm run build"
"prepack": "node scripts/prepare-package.mjs"
},
"devDependencies": {
"@types/node": "^22.5.5",
Expand All @@ -75,5 +78,18 @@
"@anthropic-ai/sandbox-runtime": "0.0.75",
"koffi": "3.2.1",
"yaml": "2.9.1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/LibreChat-AI/code-interpreter.git",
"directory": "packages/code"
},
"homepage": "https://github.com/LibreChat-AI/code-interpreter/tree/main/packages/code",
"bugs": {
"url": "https://github.com/LibreChat-AI/code-interpreter/issues"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
26 changes: 26 additions & 0 deletions packages/code/scripts/prepare-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { execFileSync } from 'node:child_process';
import { copyFile, rm } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';

const packageRoot = fileURLToPath(new URL('../', import.meta.url));
const require = createRequire(import.meta.url);
// Never carry stale compiler output or scratch files into the published artifact.
await rm(new URL('../dist/', import.meta.url), {
recursive: true,
force: true,
});
execFileSync(
process.execPath,
[require.resolve('typescript/bin/tsc'), '-p', 'tsconfig.json'],
{
cwd: packageRoot,
stdio: 'inherit',
}
);
for (const name of ['LICENSE', 'NOTICE']) {
await copyFile(
new URL(`../../../${name}`, import.meta.url),
new URL(`../${name}`, import.meta.url)
);
}
2 changes: 1 addition & 1 deletion packages/code/src/browser-setup-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,6 @@ export async function runBrowserSetupCommand(
});
} else {
const quoted = `'${profilePath.replace(/'/g, "'\\''")}'`;
process.stdout.write(`Start it with:\n librechat-code run${profilePath === resolve(defaultWorkerProfilePath()) ? '' : ` --profile ${quoted}`}\n`);
process.stdout.write(`Start it with:\n lc-code run${profilePath === resolve(defaultWorkerProfilePath()) ? '' : ` --profile ${quoted}`}\n`);
}
}
12 changes: 12 additions & 0 deletions packages/code/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';

test('CLI help presents lc-code as primary and retains the compatibility alias', () => {
const result = spawnSync(
process.execPath,
[fileURLToPath(new URL('./cli.js', import.meta.url)), '--help'],
{ encoding: 'utf8', timeout: 5000 },
);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /^Usage: lc-code <command>/);
assert.match(result.stdout, /Run lc-code setup --help/);
assert.match(result.stdout, /librechat-code remains available as a compatibility alias/);
});

test('CLI rejects an invalid worker ID before entering the run loop', () => {
const result = spawnSync(
process.execPath,
Expand Down
13 changes: 7 additions & 6 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1639,7 +1639,7 @@ async function run(
);
if (record != null) {
throw new BridgeProtocolError(
`Workspace mutations are quarantined since ${record.quarantinedAt}: ${record.reason}. Inspect or restore the workspace, then run librechat-code clear-workspace-quarantine`,
`Workspace mutations are quarantined since ${record.quarantinedAt}: ${record.reason}. Inspect or restore the workspace, then run lc-code clear-workspace-quarantine`,
undefined,
'WORKER_QUARANTINED',
);
Expand Down Expand Up @@ -1822,7 +1822,7 @@ async function clearMutationQuarantine(args: string[]): Promise<void> {
async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.length === 1 && (args[0] === '--help' || args[0] === 'help')) {
process.stdout.write(`Usage: librechat-code <command>
process.stdout.write(`Usage: lc-code <command>

setup --instance <url> Browser-approved enrollment and private profile setup
run [--profile <file>] Start a saved profile or existing worker configuration
Expand All @@ -1831,7 +1831,8 @@ async function main(): Promise<void> {
reset-workspace <id> Existing runtime maintenance
clear-workspace-quarantine Existing workspace maintenance

Run librechat-code setup --help for setup options.
Run lc-code setup --help for setup options.
librechat-code remains available as a compatibility alias.
Legacy LIBRECHAT_CODE_* configuration takes precedence over the default profile.
Explicit profiles cannot be mixed with legacy settings or run options.
`);
Expand All @@ -1846,7 +1847,7 @@ Explicit profiles cannot be mixed with legacy settings or run options.
for (let i = 1; i < args.length; i++) {
if (args[i] === '--apply') continue;
if (args[i] !== '--environment' || !args[i + 1] || args[i + 1].startsWith('--'))
throw new Error('Usage: librechat-code prune-environment-storage --environment <file> [--environment <file> ...] [--apply]');
throw new Error('Usage: lc-code prune-environment-storage --environment <file> [--environment <file> ...] [--apply]');
paths.push(args[++i]);
}
if (!paths.length || paths.length > 32) throw new Error('Supply between one and 32 environment definitions');
Expand All @@ -1867,7 +1868,7 @@ Explicit profiles cannot be mixed with legacy settings or run options.
const root = option(args, '--root');
if (!root || args.slice(1).some((arg, index, rest) =>
arg !== '--root' && rest[index - 1] !== '--root' && !arg.startsWith('--root='))) {
throw new Error('Usage: librechat-code projects --root <directory>');
throw new Error('Usage: lc-code projects --root <directory>');
}
const inventory = await discoverProjects({ root });
process.stdout.write(`${JSON.stringify(inventory, null, 2)}\n`);
Expand All @@ -1885,7 +1886,7 @@ Explicit profiles cannot be mixed with legacy settings or run options.
const runtimeSessionId = args[1]?.trim();
if (!runtimeSessionId) {
throw new Error(
'Usage: librechat-code reset-workspace <runtime-session-id>',
'Usage: lc-code reset-workspace <runtime-session-id>',
);
}
await run(runtimeSessionId);
Expand Down
2 changes: 1 addition & 1 deletion packages/code/src/projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,6 @@ test('CLI inventories a real checkout without pairing or starting a worker', asy
fileURLToPath(new URL('./cli.js', import.meta.url)),
'projects',
]),
/Usage: librechat-code projects/
/Usage: lc-code projects/
);
});
5 changes: 4 additions & 1 deletion packages/code/src/setup-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ test('CLI help explains the pairing prerequisite and setup does not silently rea
encoding: 'utf8',
});
assert.equal(help.status, 0, help.stderr);
assert.match(help.stdout, /^Usage: lc-code setup/);
assert.match(help.stdout, /lc-code setup --instance/);
assert.match(help.stdout, /librechat-code remains available as a compatibility alias/);
assert.match(help.stdout, /Settings > Code Environments/);
assert.match(help.stdout, /does not\nsign in to LibreChat/);
const guided = await execute(['setup'], env, code);
Expand Down Expand Up @@ -138,7 +141,7 @@ test(
`${code}\n`,
);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /librechat-code run\n/);
assert.match(result.stdout, /lc-code run\n/);
assert.match(result.stdout, /still needs to start/);
assert.ok(!result.stdout.includes(code));
assert.ok(!result.stderr.includes(code));
Expand Down
Loading
Loading