runwork 0.2.5 → 0.4.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.
Files changed (56) hide show
  1. package/dist/api/client.d.ts +2 -1
  2. package/dist/api/client.js +4 -0
  3. package/dist/auth/login-flow.d.ts +10 -0
  4. package/dist/auth/login-flow.js +37 -0
  5. package/dist/commands/clone.d.ts +3 -0
  6. package/dist/commands/clone.js +32 -24
  7. package/dist/commands/deploy.js +24 -7
  8. package/dist/commands/dev.d.ts +5 -0
  9. package/dist/commands/dev.js +151 -45
  10. package/dist/commands/info.d.ts +2 -0
  11. package/dist/commands/info.js +432 -0
  12. package/dist/commands/init.d.ts +3 -0
  13. package/dist/commands/init.js +29 -21
  14. package/dist/commands/integrations.js +19 -2
  15. package/dist/commands/login.js +10 -25
  16. package/dist/commands/logs.js +82 -23
  17. package/dist/commands/open.d.ts +2 -0
  18. package/dist/commands/open.js +53 -0
  19. package/dist/commands/welcome.d.ts +1 -0
  20. package/dist/commands/welcome.js +83 -0
  21. package/dist/generated/version.d.ts +1 -1
  22. package/dist/generated/version.js +1 -1
  23. package/dist/git/__tests__/sync.test.js +10 -8
  24. package/dist/git/auto-commit.d.ts +5 -1
  25. package/dist/git/auto-commit.js +15 -9
  26. package/dist/git/sync.js +46 -5
  27. package/dist/index.js +22 -1
  28. package/dist/logs/__tests__/tailer-format.test.d.ts +1 -0
  29. package/dist/logs/__tests__/tailer-format.test.js +43 -0
  30. package/dist/logs/tailer.d.ts +4 -0
  31. package/dist/logs/tailer.js +58 -10
  32. package/dist/types.d.ts +59 -0
  33. package/dist/ui/__tests__/banner.test.d.ts +1 -0
  34. package/dist/ui/__tests__/banner.test.js +82 -0
  35. package/dist/ui/__tests__/colors.test.d.ts +1 -0
  36. package/dist/ui/__tests__/colors.test.js +22 -0
  37. package/dist/ui/__tests__/keyboard.test.d.ts +1 -0
  38. package/dist/ui/__tests__/keyboard.test.js +30 -0
  39. package/dist/ui/__tests__/status-line.test.d.ts +1 -0
  40. package/dist/ui/__tests__/status-line.test.js +54 -0
  41. package/dist/ui/banner.d.ts +29 -0
  42. package/dist/ui/banner.js +118 -0
  43. package/dist/ui/colors.d.ts +4 -0
  44. package/dist/ui/colors.js +7 -0
  45. package/dist/ui/keyboard.d.ts +12 -0
  46. package/dist/ui/keyboard.js +57 -0
  47. package/dist/ui/status-line.d.ts +6 -0
  48. package/dist/ui/status-line.js +53 -0
  49. package/dist/utils/__tests__/output.test.d.ts +1 -0
  50. package/dist/utils/__tests__/output.test.js +38 -0
  51. package/dist/utils/__tests__/prompt.test.js +23 -99
  52. package/dist/utils/output.d.ts +17 -0
  53. package/dist/utils/output.js +27 -0
  54. package/dist/utils/prompt.d.ts +1 -0
  55. package/dist/utils/prompt.js +29 -21
  56. package/package.json +4 -2
@@ -6,7 +6,11 @@ interface TailerOptions {
6
6
  toTerminal: boolean;
7
7
  toFile: boolean;
8
8
  intervalMs?: number;
9
+ getFilter?: () => 'all' | 'events' | 'runtime';
10
+ useJson?: boolean;
9
11
  }
