@inkeep/create-agents 0.29.9 → 0.29.11

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,170 @@
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
+ const runApiUrl = 'http://localhost:3003';
7
+ describe('create-agents quickstart e2e', () => {
8
+ let testDir;
9
+ let projectDir;
10
+ const workspaceName = 'test-project';
11
+ const projectId = 'activities-planner';
12
+ beforeEach(async () => {
13
+ // Create a temporary directory for each test
14
+ testDir = await createTempDir();
15
+ projectDir = path.join(testDir, workspaceName);
16
+ });
17
+ afterEach(async () => {
18
+ await cleanupDir(testDir);
19
+ });
20
+ it('should work e2e', async () => {
21
+ // Run the CLI with all options (non-interactive mode)
22
+ console.log('Running CLI with options:');
23
+ console.log(`Working directory: ${testDir}`);
24
+ const result = await runCreateAgentsCLI([
25
+ workspaceName,
26
+ '--openai-key',
27
+ 'test-openai-key',
28
+ '--disable-git', // Skip git init for faster tests
29
+ ], testDir);
30
+ // Verify the CLI completed successfully
31
+ expect(result.exitCode).toBe(0);
32
+ console.log('CLI completed successfully');
33
+ // Verify the core directory structure
34
+ console.log('Verifying directory structure...');
35
+ await verifyDirectoryStructure(projectDir, [
36
+ 'src',
37
+ 'src/inkeep.config.ts',
38
+ `src/projects/${projectId}`,
39
+ 'apps/manage-api',
40
+ 'apps/run-api',
41
+ 'apps/mcp',
42
+ 'apps/manage-ui',
43
+ '.env',
44
+ 'package.json',
45
+ 'drizzle.config.ts',
46
+ ]);
47
+ console.log('Directory structure verified');
48
+ // Verify .env file has required variables
49
+ console.log('Verifying .env file...');
50
+ await verifyFile(path.join(projectDir, '.env'), [
51
+ /ENVIRONMENT=development/,
52
+ /OPENAI_API_KEY=test-openai-key/,
53
+ /DB_FILE_NAME=file:.*\/local\.db/,
54
+ /INKEEP_AGENTS_MANAGE_API_URL="http:\/\/localhost:3002"/,
55
+ /INKEEP_AGENTS_RUN_API_URL="http:\/\/localhost:3003"/,
56
+ /INKEEP_AGENTS_JWT_SIGNING_SECRET=\w+/, // Random secret should be generated
57
+ ]);
58
+ console.log('.env file verified');
59
+ // Verify inkeep.config.ts was created
60
+ console.log('Verifying inkeep.config.ts...');
61
+ await verifyFile(path.join(projectDir, 'src/inkeep.config.ts'));
62
+ console.log('inkeep.config.ts verified');
63
+ console.log('Starting dev servers');
64
+ // Start dev servers in background with output monitoring
65
+ const devProcess = execa('pnpm', ['dev'], {
66
+ cwd: path.join(projectDir, 'apps/manage-api'),
67
+ env: {
68
+ ...process.env,
69
+ FORCE_COLOR: '0',
70
+ NODE_ENV: 'test',
71
+ },
72
+ cleanup: true,
73
+ detached: false,
74
+ stderr: 'pipe',
75
+ });
76
+ // Monitor output for errors and readiness signals
77
+ let serverOutput = '';
78
+ const outputHandler = (data) => {
79
+ const text = data.toString();
80
+ serverOutput += text;
81
+ // Log important messages in CI
82
+ if (process.env.CI) {
83
+ if (text.includes('Error') || text.includes('EADDRINUSE') || text.includes('ready')) {
84
+ console.log('[Server]:', text.trim());
85
+ }
86
+ }
87
+ };
88
+ if (devProcess.stderr)
89
+ devProcess.stderr.on('data', outputHandler);
90
+ // Handle process crashes during startup
91
+ devProcess.catch((error) => {
92
+ console.error('Dev process crashed during startup:', error.message);
93
+ console.error('Server output:', serverOutput);
94
+ });
95
+ console.log('Waiting for servers to be ready');
96
+ try {
97
+ // Wait for servers to be ready with retries
98
+ await waitForServerReady(`${manageApiUrl}/health`, 120000); // Increased to 2 minutes for CI
99
+ console.log('Manage API is ready');
100
+ console.log('Pushing project');
101
+ const pushResult = await runCommand('pnpm', [
102
+ 'inkeep',
103
+ 'push',
104
+ '--project',
105
+ `src/projects/${projectId}`,
106
+ '--config',
107
+ 'src/inkeep.config.ts',
108
+ ], projectDir, 30000);
109
+ expect(pushResult.exitCode).toBe(0);
110
+ console.log('Testing API requests');
111
+ // Test API requests
112
+ const response = await fetch(`${manageApiUrl}/tenants/default/projects/${projectId}`);
113
+ const data = await response.json();
114
+ expect(data.data.tenantId).toBe('default');
115
+ expect(data.data.id).toBe(projectId);
116
+ // Link to local monorepo packages
117
+ const monorepoRoot = path.join(__dirname, '../../../../../'); // Go up to repo root
118
+ await linkLocalPackages(projectDir, monorepoRoot);
119
+ const pushResultLocal = await runCommand('pnpm', [
120
+ 'inkeep',
121
+ 'push',
122
+ '--project',
123
+ `src/projects/${projectId}`,
124
+ '--config',
125
+ 'src/inkeep.config.ts',
126
+ ], projectDir, 30000);
127
+ expect(pushResultLocal.exitCode).toBe(0);
128
+ // Test that the project works with local packages
129
+ const responseLocal = await fetch(`${manageApiUrl}/tenants/default/projects/${projectId}`);
130
+ expect(responseLocal.status).toBe(200);
131
+ }
132
+ catch (error) {
133
+ console.error('Test failed with error:', error);
134
+ // Print server output for debugging
135
+ if (devProcess.stdout) {
136
+ const stdout = await devProcess.stdout;
137
+ console.log('Server stdout:', stdout);
138
+ }
139
+ if (devProcess.stderr) {
140
+ const stderr = await devProcess.stderr;
141
+ console.error('Server stderr:', stderr);
142
+ }
143
+ throw error;
144
+ }
145
+ finally {
146
+ console.log('Killing dev process');
147
+ // Kill the process and wait for it to die
148
+ try {
149
+ devProcess.kill('SIGTERM');
150
+ }
151
+ catch {
152
+ // Might already be dead
153
+ }
154
+ // Give it 2 seconds to shut down gracefully, then force kill
155
+ await new Promise((resolve) => setTimeout(resolve, 2000));
156
+ try {
157
+ devProcess.kill('SIGKILL');
158
+ }
159
+ catch {
160
+ // Already dead or couldn't kill
161
+ }
162
+ // Wait for the process to be fully cleaned up (with timeout)
163
+ await Promise.race([
164
+ devProcess.catch(() => { }), // Wait for process to exit
165
+ new Promise((resolve) => setTimeout(resolve, 5000)), // Or timeout after 5s
166
+ ]);
167
+ console.log('Dev process cleanup complete');
168
+ }
169
+ }, 720000); // 12 minute timeout for full flow with network calls (CI can be slow)
170
+ });
@@ -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
+ }
package/dist/utils.js CHANGED
@@ -316,14 +316,18 @@ INKEEP_AGENTS_JWT_SIGNING_SECRET=${jwtSigningSecret}
316
316
  }
