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
12 changes: 12 additions & 0 deletions .changeset/biological-cyan-vicuna.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@inkeep/agents-cli": patch
"@inkeep/agents-core": patch
"@inkeep/agents-manage-api": patch
"@inkeep/agents-manage-ui": patch
"@inkeep/agents-run-api": patch
"@inkeep/agents-sdk": patch
"@inkeep/create-agents": patch
"@inkeep/ai-sdk-provider": patch
---

Add --local flag to inkeep init to set local profile as default
130 changes: 130 additions & 0 deletions agents-cli/src/__tests__/commands/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ vi.mock('node:fs', async () => {
};
});

// Mock ProfileManager
const mockProfileManager = {
profilesFileExists: vi.fn(),
loadProfiles: vi.fn(),
saveProfiles: vi.fn(),
addProfile: vi.fn(),
setActiveProfile: vi.fn(),
};

vi.mock('../../utils/profiles', async () => {
const actual = await vi.importActual('../../utils/profiles');
return {
...actual,
ProfileManager: vi.fn(() => mockProfileManager),
};
});

describe('Init Command', () => {
let consoleLogSpy: any;
let consoleErrorSpy: any;
Expand Down Expand Up @@ -239,5 +256,118 @@ describe('Init Command', () => {
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('should create local profile when profiles.yaml does not exist', async () => {
const { existsSync, writeFileSync, readdirSync } = await import('node:fs');

vi.mocked(existsSync).mockReturnValue(false);
vi.mocked(readdirSync).mockReturnValue(['package.json'] as any);
vi.mocked(writeFileSync).mockImplementation(() => {});

// Mock ProfileManager - no profiles file exists
mockProfileManager.profilesFileExists.mockReturnValue(false);

// Mock clack prompts
vi.mocked(p.text)
.mockResolvedValueOnce('./inkeep.config.ts') // confirmedPath
.mockResolvedValueOnce('test-tenant-123') // tenantId
.mockResolvedValueOnce('http://localhost:3002') // manageApiUrl
.mockResolvedValueOnce('http://localhost:3003'); // runApiUrl
vi.mocked(p.isCancel).mockReturnValue(false);

await initCommand({ local: true });

expect(mockProfileManager.saveProfiles).toHaveBeenCalledWith({
activeProfile: 'local',
profiles: {
local: {
remote: {
manageApi: 'http://localhost:3002',
manageUi: 'http://localhost:3001',
runApi: 'http://localhost:3003',
},
credential: 'none',
environment: 'development',
},
},
});
});

it('should add local profile to existing profiles.yaml', async () => {
const { existsSync, writeFileSync, readdirSync } = await import('node:fs');

vi.mocked(existsSync).mockReturnValue(false);
vi.mocked(readdirSync).mockReturnValue(['package.json'] as any);
vi.mocked(writeFileSync).mockImplementation(() => {});

// Mock ProfileManager - profiles file exists with cloud profile
mockProfileManager.profilesFileExists.mockReturnValue(true);
mockProfileManager.loadProfiles.mockReturnValue({
activeProfile: 'cloud',
profiles: {
cloud: { remote: 'cloud', credential: 'inkeep-cloud', environment: 'production' },
},
});

// Mock clack prompts
vi.mocked(p.text)
.mockResolvedValueOnce('./inkeep.config.ts') // confirmedPath
.mockResolvedValueOnce('test-tenant') // tenantId
.mockResolvedValueOnce('http://localhost:3002') // manageApiUrl
.mockResolvedValueOnce('http://localhost:3003'); // runApiUrl
vi.mocked(p.isCancel).mockReturnValue(false);

await initCommand({ local: true });

expect(mockProfileManager.addProfile).toHaveBeenCalledWith('local', {
remote: {
manageApi: 'http://localhost:3002',
manageUi: 'http://localhost:3001',
runApi: 'http://localhost:3003',
},
credential: 'none',
environment: 'development',
});
expect(mockProfileManager.setActiveProfile).toHaveBeenCalledWith('local');
});

it('should set existing local profile as active if it already exists', async () => {
const { existsSync, writeFileSync, readdirSync } = await import('node:fs');

vi.mocked(existsSync).mockReturnValue(false);
vi.mocked(readdirSync).mockReturnValue(['package.json'] as any);
vi.mocked(writeFileSync).mockImplementation(() => {});

// Mock ProfileManager - profiles file exists with local profile already
mockProfileManager.profilesFileExists.mockReturnValue(true);
mockProfileManager.loadProfiles.mockReturnValue({
activeProfile: 'cloud',
profiles: {
cloud: { remote: 'cloud', credential: 'inkeep-cloud', environment: 'production' },
local: {
remote: {
manageApi: 'http://localhost:3002',
manageUi: 'http://localhost:3001',
runApi: 'http://localhost:3003',
},
credential: 'none',
environment: 'development',
},
},
});

// Mock clack prompts
vi.mocked(p.text)
.mockResolvedValueOnce('./inkeep.config.ts') // confirmedPath
.mockResolvedValueOnce('test-tenant') // tenantId
.mockResolvedValueOnce('http://localhost:3002') // manageApiUrl
.mockResolvedValueOnce('http://localhost:3003'); // runApiUrl
vi.mocked(p.isCancel).mockReturnValue(false);

await initCommand({ local: true });

expect(mockProfileManager.addProfile).not.toHaveBeenCalled();
expect(mockProfileManager.setActiveProfile).toHaveBeenCalledWith('local');
});
});
});
51 changes: 50 additions & 1 deletion agents-cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { basename, dirname, join, resolve } from 'node:path';
import * as p from '@clack/prompts';
import chalk from 'chalk';
import { checkKeychainAvailability, loadCredentials } from '../utils/credentials';
import { DEFAULT_PROFILES_CONFIG, type Profile, ProfileManager } from '../utils/profiles';
import {
DEFAULT_PROFILES_CONFIG,
type Profile,
ProfileManager,
type ProfilesConfig,
} from '../utils/profiles';
import { loginCommand } from './login';

