Skip to content
Closed
2 changes: 1 addition & 1 deletion .github/workflows/cli-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ jobs:
run: bun install -g bats

- name: Run CLI TypeScript tests
run: cross-env bun test tests/commands/
run: cross-env bun test tests/commands/ --timeout 300000
working-directory: packages/cli
env:
ELIZA_TEST_MODE: true
4 changes: 2 additions & 2 deletions packages/cli/bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# Preload test setup - removed due to path resolution conflicts in CI
# preload = ["packages/cli/tests/global-setup.ts", "packages/cli/tests/setup.ts"]

# Test configuration - increased for CI environments with slow Ubuntu 22.04
timeout = 180000
# Test configuration - increased for CI environments (macOS has slower npm operations)
timeout = 300000
bail = 0

# Coverage configuration
Expand Down
26 changes: 10 additions & 16 deletions packages/cli/src/scripts/copy-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,24 @@ const ROOT_DIR = path.resolve(__dirname, '../../../..');
const TEMPLATES_DIR = path.resolve(ROOT_DIR, 'packages/cli/templates');

/**
* Updates package.json with the CLI version and replaces workspace references
* Updates package.json and replaces workspace references with 'latest'
* so that created projects can always install from npm
*/
async function updatePackageJson(packagePath: string, cliVersion: string) {
async function updatePackageJson(packagePath: string) {
const packageJsonContent = await fs.readFile(packagePath, 'utf-8');
const packageData = JSON.parse(packageJsonContent);

// Use a standard initial version for new packages
packageData.version = '0.1.0';

// Replace workspace references in dependencies
// Replace workspace references in dependencies with 'latest'
// This ensures created projects can install packages from npm
// regardless of the CLI version used to create them
for (const section of ['dependencies', 'devDependencies']) {
if (packageData[section]) {
for (const [dep, version] of Object.entries(packageData[section])) {
if (version === 'workspace:*') {
packageData[section][dep] = cliVersion;
packageData[section][dep] = 'latest';
}
}
Comment on lines 33 to 39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overwrites non-workspace versions

The build-time template rewriter replaces any dependency in dependencies/devDependencies whose version is exactly workspace:* with 'latest' (packages/cli/src/scripts/copy-templates.ts:33-39) without checking the package scope. If the starter templates ever include non-@elizaos/* workspace deps (common in monorepos), those will be rewritten to 'latest' too, which will break installs because those packages likely aren't published. This should probably be limited to @elizaos/* (matching the PR intent) or to a known allowlist.

}
Expand All @@ -58,11 +61,6 @@ async function main() {
await fs.emptyDir(TEMPLATES_DIR);
}

// Get CLI version from package.json
const cliPackageJsonPath = path.resolve(ROOT_DIR, 'packages/cli/package.json');
const cliPackageData = JSON.parse(await fs.readFile(cliPackageJsonPath, 'utf-8'));
const cliVersion = cliPackageData.version;

// Define templates to copy
const templates = [
{
Expand Down Expand Up @@ -92,17 +90,13 @@ async function main() {
await fs.copy(template.src, template.dest, {
filter: (srcPath) => {
const baseName = path.basename(srcPath);
if (baseName === 'node_modules' || baseName === '.git') {
// console.log(`Filtering out: ${srcPath}`); // Log which paths are being filtered
return false;
}
return true;
return baseName !== 'node_modules' && baseName !== '.git';
},
});

// Update package.json with correct version
// Update package.json to use 'latest' for @elizaos dependencies
const packageJsonPath = path.resolve(template.dest, 'package.json');
await updatePackageJson(packageJsonPath, cliVersion);
await updatePackageJson(packageJsonPath);
}

console.log('Templates have been copied and updated successfully.');
Expand Down
74 changes: 17 additions & 57 deletions packages/cli/src/utils/copy-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,10 @@ import { fileURLToPath } from 'node:url';
import { logger } from '@elizaos/core';
import { isQuietMode } from './spinner-utils';

// Define __dirname for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const __dirname = path.dirname(fileURLToPath(import.meta.url));

/**
* Copy a directory recursively
*/
/**
* Asynchronously copies the contents of a directory from a source path to a destination path, excluding specified files and directories.
* If the destination directory does not exist, it will be created.
*
* @param {string} src - The path to the source directory.
* @param {string} dest - The path to the destination directory.
* @param {string[]} [exclude=[]] - An array of file and directory names to exclude from the copy operation.
* @returns {Promise<void>} A Promise that resolves when the copy operation is complete.
* Copies a directory recursively from source to destination, excluding specified files and directories.
*/
export async function copyDir(src: string, dest: string, exclude: string[] = []) {
// Ensure paths are properly resolved as absolute paths
Expand Down Expand Up @@ -116,19 +105,11 @@ export async function copyTemplate(

// Try multiple locations to find templates, handling different runtime environments
const possibleTemplatePaths = [
// 1. Direct path from source directory (for tests and development)
path.resolve(__dirname, '../../templates', packageName),
// 2. Production: when running from dist, templates are at the same level
path.resolve(__dirname, '../templates', packageName),
// 3. Alternative production path (if utils is nested in dist)
path.resolve(__dirname, '../../templates', packageName),
// 4. Development: templates at package root
path.resolve(__dirname, '../../../templates', packageName),
// 5. Fallback for various directory structures
path.resolve(__dirname, 'templates', packageName),
// 6. Fallback to monorepo packages (when templates haven't been copied yet)
path.resolve(__dirname, '../../../..', packageName),
// 7. Alternative monorepo package path (from utils to packages directory)
path.resolve(__dirname, '../../../../packages', packageName),
];

Expand Down Expand Up @@ -175,49 +156,28 @@ export async function copyTemplate(
);
}

// Normalize workspace references only; do not pin versions to CLI version
const normalizeElizaDep = (currentVersion: string): string => {
// Convert workspace:* or workspace:^ to public registry 'latest'
if (typeof currentVersion === 'string' && currentVersion.startsWith('workspace:')) {
return 'latest';
// Set @elizaos dependencies to 'latest' for npm installability
// For local development testing, use `elizaos create --local` which links packages after creation
const normalizeElizaDeps = (deps: Record<string, string>, isDevDeps = false): void => {
for (const depName of Object.keys(deps)) {
if (depName.startsWith('@elizaos/') && deps[depName] !== 'latest') {
Comment on lines +159 to +163
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaks --local flow

copyTemplate() now forces all @elizaos/* deps (including workspace:*) to 'latest' (packages/cli/src/utils/copy-template.ts:159-170). That makes elizaos create --local unable to rely on workspace resolution/linking after creation, because the workspace protocol has already been destroyed in the generated package.json. If --local is intended to produce a monorepo-linked project, this change will make it install from npm instead (and could fail when local unpublished changes are required). Consider gating the 'latest' normalization behind the non-local path (or only converting workspace: specs while leaving non-workspace semver ranges unchanged).

if (!isQuietMode()) {
logger.info(
{ src: 'cli', util: 'copy-template', depName, version: 'latest' },
isDevDeps ? 'Setting dev dependency version' : 'Setting dependency version'
);
}
deps[depName] = 'latest';
}
}
return currentVersion;
};

if (packageJson.dependencies) {
for (const depName of Object.keys(packageJson.dependencies)) {
if (depName.startsWith('@elizaos/')) {
const before = packageJson.dependencies[depName];
const after = normalizeElizaDep(before);
if (after !== before) {
if (!isQuietMode()) {
logger.info(
{ src: 'cli', util: 'copy-template', depName, version: after },
'Setting dependency version'
);
}
packageJson.dependencies[depName] = after;
}
}
}
normalizeElizaDeps(packageJson.dependencies);
}

if (packageJson.devDependencies) {
for (const depName of Object.keys(packageJson.devDependencies)) {
if (depName.startsWith('@elizaos/')) {
const before = packageJson.devDependencies[depName];
const after = normalizeElizaDep(before);
if (after !== before) {
if (!isQuietMode()) {
logger.info(
{ src: 'cli', util: 'copy-template', depName, version: after },
'Setting dev dependency version'
);
}
packageJson.devDependencies[depName] = after;
}
}
}
normalizeElizaDeps(packageJson.devDependencies, true);
}

// Update the package name to use the actual name provided by the user
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/tests/commands/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ describe('ElizaOS Update Commands', { timeout: TEST_TIMEOUTS.SUITE_TIMEOUT }, ()

// Helper function to create project
const makeProj = async (name: string) => {
bunExecSync(`elizaos create ${name} --yes`, { encoding: 'utf8' });
bunExecSync(`elizaos create ${name} --yes`, {
encoding: 'utf8',
timeout: TEST_TIMEOUTS.PROJECT_CREATION,
});
process.chdir(join(testTmpDir, name));
};

Expand Down
Loading
Loading