diff --git a/docs/docs/cli.md b/docs/docs/cli.md index 1590c7ee..955fb678 100644 --- a/docs/docs/cli.md +++ b/docs/docs/cli.md @@ -16,8 +16,7 @@ perry agent status ## Workspaces ```bash -perry create [--clone URL] -perry start +perry start [--clone URL] # Start (creates if doesn't exist) perry stop perry delete perry list diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index ea76a781..4b633188 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -98,7 +98,7 @@ perry build 3. **Delete and recreate:** ```bash perry delete - perry create + perry start ``` 4. **Check for port conflicts:** @@ -168,7 +168,7 @@ perry build 4. **Use HTTPS URL instead:** ```bash - perry create myproject --clone https://github.com/user/repo.git + perry start myproject --clone https://github.com/user/repo.git ``` ### Workspace deleted but container still running diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index c605ac1f..bfdf357f 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -117,8 +117,8 @@ perry build # Start agent perry agent run -# Create workspace -perry create myproject --clone git@github.com:user/repo.git +# Start workspace (creates if needed) +perry start myproject --clone git@github.com:user/repo.git # Access via SSH ssh -p 2201 workspace@localhost diff --git a/src/agent/router.ts b/src/agent/router.ts index 8db3c19c..9f2ffa0b 100644 --- a/src/agent/router.ts +++ b/src/agent/router.ts @@ -151,11 +151,20 @@ export function createRouter(ctx: RouterContext) { }); const startWorkspace = os - .input(z.object({ name: z.string() })) + .input( + z.object({ + name: z.string(), + clone: z.string().optional(), + env: z.record(z.string(), z.string()).optional(), + }) + ) .output(WorkspaceInfoSchema) .handler(async ({ input }) => { try { - return await ctx.workspaces.start(input.name); + return await ctx.workspaces.start(input.name, { + clone: input.clone, + env: input.env, + }); } catch (err) { mapErrorToORPC(err, 'Failed to start workspace'); } diff --git a/src/client/api.ts b/src/client/api.ts index 8a3316d9..4f75933c 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -84,9 +84,12 @@ export class ApiClient { } } - async startWorkspace(name: string): Promise { + async startWorkspace( + name: string, + options?: { clone?: string; env?: Record } + ): Promise { try { - return await this.client.workspaces.start({ name }); + return await this.client.workspaces.start({ name, clone: options?.clone, env: options?.env }); } catch (err) { throw this.wrapError(err); } diff --git a/src/index.ts b/src/index.ts index d6217b6e..4cc7ed89 100755 --- a/src/index.ts +++ b/src/index.ts @@ -148,40 +148,20 @@ program } }); -program - .command('create ') - .description('Create a new workspace') - .option('--clone ', 'Git repository URL to clone') - .action(async (name, options) => { - try { - const client = await getClient(); - console.log(`Creating workspace '${name}'...`); - - const workspace = await client.createWorkspace({ - name, - clone: options.clone, - }); - - console.log(`Workspace '${workspace.name}' created.`); - console.log(` Status: ${workspace.status}`); - console.log(` SSH Port: ${workspace.ports.ssh}`); - } catch (err) { - handleError(err); - } - }); - program .command('start ') - .description('Start a stopped workspace') - .action(async (name) => { + .description('Start a workspace (creates it if it does not exist)') + .option('--clone ', 'Git repository URL to clone (when creating)') + .action(async (name, options) => { try { const client = await getClient(); console.log(`Starting workspace '${name}'...`); - const workspace = await client.startWorkspace(name); + const workspace = await client.startWorkspace(name, { clone: options.clone }); console.log(`Workspace '${workspace.name}' started.`); console.log(` Status: ${workspace.status}`); + console.log(` SSH Port: ${workspace.ports.ssh}`); } catch (err) { handleError(err); } diff --git a/src/workspace/manager.ts b/src/workspace/manager.ts index e5cc7f09..f29837b4 100644 --- a/src/workspace/manager.ts +++ b/src/workspace/manager.ts @@ -547,10 +547,13 @@ export class WorkspaceManager { } } - async start(name: string): Promise { + async start( + name: string, + options?: { clone?: string; env?: Record } + ): Promise { const workspace = await this.state.getWorkspace(name); if (!workspace) { - throw new Error(`Workspace '${name}' not found`); + return this.create({ name, clone: options?.clone, env: options?.env }); } const containerName = getContainerName(name); diff --git a/test/integration/agent.test.ts b/test/integration/agent.test.ts index 2f25b179..ab367532 100644 --- a/test/integration/agent.test.ts +++ b/test/integration/agent.test.ts @@ -121,11 +121,13 @@ describe('Agent - Workspace Lifecycle', () => { } }); - it('returns 404 when starting non-existent workspace', async () => { - const result = await agent.api.startWorkspace('nonexistent'); - expect(result.status).toBe(404); - expect((result.data as { code: string }).code).toBe('NOT_FOUND'); - }); + it('creates workspace when starting non-existent workspace', async () => { + const name = `nonexistent-${Date.now()}`; + const result = await agent.api.startWorkspace(name); + expect(result.status).toBe(200); + expect((result.data as { name: string }).name).toBe(name); + await agent.api.deleteWorkspace(name); + }, 60000); it('returns 404 when stopping non-existent workspace', async () => { const result = await agent.api.stopWorkspace('nonexistent'); diff --git a/test/integration/cli.test.ts b/test/integration/cli.test.ts index 226743ed..a21f913d 100644 --- a/test/integration/cli.test.ts +++ b/test/integration/cli.test.ts @@ -42,10 +42,10 @@ describe('CLI commands', () => { }); }); - describe('workspace create', () => { - it('creates a workspace', async () => { + describe('workspace start (create)', () => { + it('creates a workspace when it does not exist', async () => { const name = generateTestWorkspaceName(); - const result = await runCLIExpecting(['create', name], [`Workspace '${name}' created`], { + const result = await runCLIExpecting(['start', name], [`Workspace '${name}' started`], { env: cliEnv(), timeout: 30000, }); @@ -56,25 +56,25 @@ describe('CLI commands', () => { it('creates workspace with --clone option', async () => { const name = generateTestWorkspaceName(); - const result = await runCLI(['create', name, '--clone', 'https://github.com/example/repo'], { + const result = await runCLI(['start', name, '--clone', 'https://github.com/example/repo'], { env: cliEnv(), timeout: 30000, }); expect(result.code).toBe(0); - expect(result.stdout).toContain(`Workspace '${name}' created`); + expect(result.stdout).toContain(`Workspace '${name}' started`); await agent.api.deleteWorkspace(name); }); - it('fails when workspace already exists', async () => { + it('starts existing workspace without error', async () => { const name = generateTestWorkspaceName(); await agent.api.createWorkspace({ name }); - const result = await runCLIExpectingError(['create', name], ['already exists'], { + const result = await runCLIExpecting(['start', name], [`Workspace '${name}' started`], { env: cliEnv(), timeout: 30000, }); - expect(result.code).not.toBe(0); + expect(result.code).toBe(0); await agent.api.deleteWorkspace(name); }); @@ -136,13 +136,6 @@ describe('CLI commands', () => { await agent.api.deleteWorkspace(name); }); - - it('fails to start nonexistent workspace', async () => { - const result = await runCLIExpectingError(['start', 'nonexistent-workspace'], ['not found'], { - env: cliEnv(), - }); - expect(result.code).not.toBe(0); - }); }); describe('workspace delete', () => { @@ -172,11 +165,8 @@ describe('CLI commands', () => { }); it('fails to delete nonexistent workspace', async () => { - const result = await runCLIExpectingError( - ['delete', 'nonexistent-workspace'], - ['not found'], - { env: cliEnv() } - ); + const name = `nonexistent-${Date.now()}-${Math.random().toString(36).slice(2)}`; + const result = await runCLIExpectingError(['delete', name], ['not found'], { env: cliEnv() }); expect(result.code).not.toBe(0); }); });