export interface InitOptions {
Expand Down Expand Up @@ -439,6 +444,50 @@ export default defineConfig({
try {
writeFileSync(configPath, configContent);
console.log(chalk.green('✓'), `Created ${chalk.cyan(configPath)}`);

// Set up local profile
try {
const profileManager = new ProfileManager();
const localProfile: Profile = {
remote: {
manageApi: manageApiUrl as string,
manageUi: 'http://localhost:3001',
runApi: runApiUrl as string,
},
credential: 'none',
environment: 'development',
};

if (profileManager.profilesFileExists()) {
const config = profileManager.loadProfiles();

if (config.profiles.local) {
profileManager.setActiveProfile('local');
console.log(chalk.green('✓'), 'Set local profile as active');
} else {
profileManager.addProfile('local', localProfile);
profileManager.setActiveProfile('local');
console.log(chalk.green('✓'), 'Created and activated local profile');
}
} else {
const profilesConfig: ProfilesConfig = {
activeProfile: 'local',
profiles: {
local: localProfile,
},
};

profileManager.saveProfiles(profilesConfig);
console.log(chalk.green('✓'), 'Created local profile');
}
} catch (profileError) {
console.log(
chalk.yellow('⚠'),
'Could not set up local profile:',
profileError instanceof Error ? profileError.message : String(profileError)
);
}

console.log(chalk.gray('\nYou can now use the Inkeep CLI commands.'));

const configDir = dirname(configPath);
Expand Down
2 changes: 2 additions & 0 deletions agents-cli/src/utils/profiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ export { ProfileError, ProfileManager, profileManager } from './profile-manager'
export {
CLOUD_REMOTE,
DEFAULT_CLOUD_PROFILE,
DEFAULT_LOCAL_PROFILE,
DEFAULT_PROFILES_CONFIG,
type ExplicitRemote,
explicitRemoteSchema,
LOCAL_REMOTE,
type Profile,
type ProfilesConfig,
profileNameSchema,
Expand Down
19 changes: 19 additions & 0 deletions agents-cli/src/utils/profiles/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,22 @@ export const DEFAULT_PROFILES_CONFIG: ProfilesConfig = {
cloud: DEFAULT_CLOUD_PROFILE,
},
};

/**
* Baked-in URLs for local development deployment
*/
export const LOCAL_REMOTE = {
manageApi: 'http://localhost:3002',
manageUi: 'http://localhost:3001',
runApi: 'http://localhost:3003',
} as const;

/**
* Default local profile configuration
* Note: credential is 'none' as local deployments typically don't require auth
*/
export const DEFAULT_LOCAL_PROFILE: Profile = {
remote: LOCAL_REMOTE,
credential: 'none',
environment: 'development',
};
23 changes: 22 additions & 1 deletion agents-docs/content/typescript-sdk/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,39 @@ inkeep init [path]

**Options:**

- `--local` - Initialize for local/self-hosted development (creates a local profile as default)
- `--no-interactive` - Skip interactive path selection
- `--config <path>` - Path to use as template for new configuration

**Cloud vs Local Initialization:**

By default, `inkeep init` runs the cloud onboarding wizard which:
- Opens browser-based authentication
- Fetches your organizations and projects from Inkeep Cloud
- Creates project directories with configuration files
- Sets up the `cloud` profile as the default in `~/.inkeep/profiles.yaml`

Use `--local` for self-hosted or local development:
- Prompts for tenant ID and API URLs (defaults to `localhost:3002` and `localhost:3003`)
- Creates `inkeep.config.ts` with your local configuration
- Sets up a `local` profile as the default in `~/.inkeep/profiles.yaml`
- No authentication required

**Examples:**

```bash
# Interactive initialization
# Cloud initialization (default) - opens browser for auth
inkeep init

# Local/self-hosted initialization - no auth required
inkeep init --local

# Initialize in specific directory
inkeep init ./my-project

# Local init in specific directory
inkeep init --local ./my-project

# Non-interactive mode
inkeep init --no-interactive

Expand Down
17 changes: 13 additions & 4 deletions create-agents-template/scripts/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,17 @@ async function setupProjectInDatabase(skipDocker) {
displayPortConflictError(portErrors);
}

// Step 5: Run inkeep push
logStep(5, 'Running inkeep push command');
// Step 5: Set up local CLI profile
logStep(5, 'Setting up local CLI profile');
try {
await execAsync('pnpm inkeep init --local --no-interactive');
logSuccess('Local CLI profile configured');
} catch (error) {
logWarning('Could not set up local CLI profile - you may need to run: inkeep init --local');
}

// Step 6: Run inkeep push
logStep(6, 'Running inkeep push command');
logInfo(`Pushing project: src/projects/${projectId}`);

let pushSuccess = false;
Expand All @@ -330,8 +339,8 @@ async function setupProjectInDatabase(skipDocker) {
logWarning('The project may not have been pushed to the remote');
pushSuccess = false;
} finally {
// Step 6: Cleanup - Stop development servers
logStep(6, 'Cleaning up - stopping development servers');
// Step 7: Cleanup - Stop development servers
logStep(7, 'Cleaning up - stopping development servers');

if (devProcess.pid) {
try {
Expand Down
Loading