12
+ export declare function formatLogLine(tag: string, line: string): string;
13
+ export declare function formatRelativeTime(eventTimestamp?: string): string;
10
14
  export declare function startLogTailer(options: TailerOptions): {
11
15
  stop: () => void;
12
16
  };
@@ -1,5 +1,7 @@
1
1
  import { appendFileSync, mkdirSync, writeFileSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
+ import { bold, red, yellow, cyan, gray, dim } from '../ui/colors.js';
4
+ import { stripAnsi } from '../ui/colors.js';
3
5
  const LOG_FILE = '.runwork/logs.txt';
4
6
  function formatTime() {
5
7
  const now = new Date();
@@ -9,8 +11,35 @@ function formatTime() {
9
11
  String(now.getSeconds()).padStart(2, '0'),
10
12
  ].join(':');
11
13
  }
12
- function formatLogLine(tag, line) {
13
- return `[${formatTime()}] [${tag}] ${line}`;
14
+ export function formatLogLine(tag, line) {
15
+ const time = gray(formatTime());
16
+ let tagFormatted;
17
+ switch (tag) {
18
+ case 'ERROR':
19
+ tagFormatted = bold(red('ERROR '));
20
+ break;
21
+ case 'EVENT':
22
+ tagFormatted = yellow('EVENT ');
23
+ break;
24
+ case 'RUNTIME':
25
+ tagFormatted = cyan('RUNTIME');
26
+ break;
27
+ default:
28
+ tagFormatted = dim(tag.padEnd(7));
29
+ }
30
+ return ` ${time} ${tagFormatted} ${line}`;
31
+ }
32
+ export function formatRelativeTime(eventTimestamp) {
33
+ if (!eventTimestamp)
34
+ return formatTime();
35
+ const diff = Date.now() - new Date(eventTimestamp).getTime();
36
+ if (diff < 0)
37
+ return 'now';
38
+ if (diff < 60_000)
39
+ return `${Math.floor(diff / 1000)}s ago`;
40
+ if (diff < 3_600_000)
41
+ return `${Math.floor(diff / 60_000)}m ago`;
42
+ return formatTime();
14
43
  }
15
44
  export function startLogTailer(options) {
16
45
  const { appId, client, projectDir, toTerminal, toFile, intervalMs = 5000, } = options;
@@ -25,18 +54,30 @@ export function startLogTailer(options) {
25
54
  let seenEventIds = new Set();
26
55
  let stopped = false;
27
56
  let timer;
28
- function writeLine(line) {
29
- if (toTerminal) {
30
- console.log(line);
31
- }
57
+ function writeLineFiltered(line, tag, jsonData) {
58
+ // Always write to file (stripped of ANSI codes)
32
59
  if (toFile) {
33
60
  try {
34
- appendFileSync(logFilePath, line + '\n', 'utf-8');
61
+ appendFileSync(logFilePath, stripAnsi(line) + '\n', 'utf-8');
35
62
  }
36
63
  catch {
37
64
  // Silently ignore file write errors
38
65
  }
39
66
  }
67
+ if (!toTerminal)
68
+ return;
69
+ // JSON mode: emit NDJSON line
70
+ if (options.useJson && jsonData) {
71
+ process.stdout.write(JSON.stringify(jsonData) + '\n');
72
+ return;
73
+ }
74
+ // Human mode: apply filter and print colored line
75
+ const filter = options.getFilter?.() || 'all';
76
+ if (filter === 'events' && tag !== 'EVENT')
77
+ return;
78
+ if (filter === 'runtime' && tag !== 'RUNTIME' && tag !== 'ERROR')
79
+ return;
80
+ console.log(line);
40
81
  }
41
82
  async function poll() {
42
83
  if (stopped)
@@ -55,7 +96,9 @@ export function startLogTailer(options) {
55
96
  const newContent = logs.stdout.slice(lastStdoutLength);
56
97
  const lines = newContent.split('\n').filter(Boolean);
57
98
  for (const line of lines) {
58
- writeLine(formatLogLine('RUNTIME', line));
99
+ writeLineFiltered(formatLogLine('RUNTIME', line), 'RUNTIME', {
100
+ event: 'log', type: 'runtime', message: line, timestamp: new Date().toISOString(),
101
+ });
59
102
  }
60
103
  lastStdoutLength = logs.stdout.length;
61
104
  }
@@ -63,7 +106,9 @@ export function startLogTailer(options) {
63
106
  const newContent = logs.stderr.slice(lastStderrLength);
64
107
  const lines = newContent.split('\n').filter(Boolean);
65
108
  for (const line of lines) {
66
- writeLine(formatLogLine('ERROR', line));
109
+ writeLineFiltered(formatLogLine('ERROR', line), 'ERROR', {
110
+ event: 'log', type: 'error', message: line, timestamp: new Date().toISOString(),
111
+ });
67
112
  }
68
113
  lastStderrLength = logs.stderr.length;
69
114
  }
@@ -98,7 +143,10 @@ export function startLogTailer(options) {
98
143
  detail += ` [${meta.path}]`;
99
144
  }
100
145
  }
101
- writeLine(formatLogLine('EVENT', `${event.type}: ${detail}`));
146
+ const relTime = gray(formatRelativeTime(event.timestamp));
147
+ writeLineFiltered(` ${relTime} ${yellow('EVENT ')} ${event.type}: ${detail}`, 'EVENT', {
148
+ event: 'log', type: 'event', eventType: event.type, detail, metadata: event.metadata || {}, timestamp: event.timestamp,
149
+ });
102
150
  seenEventIds.add(event.id);
103
151
  }
104
152
  // Cap the set size to prevent unbounded growth (keep last 200 IDs)
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export interface RunworkConfig {
2
2
  workspaceId: string;
3
+ workspaceName?: string;
3
4
  appId: string;
4
5
  appName: string;
5
6
  }
@@ -25,3 +26,61 @@ export interface WorkspaceInfo {
25
26
  id: string;
26
27
  name: string;
27
28
  }
29
+ export interface RegistryEntity {
30
+ entityName: string;
31
+ appId: string;
32
+ appName: string;
33
+ schema?: Record<string, unknown>;
34
+ deploymentMode?: 'preview' | 'production';
35
+ }
36
+ export interface RegistrySchedule {
37
+ name: string;
38
+ appId: string;
39
+ appName: string;
40
+ schedule?: string;
41
+ description?: string;
42
+ deploymentMode?: 'preview' | 'production';
43
+ }
44
+ export interface RegistryWorkflow {
45
+ name: string;
46
+ appId: string;
47
+ appName: string;
48
+ description?: string;
49
+ deploymentMode?: 'preview' | 'production';
50
+ }
51
+ export interface RegistryAgent {
52
+ name: string;
53
+ appId: string;
54
+ appName: string;
55
+ type?: string;
56
+ description?: string;
57
+ deploymentMode?: 'preview' | 'production';
58
+ }
59
+ export interface RegistryEndpoint {
60
+ appId: string;
61
+ appName: string;
62
+ path: string;
63
+ method: string;
64
+ description?: string;
65
+ deploymentMode?: 'preview' | 'production';
66
+ }
67
+ export interface RegistryComponent {
68
+ componentName: string;
69
+ appId: string;
70
+ appName: string;
71
+ deploymentMode?: 'preview' | 'production';
72
+ }
73
+ export interface RegistryFileStorage {
74
+ appId: string;
75
+ appName: string;
76
+ deploymentMode?: 'preview' | 'production';
77
+ }
78
+ export interface WorkspaceAllData {
79
+ entities: RegistryEntity[];
80
+ schedules: RegistrySchedule[];
81
+ workflows: RegistryWorkflow[];
82
+ agents: RegistryAgent[];
83
+ endpoints: RegistryEndpoint[];
84
+ components: RegistryComponent[];
85
+ fileStorages: RegistryFileStorage[];
86
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getAgentCommand, SUPPORTED_AGENTS, getDevBanner, getKeyboardHints, getWelcomeBanner, getInfoPanel } from '../banner.js';
3
+ import { stripAnsi } from '../colors.js';
4
+ describe('getAgentCommand', () => {
5
+ it('returns claude command for a directory', () => {
6
+ const cmd = getAgentCommand('claude-code', '/path/to/app');
7
+ expect(cmd).toContain('claude');
8
+ expect(cmd).toContain('/path/to/app');
9
+ });
10
+ it('returns cursor command', () => {
11
+ expect(getAgentCommand('cursor', '/path')).toContain('cursor');
12
+ });
13
+ it('returns codex command', () => {
14
+ expect(getAgentCommand('codex', '/path')).toContain('codex');
15
+ });
16
+ it('returns null for unknown agent', () => {
17
+ expect(getAgentCommand('unknown-agent', '/path')).toBe(null);
18
+ });
19
+ });
20
+ describe('SUPPORTED_AGENTS', () => {
21
+ it('includes common agents', () => {
22
+ const ids = SUPPORTED_AGENTS.map(a => a.id);
23
+ expect(ids).toContain('claude-code');
24
+ expect(ids).toContain('cursor');
25
+ expect(ids).toContain('codex');
26
+ });
27
+ });
28
+ describe('getDevBanner', () => {
29
+ it('includes preview URL and app name', () => {
30
+ const banner = getDevBanner({ appName: 'test-app', previewUrl: 'https://test.runwork.dev' });
31
+ const plain = stripAnsi(banner);
32
+ expect(plain).toContain('https://test.runwork.dev');
33
+ expect(plain).toContain('test-app');
34
+ });
35
+ });
36
+ describe('getKeyboardHints', () => {
37
+ it('includes shortcut keys', () => {
38
+ const hints = stripAnsi(getKeyboardHints());
39
+ expect(hints).toContain('o');
40
+ expect(hints).toContain('q');
41
+ });
42
+ });
43
+ describe('getInfoPanel', () => {
44
+ it('includes app info and agent commands', () => {
45
+ const panel = getInfoPanel({
46
+ appName: 'my-app',
47
+ previewUrl: 'https://test.dev',
48
+ workspaceName: 'my-ws',
49
+ directory: '/path/to/app',
50
+ });
51
+ const plain = stripAnsi(panel);
52
+ expect(plain).toContain('my-app');
53
+ expect(plain).toContain('https://test.dev');
54
+ expect(plain).toContain('my-ws');
55
+ expect(plain).toContain('/path/to/app');
56
+ expect(plain).toContain('Claude Code');
57
+ expect(plain).toContain('Cursor');
58
+ });
59
+ it('omits workspace when not provided', () => {
60
+ const panel = getInfoPanel({
61
+ appName: 'my-app',
62
+ previewUrl: 'https://test.dev',
63
+ directory: '/path',
64
+ });
65
+ const plain = stripAnsi(panel);
66
+ expect(plain).not.toContain('Workspace');
67
+ });
68
+ });
69
+ describe('getDevBanner', () => {
70
+ it('includes workspace name when provided', () => {
71
+ const banner = getDevBanner({ appName: 'app', previewUrl: 'https://x', workspaceName: 'ws' });
72
+ const plain = stripAnsi(banner);
73
+ expect(plain).toContain('ws');
74
+ });
75
+ });
76
+ describe('getWelcomeBanner', () => {
77
+ it('includes Runwork name and mentions AI agents', () => {
78
+ const text = stripAnsi(getWelcomeBanner());
79
+ expect(text).toContain('Runwork');
80
+ expect(text).toContain('Claude');
81
+ });
82
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { bold, red, green, yellow, cyan, dim, gray, stripAnsi, pc } from '../colors.js';
3
+ describe('colors', () => {
4
+ it('re-exports picocolors functions that produce strings', () => {
5
+ // Verify they're callable and return strings (not just that they exist)
6
+ expect(bold('test')).toEqual(expect.any(String));
7
+ expect(red('test')).toEqual(expect.any(String));
8
+ expect(green('test')).toEqual(expect.any(String));
9
+ expect(yellow('test')).toEqual(expect.any(String));
10
+ expect(cyan('test')).toEqual(expect.any(String));
11
+ expect(dim('test')).toEqual(expect.any(String));
12
+ expect(gray('test')).toEqual(expect.any(String));
13
+ });
14
+ it('exports the full picocolors object', () => {
15
+ expect(typeof pc.bold).toBe('function');
16
+ });
17
+ it('strips ANSI codes', () => {
18
+ expect(stripAnsi('\x1b[1mhello\x1b[22m')).toBe('hello');
19
+ expect(stripAnsi('no codes')).toBe('no codes');
20
+ expect(stripAnsi('\x1b[31m\x1b[1mfail\x1b[22m\x1b[39m')).toBe('fail');
21
+ });
22
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseKeypress } from '../keyboard.js';
3
+ describe('parseKeypress', () => {
4
+ it('detects "o" key', () => {
5
+ expect(parseKeypress(Buffer.from('o'))).toBe('o');
6
+ });
7
+ it('detects uppercase "O" as lowercase', () => {
8
+ expect(parseKeypress(Buffer.from('O'))).toBe('o');
9
+ });
10
+ it('detects "p" key', () => {
11
+ expect(parseKeypress(Buffer.from('p'))).toBe('p');
12
+ });
13
+ it('detects Ctrl+C as quit', () => {
14
+ expect(parseKeypress(Buffer.from([0x03]))).toBe('quit');
15
+ });
16
+ it('detects "q" as quit', () => {
17
+ expect(parseKeypress(Buffer.from('q'))).toBe('quit');
18
+ });
19
+ it('returns null for unknown keys', () => {
20
+ expect(parseKeypress(Buffer.from('x'))).toBe(null);
21
+ });
22
+ it('detects log filter keys (a, e, r)', () => {
23
+ expect(parseKeypress(Buffer.from('a'))).toBe('a');
24
+ expect(parseKeypress(Buffer.from('e'))).toBe('e');
25
+ expect(parseKeypress(Buffer.from('r'))).toBe('r');
26
+ });
27
+ it('detects "i" for info', () => {
28
+ expect(parseKeypress(Buffer.from('i'))).toBe('i');
29
+ });
30
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createStatusLine } from '../status-line.js';
3
+ function createMockStream() {
4
+ const mockWrite = vi.fn().mockReturnValue(true);
5
+ return {
6
+ write: mockWrite,
7
+ isTTY: true,
8
+ rows: 24,
9
+ columns: 80,
10
+ on: vi.fn(),
11
+ removeListener: vi.fn(),
12
+ };
13
+ }
14
+ describe('createStatusLine', () => {
15
+ it('returns a no-op status line when not a TTY', () => {
16
+ const stream = createMockStream();
17
+ stream.isTTY = false;
18
+ const status = createStatusLine(stream);
19
+ status.update('test');
20
+ expect(stream.write).not.toHaveBeenCalled();
21
+ });
22
+ it('sets up scroll region on creation for TTY', () => {
23
+ const stream = createMockStream();
24
+ createStatusLine(stream);
25
+ // Should have written scroll region escape sequence
26
+ const output = stream.write.mock.calls.map((c) => c[0]).join('');
27
+ // Scroll region: \x1b[1;23r (rows-1 = 23)
28
+ expect(output).toContain('\x1b[1;23r');
29
+ });
30
+ it('writes status text with inverse styling', () => {
31
+ const stream = createMockStream();
32
+ const status = createStatusLine(stream);
33
+ status.update('Watching | 3 files synced');
34
+ const output = stream.write.mock.calls.map((c) => c[0]).join('');
35
+ expect(output).toContain('Watching');
36
+ });
37
+ it('listens for resize events', () => {
38
+ const stream = createMockStream();
39
+ createStatusLine(stream);
40
+ expect(stream.on).toHaveBeenCalledWith('resize', expect.any(Function));
41
+ });
42
+ it('cleans up on destroy', () => {
43
+ const stream = createMockStream();
44
+ const status = createStatusLine(stream);
45
+ status.update('test');
46
+ stream.write.mockClear();
47
+ status.destroy();
48
+ expect(stream.write).toHaveBeenCalled();
49
+ expect(stream.removeListener).toHaveBeenCalledWith('resize', expect.any(Function));
50
+ // Should reset scroll region to full terminal
51
+ const output = stream.write.mock.calls.map((c) => c[0]).join('');
52
+ expect(output).toContain('\x1b[1;24r');
53
+ });
54
+ });
@@ -0,0 +1,29 @@
1
+ export interface AgentInfo {
2
+ id: string;
3
+ name: string;
4
+ /** Shell command to open the agent in a directory. {dir} is replaced with the path. */
5
+ command: string;
6
+ }
7
+ export declare const SUPPORTED_AGENTS: AgentInfo[];
8
+ export declare function getAgentCommand(agentId: string, directory: string): string | null;
9
+ export declare function getAgentGuidance(directory: string): string;
10
+ export interface DevBannerOptions {
11
+ appName: string;
12
+ previewUrl: string;
13
+ workspaceName?: string;
14
+ }
15
+ export declare function getDevBanner(options: DevBannerOptions): string;
16
+ export declare function getKeyboardHints(): string;
17
+ /**
18
+ * Interactive wizard that asks which AI agent the user wants to use,
19
+ * then shows specific instructions for opening this app in that agent.
20
+ */
21
+ export declare function runAgentWizard(directory: string): Promise<void>;
22
+ /**
23
+ * Returns full info panel (for 'i' key in dev mode) showing
24
+ * workspace, app, preview URL, and agent guidance.
25
+ */
26
+ export declare function getInfoPanel(options: DevBannerOptions & {
27
+ directory: string;
28
+ }): string;
29
+ export declare function getWelcomeBanner(): string;
@@ -0,0 +1,118 @@
1
+ import { bold, cyan, dim, green } from './colors.js';
2
+ export const SUPPORTED_AGENTS = [
3
+ { id: 'claude-code', name: 'Claude Code', command: 'cd {dir} && claude' },
4
+ { id: 'cursor', name: 'Cursor', command: 'cursor {dir}' },
5
+ { id: 'codex', name: 'OpenAI Codex', command: 'cd {dir} && codex' },
6
+ { id: 'claude-desktop', name: 'Claude Desktop', command: 'Open Claude Desktop and add {dir} as a project folder' },
7
+ { id: 'antigravity', name: 'Antigravity', command: 'cd {dir} && antigravity' },
8
+ ];
9
+ export function getAgentCommand(agentId, directory) {
10
+ const agent = SUPPORTED_AGENTS.find(a => a.id === agentId);
11
+ if (!agent)
12
+ return null;
13
+ return agent.command.replace(/{dir}/g, directory);
14
+ }
15
+ export function getAgentGuidance(directory) {
16
+ const lines = [
17
+ '',
18
+ bold('Open in your AI agent:'),
19
+ '',
20
+ ];
21
+ for (const agent of SUPPORTED_AGENTS) {
22
+ const cmd = agent.command.replace(/{dir}/g, directory);
23
+ lines.push(` ${agent.name.padEnd(18)} ${cyan(cmd)}`);
24
+ }
25
+ return lines.join('\n');
26
+ }
27
+ export function getDevBanner(options) {
28
+ const lines = [
29
+ '',
30
+ bold('Runwork Dev'),
31
+ '',
32
+ ];
33
+ if (options.workspaceName) {
34
+ lines.push(` Workspace: ${dim(options.workspaceName)}`);
35
+ }
36
+ lines.push(` App: ${cyan(options.appName)}`, ` Preview: ${green(options.previewUrl)}`, '', dim(' Changes sync automatically. Logs appear below.'), '');
37
+ return lines.join('\n');
38
+ }
39
+ export function getKeyboardHints() {
40
+ return dim(' Press: ') +
41
+ dim('[') + 'o' + dim(']pen preview ') +
42
+ dim('[') + 'a' + dim(']ll logs ') +
43
+ dim('[') + 'e' + dim(']vents ') +
44
+ dim('[') + 'r' + dim(']untime ') +
45
+ dim('[') + 'q' + dim(']uit');
46
+ }
47
+ /**
48
+ * Interactive wizard that asks which AI agent the user wants to use,
49
+ * then shows specific instructions for opening this app in that agent.
50
+ */
51
+ export async function runAgentWizard(directory) {
52
+ const { select, isCancel } = await import('@clack/prompts');
53
+ console.log('');
54
+ const result = await select({
55
+ message: 'Which AI agent do you use?',
56
+ options: SUPPORTED_AGENTS.map(a => ({
57
+ value: a.id,
58
+ label: a.name,
59
+ })),
60
+ });
61
+ if (isCancel(result)) {
62
+ return;
63
+ }
64
+ const agent = SUPPORTED_AGENTS.find(a => a.id === result);
65
+ if (!agent)
66
+ return;
67
+ const cmd = agent.command.replace(/{dir}/g, directory);
68
+ console.log('');
69
+ console.log(bold(`Open your app in ${agent.name}:`));
70
+ console.log('');
71
+ console.log(` ${cyan(cmd)}`);
72
+ console.log('');
73
+ if (agent.id === 'claude-desktop') {
74
+ console.log(dim(' Claude Desktop works with project folders. Add your app directory'));
75
+ console.log(dim(' as a project and Claude will have full context of your Runwork app.'));
76
+ }
77
+ else {
78
+ console.log(dim(' Run this in a separate terminal while `runwork dev` is running.'));
79
+ console.log(dim(' Your AI agent will have full access to your app code and types.'));
80
+ }
81
+ console.log('');
82
+ }
83
+ /**
84
+ * Returns full info panel (for 'i' key in dev mode) showing
85
+ * workspace, app, preview URL, and agent guidance.
86
+ */
87
+ export function getInfoPanel(options) {
88
+ const lines = [
89
+ '',
90
+ bold('App Info'),
91
+ '',
92
+ ];
93
+ if (options.workspaceName) {
94
+ lines.push(` Workspace: ${dim(options.workspaceName)}`);
95
+ }
96
+ lines.push(` App: ${cyan(options.appName)}`, ` Preview: ${green(options.previewUrl)}`, ` Directory: ${dim(options.directory)}`);
97
+ lines.push('', bold('Open in your AI agent:'), '');
98
+ for (const agent of SUPPORTED_AGENTS) {
99
+ const cmd = agent.command.replace(/{dir}/g, options.directory);
100
+ lines.push(` ${agent.name.padEnd(18)} ${cyan(cmd)}`);
101
+ }
102
+ lines.push('');
103
+ return lines.join('\n');
104
+ }
105
+ export function getWelcomeBanner() {
106
+ return [
107
+ '',
108
+ bold('Welcome to Runwork!'),
109
+ '',
110
+ ' Runwork lets you build full-stack apps with AI agents like',
111
+ ' Claude Code, Cursor, Codex, and more -- running on your machine,',
112
+ ' deployed to the edge.',
113
+ '',
114
+ ' To get started, you need a Runwork account.',
115
+ ' We will open your browser to sign up or log in.',
116
+ '',
117
+ ].join('\n');
118
+ }
@@ -0,0 +1,4 @@
1
+ import pc from 'picocolors';
2
+ export declare const bold: import("picocolors/types").Formatter, dim: import("picocolors/types").Formatter, red: import("picocolors/types").Formatter, green: import("picocolors/types").Formatter, yellow: import("picocolors/types").Formatter, cyan: import("picocolors/types").Formatter, gray: import("picocolors/types").Formatter, italic: import("picocolors/types").Formatter, underline: import("picocolors/types").Formatter, inverse: import("picocolors/types").Formatter;
3
+ export { pc };
4
+ export declare function stripAnsi(text: string): string;
@@ -0,0 +1,7 @@
1
+ import pc from 'picocolors';
2
+ export const { bold, dim, red, green, yellow, cyan, gray, italic, underline, inverse } = pc;
3
+ export { pc };
4
+ // eslint-disable-next-line no-control-regex
5
+ export function stripAnsi(text) {
6
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
7
+ }
@@ -0,0 +1,12 @@
1
+ export type KeyAction = 'o' | 'p' | 'a' | 'e' | 'r' | 'i' | 'quit' | null;
2
+ /** Parse a raw stdin buffer into a named action. */
3
+ export declare function parseKeypress(data: Buffer): KeyAction;
4
+ export interface KeyboardListener {
5
+ start(handler: (action: KeyAction) => void): void;
6
+ stop(): void;
7
+ }
8
+ /**
9
+ * Listens for raw keypresses on stdin.
10
+ * Only works when stdin is a TTY. Returns a no-op listener otherwise.
11
+ */
12
+ export declare function createKeyboardListener(): KeyboardListener;
@@ -0,0 +1,57 @@
1
+ /** Parse a raw stdin buffer into a named action. */
2
+ export function parseKeypress(data) {
3
+ if (data.length === 1) {
4
+ const byte = data[0];
5
+ if (byte === 0x03)
6
+ return 'quit'; // Ctrl+C
7
+ const char = String.fromCharCode(byte).toLowerCase();
8
+ switch (char) {
9
+ case 'o': return 'o';
10
+ case 'p': return 'p';
11
+ case 'a': return 'a';
12
+ case 'e': return 'e';
13
+ case 'r': return 'r';
14
+ case 'i': return 'i';
15
+ case 'q': return 'quit';
16
+ default: return null;
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+ /**
22
+ * Listens for raw keypresses on stdin.
23
+ * Only works when stdin is a TTY. Returns a no-op listener otherwise.
24
+ */
25
+ export function createKeyboardListener() {
26
+ if (!process.stdin.isTTY) {
27
+ return { start() { }, stop() { } };
28
+ }
29
+ let dataHandler = null;
30
+ return {
31
+ start(handler) {
32
+ process.stdin.setRawMode(true);
33
+ process.stdin.resume();
34
+ dataHandler = (data) => {
35
+ const action = parseKeypress(data);
36
+ if (action)
37
+ handler(action);
38
+ };
39
+ process.stdin.on('data', dataHandler);
40
+ },
41
+ stop() {
42
+ if (dataHandler) {
43
+ process.stdin.removeListener('data', dataHandler);
44
+ dataHandler = null;
45
+ }
46
+ try {
47
+ if (process.stdin.isTTY) {
48
+ process.stdin.setRawMode(false);
49
+ process.stdin.pause();
50
+ }
51
+ }
52
+ catch {
53
+ // stdin may already be closed during shutdown
54
+ }
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,6 @@
1
+ export interface StatusLine {
2
+ /** Update with pre-styled text. ANSI colors are preserved in the bar. */
3
+ update(text: string): void;
4
+ destroy(): void;
5
+ }
6
+ export declare function createStatusLine(stream: NodeJS.WriteStream): StatusLine;