Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/meaningful-orange-kangaroo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inkeep/create-agents": patch
---

Sync template @inkeep/* dependency versions to match CLI version after clone
162 changes: 161 additions & 1 deletion packages/create-agents/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as p from '@clack/prompts';
import fs from 'fs-extra';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cloneTemplate, cloneTemplateLocal, getAvailableTemplates } from '../templates';
import { createAgents, defaultMockModelConfigurations } from '../utils';
import { createAgents, defaultMockModelConfigurations, syncTemplateDependencies } from '../utils';

// Create the mock execAsync function that will be used by promisify - hoisted so it's available in mocks
const { mockExecAsync } = vi.hoisted(() => ({
Expand All @@ -25,6 +25,20 @@ vi.mock('node:child_process', () => ({
vi.mock('node:util', () => ({
promisify: vi.fn(() => mockExecAsync),
}));
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
return {
...actual,
readFileSync: vi.fn(() => JSON.stringify({ version: '1.2.3' })),
};
});
vi.mock('node:url', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:url')>();
return {
...actual,
fileURLToPath: vi.fn(() => '/fake/dist/utils.js'),
};
});

// Setup default mocks
const mockSpinner = {
Expand Down Expand Up @@ -663,3 +677,149 @@ function setupDefaultMocks() {
vi.mocked(cloneTemplateLocal).mockResolvedValue(undefined);
mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
}

describe('syncTemplateDependencies', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should update @inkeep/* dependencies to match CLI version', async () => {
const mockPkg = {
name: 'test-project',
dependencies: {
'@inkeep/agents-core': '^0.50.3',
'@inkeep/agents-sdk': '^0.50.3',
'some-other-package': '^1.0.0',
},
};
vi.mocked(fs.pathExists).mockResolvedValue(true as any);
vi.mocked(fs.readJson).mockResolvedValue(mockPkg);
vi.mocked(fs.writeJson).mockResolvedValue(undefined);

await syncTemplateDependencies('/test/path');

expect(fs.writeJson).toHaveBeenCalledWith(
'/test/path/package.json',
expect.objectContaining({
dependencies: {
'@inkeep/agents-core': '^1.2.3',
'@inkeep/agents-sdk': '^1.2.3',
'some-other-package': '^1.0.0',
},
}),
{ spaces: 2 }
);
});

it('should skip if template package.json does not exist', async () => {
vi.mocked(fs.pathExists).mockResolvedValue(false as any);

await syncTemplateDependencies('/test/path');

expect(fs.readJson).not.toHaveBeenCalled();
expect(fs.writeJson).not.toHaveBeenCalled();
});

it('should handle template with no @inkeep/* dependencies', async () => {
const mockPkg = {
name: 'test-project',
dependencies: {
'some-other-package': '^1.0.0',
},
};
vi.mocked(fs.pathExists).mockResolvedValue(true as any);
vi.mocked(fs.readJson).mockResolvedValue(mockPkg);
vi.mocked(fs.writeJson).mockResolvedValue(undefined);

await syncTemplateDependencies('/test/path');

expect(fs.writeJson).toHaveBeenCalledWith(
'/test/path/package.json',
expect.objectContaining({
dependencies: {
'some-other-package': '^1.0.0',
},
}),
{ spaces: 2 }
);
});

it('should handle template with no devDependencies', async () => {
const mockPkg = {
name: 'test-project',
dependencies: {
'@inkeep/agents-core': '^0.50.3',
},
};
vi.mocked(fs.pathExists).mockResolvedValue(true as any);
vi.mocked(fs.readJson).mockResolvedValue(mockPkg);
vi.mocked(fs.writeJson).mockResolvedValue(undefined);

await syncTemplateDependencies('/test/path');

expect(fs.writeJson).toHaveBeenCalledWith(
'/test/path/package.json',
expect.objectContaining({
dependencies: {
'@inkeep/agents-core': '^1.2.3',
},
}),
{ spaces: 2 }
);
});
Comment thread
nick-inkeep marked this conversation as resolved.

it('should update devDependencies @inkeep/* packages', async () => {
const mockPkg = {
name: 'test-project',
dependencies: {},
devDependencies: {
'@inkeep/agents-sdk': '^0.49.0',
vitest: '^1.0.0',
},
};
vi.mocked(fs.pathExists).mockResolvedValue(true as any);
vi.mocked(fs.readJson).mockResolvedValue(mockPkg);
vi.mocked(fs.writeJson).mockResolvedValue(undefined);

await syncTemplateDependencies('/test/path');

expect(fs.writeJson).toHaveBeenCalledWith(
'/test/path/package.json',
expect.objectContaining({
devDependencies: {
'@inkeep/agents-sdk': '^1.2.3',
vitest: '^1.0.0',
},
}),
{ spaces: 2 }
);
});

it('should not modify non-@inkeep dependencies', async () => {
const mockPkg = {
name: 'test-project',
dependencies: {
'@inkeep/agents-core': '^0.50.3',
react: '^18.0.0',
next: '^14.0.0',
},
};
vi.mocked(fs.pathExists).mockResolvedValue(true as any);
vi.mocked(fs.readJson).mockResolvedValue(mockPkg);
vi.mocked(fs.writeJson).mockResolvedValue(undefined);

await syncTemplateDependencies('/test/path');

expect(fs.writeJson).toHaveBeenCalledWith(
'/test/path/package.json',
expect.objectContaining({
dependencies: {
'@inkeep/agents-core': '^1.2.3',
react: '^18.0.0',
next: '^14.0.0',
},
}),
{ spaces: 2 }
);
});
});
35 changes: 35 additions & 0 deletions packages/create-agents/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { exec } from 'node:child_process';
import { readFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import * as p from '@clack/prompts';
import { ANTHROPIC_MODELS, GOOGLE_MODELS, OPENAI_MODELS } from '@inkeep/agents-core';
Expand Down Expand Up @@ -42,6 +44,37 @@ const execAsync = promisify(exec);

const agentsApiPort = '3002';

function getCliVersion(): string {
try {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkgJson = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
return pkgJson.version;
} catch {
return '';
}
}
Comment thread
nick-inkeep marked this conversation as resolved.

export async function syncTemplateDependencies(templatePath: string): Promise<void> {
const pkgPath = path.join(templatePath, 'package.json');
if (!(await fs.pathExists(pkgPath))) return;

const pkg = await fs.readJson(pkgPath);
const cliVersion = getCliVersion();
if (!cliVersion) return;

for (const depType of ['dependencies', 'devDependencies'] as const) {
const deps = pkg[depType];
if (!deps) continue;
for (const name of Object.keys(deps)) {
if (name.startsWith('@inkeep/')) {
deps[name] = `^${cliVersion}`;
}
}
}

await fs.writeJson(pkgPath, pkg, { spaces: 2 });
}

export const defaultGoogleModelConfigurations = {
base: {
model: GOOGLE_MODELS.GEMINI_2_5_FLASH,
Expand Down Expand Up @@ -398,6 +431,8 @@ export const createAgents = async (

process.chdir(directoryPath);

await syncTemplateDependencies('.');

const config = {
dirName,
tenantId,
Expand Down