317
317
  async function createInkeepConfig(config) {
318
318
  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;`;
319
+
320
+ const config = defineConfig({
321
+ tenantId: "${config.tenantId}",
322
+ agentsManageApi: {
323
+ url: 'http://localhost:3002',
324
+ },
325
+ agentsRunApi: {
326
+ url: 'http://localhost:3003',
327
+ },
328
+ });
329
+
330
+ export default config;`;
327
331
  await fs.writeFile(`src/inkeep.config.ts`, inkeepConfig);
328
332
  if (config.customProject) {
329
333
  const customIndexContent = `import { project } from '@inkeep/agents-sdk';
@@ -358,14 +362,16 @@ async function isPortAvailable(port) {
358
362
  const net = await import('node:net');
359
363
  return new Promise((resolve) => {
360
364
  const server = net.createServer();
361
- server.once('error', () => {
362
- resolve(false);
365
+ server.once('error', (err) => {
366
+ // Only treat EADDRINUSE as "port in use", other errors might be transient
367
+ resolve(err.code === 'EADDRINUSE' ? false : true);
363
368
  });
364
369
  server.once('listening', () => {
365
- server.close();
366
- resolve(true);
370
+ server.close(() => {
371
+ resolve(true);
372
+ });
367
373
  });
368
- server.listen(port);
374
+ server.listen(port, 'localhost');
369
375
  });
370
376
  }
371
377
  /**
@@ -399,6 +405,25 @@ async function checkPortsAvailability() {
399
405
  });
400
406
  }
401
407
  }
408
+ /**
409
+ * Wait for a server to be ready by polling a health endpoint
410
+ */
411
+ async function waitForServerReady(url, timeout) {
412
+ const start = Date.now();
413
+ while (Date.now() - start < timeout) {
414
+ try {
415
+ const response = await fetch(url);
416
+ if (response.ok) {
417
+ return;
418
+ }
419
+ }
420
+ catch {
421
+ // Server not ready yet, continue polling
422
+ }
423
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Check every second
424
+ }
425
+ throw new Error(`Server not ready at ${url} after ${timeout}ms`);
426
+ }
402
427
  async function setupProjectInDatabase(config) {
403
428
  // Proactively check if ports are available BEFORE starting servers
404
429
  await checkPortsAvailability();
@@ -411,7 +436,6 @@ async function setupProjectInDatabase(config) {
411
436
  shell: true,
412
437
  windowsHide: true,
413
438
  });
414
- await new Promise((resolve) => setTimeout(resolve, 5000));
415
439
  // Track if port errors occur during startup (as a safety fallback)
416
440
  const portErrors = { runApi: false, manageApi: false };
417
441
  // Regex patterns for detecting port errors in output
@@ -430,8 +454,15 @@ async function setupProjectInDatabase(config) {
430
454
  }
431
455
  };
432
456
  devProcess.stdout.on('data', checkForPortErrors);
433
- // Give servers time to start
434
- await new Promise((resolve) => setTimeout(resolve, 3000));
457
+ // Wait for servers to be ready
458
+ try {
459
+ await waitForServerReady(`http://localhost:${manageApiPort}/health`, 60000);
460
+ await waitForServerReady(`http://localhost:${runApiPort}/health`, 60000);
461
+ }
462
+ catch (error) {
463
+ // If servers don't start, we'll still try push but it will likely fail
464
+ console.warn('Warning: Servers may not be fully ready:', error instanceof Error ? error.message : String(error));
465
+ }
435
466
  // Check if any port errors occurred during startup
436
467
  if (portErrors.runApi || portErrors.manageApi) {
437
468
  displayPortConflictError(portErrors);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkeep/create-agents",
3
- "version": "0.29.9",
3
+ "version": "0.29.11",
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.9"
37
+ "@inkeep/agents-core": "0.29.11"
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
  }