@inkeep/create-agents 0.29.10 → 0.30.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,175 @@
1
+ import path from 'node:path';
2
+ import { execa } from 'execa';
3
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
+ import { cleanupDir, createTempDir, linkLocalPackages, runCommand, runCreateAgentsCLI, verifyDirectoryStructure, verifyFile, waitForServerReady, } from './utils';
5
+ const manageApiUrl = 'http://localhost:3002';
6
+ describe('create-agents quickstart e2e', () => {
7
+ let testDir;
8
+ let projectDir;
9
+ const workspaceName = 'test-project';
10
+ const projectId = 'activities-planner';
11
+ beforeEach(async () => {
12
+ // Create a temporary directory for each test
13
+ testDir = await createTempDir();
14
+ projectDir = path.join(testDir, workspaceName);
15
+ });
16
+ afterEach(async () => {
17
+ await cleanupDir(testDir);
18
+ });
19
+ it('should work e2e', async () => {
20
+ const monorepoRoot = path.join(__dirname, '../../../../../');
21
+ const createAgentsPrefix = path.join(monorepoRoot, 'create-agents-template');
22
+ const projectTemplatesPrefix = path.join(monorepoRoot, 'agents-cookbook/template-projects');
23
+ // Run the CLI with all options (non-interactive mode)
24
+ console.log('Running CLI with options:');
25
+ console.log(`Working directory: ${testDir}`);
26
+ const result = await runCreateAgentsCLI([
27
+ workspaceName,
28
+ '--openai-key',
29
+ 'test-openai-key',
30
+ '--disable-git', // Skip git init for faster tests
31
+ '--local-agents-prefix',
32
+ createAgentsPrefix,
33
+ '--local-templates-prefix',
34
+ projectTemplatesPrefix,
35
+ ], testDir);
36
+ // Verify the CLI completed successfully
37
+ expect(result.exitCode).toBe(0);
38
+ console.log('CLI completed successfully');
39
+ // Verify the core directory structure
40
+ console.log('Verifying directory structure...');
41
+ await verifyDirectoryStructure(projectDir, [
42
+ 'src',
43
+ 'src/inkeep.config.ts',
44
+ `src/projects/${projectId}`,
45
+ 'apps/manage-api',
46
+ 'apps/run-api',
47
+ 'apps/mcp',
48
+ 'apps/manage-ui',
49
+ '.env',
50
+ 'package.json',
51
+ 'drizzle.config.ts',
52
+ ]);
53
+ console.log('Directory structure verified');
54
+ // Verify .env file has required variables
55
+ console.log('Verifying .env file...');
56
+ await verifyFile(path.join(projectDir, '.env'), [
57
+ /ENVIRONMENT=development/,
58
+ /OPENAI_API_KEY=test-openai-key/,
59
+ /DB_FILE_NAME=file:.*\/local\.db/,
60
+ /INKEEP_AGENTS_MANAGE_API_URL="http:\/\/localhost:3002"/,
61
+ /INKEEP_AGENTS_RUN_API_URL="http:\/\/localhost:3003"/,
62
+ /INKEEP_AGENTS_JWT_SIGNING_SECRET=\w+/, // Random secret should be generated
63
+ ]);
64
+ console.log('.env file verified');
65
+ // Verify inkeep.config.ts was created
66
+ console.log('Verifying inkeep.config.ts...');
67
+ await verifyFile(path.join(projectDir, 'src/inkeep.config.ts'));
68
+ console.log('inkeep.config.ts verified');
69
+ console.log('Starting dev servers');
70
+ // Start dev servers in background with output monitoring
71
+ const devProcess = execa('pnpm', ['dev'], {
72
+ cwd: path.join(projectDir, 'apps/manage-api'),
73
+ env: {
74
+ ...process.env,
75
+ FORCE_COLOR: '0',
76
+ NODE_ENV: 'test',
77
+ },
78
+ cleanup: true,
79
+ detached: false,
80
+ stderr: 'pipe',
81
+ });
82
+ // Monitor output for errors and readiness signals
83
+ let serverOutput = '';
84
+ const outputHandler = (data) => {
85
+ const text = data.toString();
86
+ serverOutput += text;
87
+ // Log important messages in CI
88
+ if (process.env.CI) {
89
+ if (text.includes('Error') || text.includes('EADDRINUSE') || text.includes('ready')) {
90
+ console.log('[Server]:', text.trim());
91
+ }
92
+ }
93
+ };
94
+ if (devProcess.stderr)
95
+ devProcess.stderr.on('data', outputHandler);
96
+ // Handle process crashes during startup
97
+ devProcess.catch((error) => {
98
+ console.error('Dev process crashed during startup:', error.message);
99
+ console.error('Server output:', serverOutput);
100
+ });
101
+ console.log('Waiting for servers to be ready');
102
+ try {
103
+ // Wait for servers to be ready with retries
104
+ await waitForServerReady(`${manageApiUrl}/health`, 120000); // Increased to 2 minutes for CI
105
+ console.log('Manage API is ready');
106
+ console.log('Pushing project');
107
+ const pushResult = await runCommand('pnpm', [
108
+ 'inkeep',
109
+ 'push',
110
+ '--project',
111
+ `src/projects/${projectId}`,
112
+ '--config',
113
+ 'src/inkeep.config.ts',
114
+ ], projectDir, 30000);
115
+ expect(pushResult.exitCode).toBe(0);
116
+ console.log('Testing API requests');
117
+ // Test API requests
118
+ const response = await fetch(`${manageApiUrl}/tenants/default/projects/${projectId}`);
119
+ const data = await response.json();
120
+ expect(data.data.tenantId).toBe('default');
121
+ expect(data.data.id).toBe(projectId);
122
+ // Link to local monorepo packages
123
+ await linkLocalPackages(projectDir, monorepoRoot);
124
+ const pushResultLocal = await runCommand('pnpm', [
125
+ 'inkeep',
126
+ 'push',
127
+ '--project',
128
+ `src/projects/${projectId}`,
129
+ '--config',
130
+ 'src/inkeep.config.ts',
131
+ ], projectDir, 30000);
132
+ expect(pushResultLocal.exitCode).toBe(0);
133
+ // Test that the project works with local packages
134
+ const responseLocal = await fetch(`${manageApiUrl}/tenants/default/projects/${projectId}`);
135
+ expect(responseLocal.status).toBe(200);
136
+ }
137
+ catch (error) {
138
+ console.error('Test failed with error:', error);
139
+ // Print server output for debugging
140
+ if (devProcess.stdout) {
141
+ const stdout = await devProcess.stdout;
142
+ console.log('Server stdout:', stdout);
143
+ }
144
+ if (devProcess.stderr) {
145
+ const stderr = await devProcess.stderr;
146
+ console.error('Server stderr:', stderr);
147
+ }
148
+ throw error;
149
+ }
150
+ finally {
151
+ console.log('Killing dev process');
152
+ // Kill the process and wait for it to die
153
+ try {
154
+ devProcess.kill('SIGTERM');
155
+ }
156
+ catch {
157
+ // Might already be dead
158
+ }
159
+ // Give it 2 seconds to shut down gracefully, then force kill
160
+ await new Promise((resolve) => setTimeout(resolve, 2000));
161
+ try {
162
+ devProcess.kill('SIGKILL');
163
+ }
164
+ catch {
165
+ // Already dead or couldn't kill
166
+ }
167
+ // Wait for the process to be fully cleaned up (with timeout)
168
+ await Promise.race([
169
+ devProcess.catch(() => { }), // Wait for process to exit
170
+ new Promise((resolve) => setTimeout(resolve, 5000)), // Or timeout after 5s
171
+ ]);
172
+ console.log('Dev process cleanup complete');
173
+ }
174
+ }, 720000); // 12 minute timeout for full flow with network calls (CI can be slow)
175
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Run the create-agents CLI with the given arguments
3
+ */
4
+ export declare function runCreateAgentsCLI(args: string[], cwd: string, timeout?: number): Promise<{
5
+ stdout: string;
6
+ stderr: string;
7
+ exitCode: number | undefined;
8
+ }>;
9
+ /**
10
+ * Run a command in the created project directory
11
+ */
12
+ export declare function runCommand(command: string, args: string[], cwd: string, timeout?: number): Promise<{
13
+ stdout: string;
14
+ stderr: string;
15
+ exitCode: number | undefined;
16
+ }>;
17
+ /**
18
+ * Create a temporary directory for testing
19
+ */
20
+ export declare function createTempDir(prefix?: string): Promise<string>;
21
+ /**
22
+ * Clean up a test directory with retries
23
+ */
24
+ export declare function cleanupDir(dir: string): Promise<void>;
25
+ /**
26
+ * Verify that a file exists and optionally check its contents
27
+ */
28
+ export declare function verifyFile(filePath: string, expectedContents?: string[] | RegExp[]): Promise<void>;
29
+ /**
30
+ * Verify that a directory has the expected structure
31
+ */
32
+ export declare function verifyDirectoryStructure(baseDir: string, expectedPaths: string[]): Promise<void>;
33
+ /**
34
+ * Link local monorepo packages to the created project
35
+ * This replaces published @inkeep packages with local versions for testing
36
+ */
37
+ export declare function linkLocalPackages(projectDir: string, monorepoRoot: string): Promise<void>;
38
+ /**
39
+ * Wait for a server to be ready by polling a health endpoint
40
+ */
41
+ export declare function waitForServerReady(url: string, timeout: number): Promise<void>;
@@ -0,0 +1,217 @@
1
+ import os from 'node:os';
2
+ import path, { dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { execa } from 'execa';
5
+ import fs from 'fs-extra';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ /**
8
+ * Run the create-agents CLI with the given arguments
9
+ */
10
+ export async function runCreateAgentsCLI(args, cwd, timeout = 300000 // 5 minutes default for full flow
11
+ ) {
12
+ const cliPath = path.join(__dirname, '../../../src/index.ts');
13
+ try {
14
+ // Run using tsx to execute TypeScript directly
15
+ const result = await execa('tsx', [cliPath, ...args], {
16
+ cwd,
17
+ timeout,
18
+ env: { ...process.env, FORCE_COLOR: '0' }, // Disable colors for easier assertion
19
+ all: true, // Capture combined stdout + stderr in order
20
+ });
21
+ return {
22
+ stdout: result.stdout,
23
+ stderr: result.stderr,
24
+ exitCode: result.exitCode,
25
+ };
26
+ }
27
+ catch (error) {
28
+ // execa throws on non-zero exit codes, capture the error info
29
+ return {
30
+ stdout: error.stdout || '',
31
+ stderr: error.stderr || '',
32
+ exitCode: error.exitCode || 1,
33
+ };
34
+ }
35
+ }
36
+ /**
37
+ * Run a command in the created project directory
38
+ */
39
+ export async function runCommand(command, args, cwd, timeout = 120000 // 2 minutes default
40
+ ) {
41
+ try {
42
+ const result = await execa(command, args, {
43
+ cwd,
44
+ timeout,
45
+ env: { ...process.env, FORCE_COLOR: '0' },
46
+ shell: true,
47
+ });
48
+ return {
49
+ stdout: result.stdout,
50
+ stderr: result.stderr,
51
+ exitCode: result.exitCode,
52
+ };
53
+ }
54
+ catch (error) {
55
+ return {
56
+ stdout: error.stdout || '',
57
+ stderr: error.stderr || '',
58
+ exitCode: error.exitCode || 1,
59
+ };
60
+ }
61
+ }
62
+ /**
63
+ * Create a temporary directory for testing
64
+ */
65
+ export async function createTempDir(prefix = 'create-agents-e2e-') {
66
+ return fs.mkdtemp(path.join(os.tmpdir(), prefix));
67
+ }
68
+ /**
69
+ * Clean up a test directory with retries
70
+ */
71
+ export async function cleanupDir(dir) {
72
+ if (!(await fs.pathExists(dir))) {
73
+ return;
74
+ }
75
+ try {
76
+ // Try multiple times with delays (common in CI)
77
+ for (let i = 0; i < 3; i++) {
78
+ try {
79
+ await fs.remove(dir);
80
+ return;
81
+ }
82
+ catch (error) {
83
+ if (i === 2)
84
+ throw error; // Last attempt, throw the error
85
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1s before retry
86
+ }
87
+ }
88
+ }
89
+ catch (error) {
90
+ // If still failing, try force removal
91
+ if (error.code === 'ENOTEMPTY' || error.code === 'EBUSY') {
92
+ await execa('rm', ['-rf', dir], { shell: true }).catch(() => {
93
+ console.warn(`Failed to clean up ${dir}`);
94
+ });
95
+ }
96
+ }
97
+ }
98
+ /**
99
+ * Verify that a file exists and optionally check its contents
100
+ */
101
+ export async function verifyFile(filePath, expectedContents) {
102
+ const exists = await fs.pathExists(filePath);
103
+ if (!exists) {
104
+ throw new Error(`Expected file to exist: ${filePath}`);
105
+ }
106
+ if (expectedContents) {
107
+ const content = await fs.readFile(filePath, 'utf-8');
108
+ for (const expected of expectedContents) {
109
+ if (typeof expected === 'string') {
110
+ if (!content.includes(expected)) {
111
+ throw new Error(`Expected file ${filePath} to contain: ${expected}`);
112
+ }
113
+ }
114
+ else {
115
+ if (!expected.test(content)) {
116
+ throw new Error(`Expected file ${filePath} to match pattern: ${expected}`);
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * Verify that a directory has the expected structure
124
+ */
125
+ export async function verifyDirectoryStructure(baseDir, expectedPaths) {
126
+ for (const expectedPath of expectedPaths) {
127
+ const fullPath = path.join(baseDir, expectedPath);
128
+ const exists = await fs.pathExists(fullPath);
129
+ if (!exists) {
130
+ throw new Error(`Expected path to exist: ${fullPath}`);
131
+ }
132
+ }
133
+ }
134
+ /**
135
+ * Link local monorepo packages to the created project
136
+ * This replaces published @inkeep packages with local versions for testing
137
+ */
138
+ export async function linkLocalPackages(projectDir, monorepoRoot) {
139
+ const packageJsonPaths = [
140
+ path.join(projectDir, 'package.json'),
141
+ path.join(projectDir, 'apps/manage-api/package.json'),
142
+ path.join(projectDir, 'apps/run-api/package.json'),
143
+ ];
144
+ const packageJsons = {};
145
+ for (const packageJsonPath of packageJsonPaths) {
146
+ packageJsons[packageJsonPath] = await fs.readJson(packageJsonPath);
147
+ }
148
+ // Define local @inkeep packages to link
149
+ const inkeepPackages = {
150
+ '@inkeep/agents-sdk': `link:${path.join(monorepoRoot, 'packages/agents-sdk')}`,
151
+ '@inkeep/agents-core': `link:${path.join(monorepoRoot, 'packages/agents-core')}`,
152
+ '@inkeep/agents-manage-api': `link:${path.join(monorepoRoot, 'agents-manage-api')}`,
153
+ '@inkeep/agents-run-api': `link:${path.join(monorepoRoot, 'agents-run-api')}`,
154
+ '@inkeep/agents-cli': `link:${path.join(monorepoRoot, 'agents-cli')}`,
155
+ };
156
+ // Replace package versions with local links
157
+ for (const [pkg, linkPath] of Object.entries(inkeepPackages)) {
158
+ for (const packageJsonPath of packageJsonPaths) {
159
+ if (packageJsons[packageJsonPath].dependencies?.[pkg]) {
160
+ packageJsons[packageJsonPath].dependencies[pkg] = linkPath;
161
+ }
162
+ if (packageJsons[packageJsonPath].devDependencies?.[pkg]) {
163
+ packageJsons[packageJsonPath].devDependencies[pkg] = linkPath;
164
+ }
165
+ }
166
+ }
167
+ // Write updated package.json
168
+ for (const packageJsonPath of packageJsonPaths) {
169
+ await fs.writeJson(packageJsonPath, packageJsons[packageJsonPath], { spaces: 2 });
170
+ }
171
+ // Reinstall to create the symlinks
172
+ await execa('pnpm', ['install', '--no-frozen-lockfile'], {
173
+ cwd: projectDir,
174
+ env: { ...process.env, FORCE_COLOR: '0' },
175
+ });
176
+ }
177
+ /**
178
+ * Wait for a server to be ready by polling a health endpoint
179
+ */
180
+ export async function waitForServerReady(url, timeout) {
181
+ const start = Date.now();
182
+ let lastError = null;
183
+ let attempts = 0;
184
+ console.log(`Waiting for server at ${url}...`);
185
+ while (Date.now() - start < timeout) {
186
+ attempts++;
187
+ try {
188
+ const response = await fetch(url, {
189
+ signal: AbortSignal.timeout(5000), // 5 second timeout per request
190
+ });
191
+ if (response.ok) {
192
+ console.log(`✓ Server ready at ${url} after ${attempts} attempts (${Date.now() - start}ms)`);
193
+ return;
194
+ }
195
+ lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
196
+ // Log status every 10 attempts in CI
197
+ if (process.env.CI && attempts % 10 === 0) {
198
+ console.log(`Still waiting for ${url}... (attempt ${attempts}, ${Math.floor((Date.now() - start) / 1000)}s elapsed)`);
199
+ }
200
+ }
201
+ catch (error) {
202
+ // Server not ready yet or connection refused
203
+ lastError = error instanceof Error ? error : new Error(String(error));
204
+ // Log connection errors periodically in CI
205
+ if (process.env.CI && attempts % 15 === 0) {
206
+ console.log(`Connection attempt ${attempts} failed: ${lastError.message}`);
207
+ }
208
+ }
209
+ // Wait before next attempt (exponential backoff up to 5s)
210
+ const waitTime = Math.min(1000 + attempts * 100, 5000);
211
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
212
+ }
213
+ // Timeout reached - provide detailed error
214
+ const elapsed = Date.now() - start;
215
+ const errorDetails = lastError ? `: ${lastError.message}` : '';
216
+ throw new Error(`Server not ready at ${url} after ${elapsed}ms (${attempts} attempts)${errorDetails}`);
217
+ }
@@ -1,7 +1,7 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import fs from 'fs-extra';
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
- import { cloneTemplate, getAvailableTemplates } from '../templates';
4
+ import { cloneTemplate, cloneTemplateLocal, getAvailableTemplates } from '../templates';
5
5
  import { createAgents } from '../utils';
6
6
  // Mock all dependencies
7
7
  vi.mock('fs-extra');
@@ -55,6 +55,7 @@ describe('createAgents - Template and Project ID Logic', () => {
55
55
  'data-analysis',
56
56
  ]);
57
57
  vi.mocked(cloneTemplate).mockResolvedValue(undefined);
58
+ vi.mocked(cloneTemplateLocal).mockResolvedValue(undefined);
58
59
  // Mock util.promisify to return a mock exec function
59
60
  const mockExecAsync = vi.fn().mockResolvedValue({ stdout: '', stderr: '' });
60
61
  const util = require('node:util');
@@ -79,10 +80,10 @@ describe('createAgents - Template and Project ID Logic', () => {
79
80
  openAiKey: 'test-openai-key',
80
81
  anthropicKey: 'test-anthropic-key',
81
82
  });
82
- // Should clone base template and weather-project template
83
+ // Should clone base template and activities-planner template
83
84
  expect(cloneTemplate).toHaveBeenCalledTimes(2);
84
- expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
85
- expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/template-projects/activities-planner', 'src/projects/activities-planner', expect.arrayContaining([
85
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents/create-agents-template', expect.any(String), undefined);
86
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents/agents-cookbook/template-projects/activities-planner', 'src/projects/activities-planner', expect.arrayContaining([
86
87
  expect.objectContaining({
87
88
  filePath: 'index.ts',
88
89
  replacements: expect.objectContaining({
@@ -117,8 +118,8 @@ describe('createAgents - Template and Project ID Logic', () => {
117
118
  expect(getAvailableTemplates).toHaveBeenCalled();
118
119
  // Should clone base template and the specified template
119
120
  expect(cloneTemplate).toHaveBeenCalledTimes(2);
120
- expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
121
- expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/template-projects/chatbot', 'src/projects/chatbot', expect.arrayContaining([
121
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents/create-agents-template', expect.any(String), undefined);
122
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents/agents-cookbook/template-projects/chatbot', 'src/projects/chatbot', expect.arrayContaining([
122
123
  expect.objectContaining({
123
124
  filePath: 'index.ts',
124
125
  replacements: expect.objectContaining({
@@ -168,7 +169,7 @@ describe('createAgents - Template and Project ID Logic', () => {
168
169
  });
169
170
  // Should clone base template but NOT project template
170
171
  expect(cloneTemplate).toHaveBeenCalledTimes(1);
171
- expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
172
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents/create-agents-template', expect.any(String), undefined);
172
173
  // Should NOT validate templates
173
174
  expect(getAvailableTemplates).not.toHaveBeenCalled();
174
175
  // Should create empty project directory
@@ -190,7 +191,7 @@ describe('createAgents - Template and Project ID Logic', () => {
190
191
  });
191
192
  // Should only clone base template, not project template
192
193
  expect(cloneTemplate).toHaveBeenCalledTimes(1);
193
- expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
194
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents/create-agents-template', expect.any(String), undefined);
194
195
  expect(getAvailableTemplates).not.toHaveBeenCalled();
195
196
  expect(fs.ensureDir).toHaveBeenCalledWith('src/projects/my-custom-project');
196
197
  // Check that .env file is created
@@ -214,7 +215,7 @@ describe('createAgents - Template and Project ID Logic', () => {
214
215
  anthropicKey: 'test-key',
215
216
  });
216
217
  expect(cloneTemplate).toHaveBeenCalledTimes(2);
217
- expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/template-projects/my-complex-template', 'src/projects/my-complex-template', expect.arrayContaining([
218
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents/agents-cookbook/template-projects/my-complex-template', 'src/projects/my-complex-template', expect.arrayContaining([
218
219
  expect.objectContaining({
219
220
  filePath: 'index.ts',
220
221
  replacements: expect.objectContaining({
@@ -328,4 +329,5 @@ function setupDefaultMocks() {
328
329
  vi.mocked(fs.writeFile).mockResolvedValue(undefined);
329
330
  vi.mocked(getAvailableTemplates).mockResolvedValue(['event-planner', 'chatbot', 'data-analysis']);
330
331
  vi.mocked(cloneTemplate).mockResolvedValue(undefined);
332
+ vi.mocked(cloneTemplateLocal).mockResolvedValue(undefined);
331
333
  }
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ program
11
11
  .option('--anthropic-key <anthropic-key>', 'Anthropic API key')
12
12
  .option('--custom-project-id <custom-project-id>', 'Custom project id for experienced users who want an empty project directory')
13
13
  .option('--disable-git', 'Disable git initialization')
14
+ .option('--local-agents-prefix <local-agents-prefix>', 'Local prefix for create-agents-template')
15
+ .option('--local-templates-prefix <local-templates-prefix>', 'Local prefix for project templates')
14
16
  .parse();
15
17
  async function main() {
16
18
  const options = program.opts();
@@ -23,6 +25,8 @@ async function main() {
23
25
  customProjectId: options.customProjectId,
24
26
  template: options.template,
25
27
  disableGit: options.disableGit,
28
+ localAgentsPrefix: options.localAgentsPrefix,
29
+ localTemplatesPrefix: options.localTemplatesPrefix,
26
30
  });
27
31
  }
28
32
  catch (error) {
@@ -5,7 +5,7 @@ export interface ContentReplacement {
5
5
  replacements: Record<string, any>;
6
6
  }
7
7
  export declare function cloneTemplate(templatePath: string, targetPath: string, replacements?: ContentReplacement[]): Promise<void>;
8
- export declare function cloneTemplateLocal(templatePath: string, targetPath: string, replacements: ContentReplacement[]): Promise<void>;
8
+ export declare function cloneTemplateLocal(templatePath: string, targetPath: string, replacements?: ContentReplacement[]): Promise<void>;
9
9
  /**
10
10
  * Replace content in cloned template files
11
11
  */
@@ -14,4 +14,4 @@ export declare function replaceContentInFiles(targetPath: string, replacements:
14
14
  * Replace object properties in TypeScript code content
15
15
  */
16
16
  export declare function replaceObjectProperties(content: string, replacements: Record<string, any>): Promise<string>;
17
- export declare function getAvailableTemplates(): Promise<string[]>;
17
+ export declare function getAvailableTemplates(localPrefix?: string): Promise<string[]>;
package/dist/templates.js CHANGED
@@ -253,9 +253,16 @@ function injectPropertyIntoObject(content, propertyPath, replacement) {
253
253
  console.warn(`Could not inject property "${propertyPath}" - no suitable object found in content`);
254
254
  return content;
255
255
  }
256
- export async function getAvailableTemplates() {
257
- // Fetch the list of templates from your repo
258
- const response = await fetch('https://api.github.com/repos/inkeep/agents-cookbook/contents/template-projects');
259
- const contents = await response.json();
260
- return contents.filter((item) => item.type === 'dir').map((item) => item.name);
256
+ export async function getAvailableTemplates(localPrefix) {
257
+ if (localPrefix && localPrefix.length > 0) {
258
+ const fullTemplatePath = path.join(localPrefix, 'template-projects');
259
+ const response = await fs.readdir(fullTemplatePath);
260
+ return response.filter((item) => fs.stat(path.join(fullTemplatePath, item)).then((stat) => stat.isDirectory()));
261
+ }
262
+ else {
263
+ // Fetch the list of templates from your repo
264
+ const response = await fetch(`https://api.github.com/repos/inkeep/agents/contents/agents-cookbook/template-projects`);
265
+ const contents = await response.json();
266
+ return contents.filter((item) => item.type === 'dir').map((item) => item.name);
267
+ }
261
268
  }
package/dist/utils.d.ts CHANGED
@@ -40,5 +40,7 @@ export declare const createAgents: (args?: {
40
40
  template?: string;
41
41
  customProjectId?: string;
42
42
  disableGit?: boolean;
43
+ localAgentsPrefix?: string;
44
+ localTemplatesPrefix?: string;
43
45
  }) => Promise<void>;
44
46
  export declare function createCommand(dirName?: string, options?: any): Promise<void>;
package/dist/utils.js CHANGED
@@ -6,7 +6,7 @@ import * as p from '@clack/prompts';
6
6
  import { ANTHROPIC_MODELS, GOOGLE_MODELS, OPENAI_MODELS } from '@inkeep/agents-core';
7
7
  import fs from 'fs-extra';
8
8
  import color from 'picocolors';
9
- import { cloneTemplate, getAvailableTemplates } from './templates.js';
9
+ import { cloneTemplate, cloneTemplateLocal, getAvailableTemplates, } from './templates.js';
10
10
  // Shared validation utility
11
11
  const DIRECTORY_VALIDATION = {
12
12
  pattern: /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/,
@@ -28,6 +28,8 @@ const DIRECTORY_VALIDATION = {
28
28
  return undefined;
29
29
  },
30
30
  };
31
+ const agentsTemplateRepo = 'https://github.com/inkeep/agents/create-agents-template';
32
+ const projectTemplateRepo = 'https://github.com/inkeep/agents/agents-cookbook/template-projects';
31
33
  const execAsync = promisify(exec);
32
34
  const manageApiPort = '3002';
33
35
  const runApiPort = '3003';
@@ -65,7 +67,7 @@ export const defaultAnthropicModelConfigurations = {
65
67
  },
66
68
  };
67
69
  export const createAgents = async (args = {}) => {
68
- let { dirName, openAiKey, anthropicKey, googleKey, template, customProjectId, disableGit } = args;
70
+ let { dirName, openAiKey, anthropicKey, googleKey, template, customProjectId, disableGit, localAgentsPrefix, localTemplatesPrefix, } = args;
69
71
  const tenantId = 'default';
70
72
  let projectId;
71
73
  let templateName;
@@ -74,7 +76,7 @@ export const createAgents = async (args = {}) => {
74
76
  templateName = '';
75
77
  }
76
78
  else if (template) {
77
- const availableTemplates = await getAvailableTemplates();
79
+ const availableTemplates = await getAvailableTemplates(localTemplatesPrefix);
78
80
  if (!availableTemplates.includes(template)) {
79
81
  p.cancel(`${color.red('✗')} Template "${template}" not found\n\n` +
80
82
  `${color.yellow('Available templates:')}\n` +
@@ -188,10 +190,6 @@ export const createAgents = async (args = {}) => {
188
190
  const s = p.spinner();
189
191
  s.start('Creating directory structure...');
190
192
  try {
191
- const agentsTemplateRepo = 'https://github.com/inkeep/create-agents-template';
192
- const projectTemplateRepo = templateName
193
- ? `https://github.com/inkeep/agents-cookbook/template-projects/${templateName}`
194
- : null;
195
193
  const directoryPath = path.resolve(process.cwd(), dirName);
196
194
  if (await fs.pathExists(directoryPath)) {
197
195
  s.stop();
@@ -206,7 +204,10 @@ export const createAgents = async (args = {}) => {
206
204
  await fs.emptyDir(directoryPath);
207
205
  }
208
206
  s.message('Building template...');
209
- await cloneTemplate(agentsTemplateRepo, directoryPath);
207
+ await cloneTemplateHelper({
208
+ targetPath: directoryPath,
209
+ localPrefix: localAgentsPrefix,
210
+ });
210
211
  process.chdir(directoryPath);
211
212
  const config = {
212
213
  dirName,
@@ -223,7 +224,7 @@ export const createAgents = async (args = {}) => {
223
224
  await createWorkspaceStructure();
224
225
  s.message('Setting up environment files...');
225
226
  await createEnvironmentFiles(config);
226
- if (projectTemplateRepo) {
227
+ if (templateName && templateName.length > 0) {
227
228
  s.message('Creating project template folder...');
228
229
  const templateTargetPath = `src/projects/${projectId}`;
229
230
  const contentReplacements = [
@@ -234,7 +235,12 @@ export const createAgents = async (args = {}) => {
234
235
  },
235
236
  },
236
237
  ];
237
- await cloneTemplate(projectTemplateRepo, templateTargetPath, contentReplacements);
238
+ await cloneTemplateHelper({
239
+ templateName,
240
+ targetPath: templateTargetPath,
241
+ localPrefix: localTemplatesPrefix,
242
+ replacements: contentReplacements,
243
+ });
238
244
  }
239
245
  else {
240
246
  s.message('Creating empty project folder...');
@@ -316,14 +322,18 @@ INKEEP_AGENTS_JWT_SIGNING_SECRET=${jwtSigningSecret}
316
322
  }
317
323
  async function createInkeepConfig(config) {
318
324
  const inkeepConfig = `import { defineConfig } from '@inkeep/agents-cli/config';
319
-
320
- const config = defineConfig({
321
- tenantId: "${config.tenantId}",
322
- agentsManageApiUrl: 'http://localhost:3002',
323
- agentsRunApiUrl: 'http://localhost:3003',
324
- });
325
-
326
- export default config;`;
325
+
326
+ const config = defineConfig({
327
+ tenantId: "${config.tenantId}",
328
+ agentsManageApi: {
329
+ url: 'http://localhost:3002',
330
+ },
331
+ agentsRunApi: {
332
+ url: 'http://localhost:3003',
333
+ },
334
+ });
335
+
336
+ export default config;`;
327
337
  await fs.writeFile(`src/inkeep.config.ts`, inkeepConfig);
328
338
  if (config.customProject) {
329
339
  const customIndexContent = `import { project } from '@inkeep/agents-sdk';
@@ -358,14 +368,16 @@ async function isPortAvailable(port) {
358
368
  const net = await import('node:net');
359
369
  return new Promise((resolve) => {
360
370
  const server = net.createServer();
361
- server.once('error', () => {
362
- resolve(false);
371
+ server.once('error', (err) => {
372
+ // Only treat EADDRINUSE as "port in use", other errors might be transient
373
+ resolve(err.code === 'EADDRINUSE' ? false : true);
363
374
  });
364
375
  server.once('listening', () => {
365
- server.close();
366
- resolve(true);
376
+ server.close(() => {
377
+ resolve(true);
378
+ });
367
379
  });
368
- server.listen(port);
380
+ server.listen(port, 'localhost');
369
381
  });
370
382
  }
371
383
  /**
@@ -399,6 +411,25 @@ async function checkPortsAvailability() {
399
411
  });
400
412
  }
401
413
  }
414
+ /**
415
+ * Wait for a server to be ready by polling a health endpoint
416
+ */
417
+ async function waitForServerReady(url, timeout) {
418
+ const start = Date.now();
419
+ while (Date.now() - start < timeout) {
420
+ try {
421
+ const response = await fetch(url);
422
+ if (response.ok) {
423
+ return;
424
+ }
425
+ }
426
+ catch {
427
+ // Server not ready yet, continue polling
428
+ }
429
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Check every second
430
+ }
431
+ throw new Error(`Server not ready at ${url} after ${timeout}ms`);
432
+ }
402
433
  async function setupProjectInDatabase(config) {
403
434
  // Proactively check if ports are available BEFORE starting servers
404
435
  await checkPortsAvailability();
@@ -411,7 +442,6 @@ async function setupProjectInDatabase(config) {
411
442
  shell: true,
412
443
  windowsHide: true,
413
444
  });
414
- await new Promise((resolve) => setTimeout(resolve, 5000));
415
445
  // Track if port errors occur during startup (as a safety fallback)
416
446
  const portErrors = { runApi: false, manageApi: false };
417
447
  // Regex patterns for detecting port errors in output
@@ -430,8 +460,15 @@ async function setupProjectInDatabase(config) {
430
460
  }
431
461
  };
432
462
  devProcess.stdout.on('data', checkForPortErrors);
433
- // Give servers time to start
434
- await new Promise((resolve) => setTimeout(resolve, 3000));
463
+ // Wait for servers to be ready
464
+ try {
465
+ await waitForServerReady(`http://localhost:${manageApiPort}/health`, 60000);
466
+ await waitForServerReady(`http://localhost:${runApiPort}/health`, 60000);
467
+ }
468
+ catch (error) {
469
+ // If servers don't start, we'll still try push but it will likely fail
470
+ console.warn('Warning: Servers may not be fully ready:', error instanceof Error ? error.message : String(error));
471
+ }
435
472
  // Check if any port errors occurred during startup
436
473
  if (portErrors.runApi || portErrors.manageApi) {
437
474
  displayPortConflictError(portErrors);
@@ -474,6 +511,27 @@ async function setupDatabase() {
474
511
  throw new Error(`Failed to setup database: ${error instanceof Error ? error.message : 'Unknown error'}`);
475
512
  }
476
513
  }
514
+ async function cloneTemplateHelper(options) {
515
+ const { targetPath, templateName, localPrefix, replacements } = options;
516
+ // If local prefix is provided, use it to clone the template. This is useful for local development and testing.
517
+ if (localPrefix && localPrefix.length > 0) {
518
+ if (templateName) {
519
+ const fullTemplatePath = path.join(localPrefix, templateName);
520
+ await cloneTemplateLocal(fullTemplatePath, targetPath, replacements);
521
+ }
522
+ else {
523
+ await cloneTemplateLocal(localPrefix, targetPath, replacements);
524
+ }
525
+ }
526
+ else {
527
+ if (templateName) {
528
+ await cloneTemplate(`${projectTemplateRepo}/${templateName}`, targetPath, replacements);
529
+ }
530
+ else {
531
+ await cloneTemplate(agentsTemplateRepo, targetPath, replacements);
532
+ }
533
+ }
534
+ }
477
535
  export async function createCommand(dirName, options) {
478
536
  await createAgents({
479
537
  dirName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkeep/create-agents",
3
- "version": "0.29.10",
3
+ "version": "0.30.0",
4
4
  "description": "Create an Inkeep Agent Framework project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,15 +31,16 @@
31
31
  "@clack/prompts": "^0.11.0",
32
32
  "commander": "^12.0.0",
33
33
  "degit": "^2.8.4",
34
+ "drizzle-kit": "^0.31.5",
34
35
  "fs-extra": "^11.0.0",
35
36
  "picocolors": "^1.0.0",
36
- "drizzle-kit": "^0.31.5",
37
- "@inkeep/agents-core": "0.29.10"
37
+ "@inkeep/agents-core": "0.30.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/degit": "^2.8.6",
41
41
  "@types/fs-extra": "^11.0.0",
42
42
  "@types/node": "^20.12.0",
43
+ "execa": "^9.6.0",
43
44
  "tsx": "^4.7.0",
44
45
  "typescript": "^5.4.0",
45
46
  "vitest": "^3.2.4"
@@ -62,8 +63,12 @@
62
63
  "lint": "biome lint src",
63
64
  "lint:fix": "biome check --write .",
64
65
  "format": "biome format --write .",
65
- "test": "vitest --run --passWithNoTests",
66
- "test:watch": "vitest",
66
+ "test": "vitest --run src/__tests__ --exclude src/__tests__/e2e/** --passWithNoTests",
67
+ "test:watch": "vitest src/__tests__ --exclude src/__tests__/e2e/**",
68
+ "test:unit": "vitest --run src/__tests__ --exclude src/__tests__/e2e/**",
69
+ "test:e2e": "vitest --run src/__tests__/e2e",
70
+ "test:e2e:watch": "vitest src/__tests__/e2e",
71
+ "test:all": "vitest --run --passWithNoTests",
67
72
  "typecheck": "tsc --noEmit"
68
73
  }
69
74
  }