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
@@ -0,0 +1,53 @@
1
+ import { stripAnsi } from './colors.js';
2
+ // ANSI escape helpers for direct use in the bar (not through picocolors,
3
+ // since we need to layer colors on top of the background)
4
+ const BG_DARK = '\x1b[48;5;236m'; // dark gray background
5
+ const RESET = '\x1b[0m';
6
+ export function createStatusLine(stream) {
7
+ if (!stream.isTTY) {
8
+ return { update() { }, destroy() { } };
9
+ }
10
+ let lastStyled = '';
11
+ let lastPlainLen = 0;
12
+ let rows = stream.rows || 24;
13
+ let cols = stream.columns || 80;
14
+ const originalWrite = stream.write.bind(stream);
15
+ function setupScrollRegion() {
16
+ originalWrite(`\x1b[1;${rows - 1}r`);
17
+ originalWrite(`\x1b[${rows - 1};1H`);
18
+ }
19
+ function renderBar() {
20
+ if (!lastStyled)
21
+ return;
22
+ // Pad with spaces to fill the bar width (using plain length for calculation)
23
+ const padding = lastPlainLen < cols
24
+ ? ' '.repeat(cols - lastPlainLen)
25
+ : '';
26
+ // Render: dark background + styled content + padding + reset
27
+ originalWrite(`\x1b7\x1b[${rows};1H\x1b[2K${BG_DARK}${lastStyled}${padding}${RESET}\x1b8`);
28
+ }
29
+ // Handle terminal resize
30
+ function onResize() {
31
+ rows = stream.rows || 24;
32
+ cols = stream.columns || 80;
33
+ setupScrollRegion();
34
+ renderBar();
35
+ }
36
+ // Activate
37
+ setupScrollRegion();
38
+ stream.on('resize', onResize);
39
+ return {
40
+ update(text) {
41
+ lastStyled = ` ${text} `;
42
+ lastPlainLen = stripAnsi(lastStyled).length;
43
+ renderBar();
44
+ },
45
+ destroy() {
46
+ stream.removeListener('resize', onResize);
47
+ // Clear status bar and reset scroll region to full terminal
48
+ originalWrite(`\x1b7\x1b[${rows};1H\x1b[2K\x1b8`);
49
+ originalWrite(`\x1b[1;${rows}r`);
50
+ lastStyled = '';
51
+ },
52
+ };
53
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { shouldOutputJson, jsonOut, jsonLine } from '../output.js';
3
+ describe('shouldOutputJson', () => {
4
+ it('returns true when json flag is true', () => {
5
+ expect(shouldOutputJson(true)).toBe(true);
6
+ });
7
+ it('returns false when json flag is false (--no-json)', () => {
8
+ expect(shouldOutputJson(false)).toBe(false);
9
+ });
10
+ it('returns false when undefined and stdout is TTY', () => {
11
+ const original = process.stdout.isTTY;
12
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, writable: true });
13
+ expect(shouldOutputJson(undefined)).toBe(false);
14
+ Object.defineProperty(process.stdout, 'isTTY', { value: original, writable: true });
15
+ });
16
+ it('returns true when undefined and stdout is not TTY', () => {
17
+ const original = process.stdout.isTTY;
18
+ Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
19
+ expect(shouldOutputJson(undefined)).toBe(true);
20
+ Object.defineProperty(process.stdout, 'isTTY', { value: original, writable: true });
21
+ });
22
+ });
23
+ describe('jsonOut', () => {
24
+ it('writes JSON to stdout with newline', () => {
25
+ const spy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
26
+ jsonOut({ hello: 'world' });
27
+ expect(spy).toHaveBeenCalledWith('{"hello":"world"}\n');
28
+ spy.mockRestore();
29
+ });
30
+ });
31
+ describe('jsonLine', () => {
32
+ it('writes a single NDJSON line', () => {
33
+ const spy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
34
+ jsonLine({ event: 'test', ts: 123 });
35
+ expect(spy).toHaveBeenCalledWith('{"event":"test","ts":123}\n');
36
+ spy.mockRestore();
37
+ });
38
+ });
@@ -1,103 +1,27 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- const mockQuestion = vi.fn();
3
- const mockClose = vi.fn();
4
- vi.mock('readline', () => ({
5
- createInterface: () => ({
6
- question: mockQuestion,
7
- close: mockClose,
8
- }),
9
- }));
10
- const { promptSelect, promptInput } = await import('../prompt.js');
11
- describe('utils/prompt', () => {
12
- beforeEach(() => {
13
- vi.clearAllMocks();
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { promptSelect } from '../prompt.js';
3
+ describe('promptSelect', () => {
4
+ it('throws when no choices available', async () => {
5
+ await expect(promptSelect('Pick:', [])).rejects.toThrow('No choices available');
14
6
  });
15
- describe('promptSelect()', () => {
16
- it('throws on empty choices array', async () => {
17
- await expect(promptSelect('Pick one', [])).rejects.toThrow('No choices available');
18
- });
19
- it('auto-selects when only one choice', async () => {
20
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
21
- const result = await promptSelect('Pick one', [
22
- { label: 'Only Option', value: 42 },
23
- ]);
24
- expect(result).toEqual({ label: 'Only Option', value: 42 });
25
- expect(logSpy).toHaveBeenCalledWith('Pick one: Only Option');
26
- logSpy.mockRestore();
27
- });
28
- it('auto-selects preserves all properties on the choice object', async () => {
29
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
30
- const choice = { label: 'Test', id: 'abc', extra: true };
31
- const result = await promptSelect('Select', [choice]);
32
- expect(result).toBe(choice);
33
- logSpy.mockRestore();
34
- });
35
- it('returns correct choice by index when multiple choices', async () => {
36
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
37
- // promptSelect calls promptInput internally, which uses readline
38
- // Mock the question callback to simulate user selecting "2"
39
- mockQuestion.mockImplementation((_prompt, cb) => {
40
- cb('2');
41
- });
42
- const choices = [
43
- { label: 'First', id: 1 },
44
- { label: 'Second', id: 2 },
45
- { label: 'Third', id: 3 },
46
- ];
47
- const result = await promptSelect('Choose', choices);
48
- expect(result).toEqual({ label: 'Second', id: 2 });
49
- logSpy.mockRestore();
50
- });
51
- it('exits process on invalid selection', async () => {
52
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
53
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
54
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
55
- throw new Error('process.exit');
56
- }));
57
- mockQuestion.mockImplementation((_prompt, cb) => {
58
- cb('99');
59
- });
60
- const choices = [
61
- { label: 'First', id: 1 },
62
- { label: 'Second', id: 2 },
63
- ];
64
- await expect(promptSelect('Choose', choices)).rejects.toThrow('process.exit');
65
- expect(exitSpy).toHaveBeenCalledWith(1);
66
- logSpy.mockRestore();
67
- errorSpy.mockRestore();
68
- exitSpy.mockRestore();
69
- });
7
+ it('returns single choice without prompting', async () => {
8
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
9
+ const choice = { label: 'Only option', value: 'only' };
10
+ const result = await promptSelect('Pick:', [choice]);
11
+ expect(result).toBe(choice);
12
+ consoleSpy.mockRestore();
70
13
  });
71
- describe('promptInput()', () => {
72
- it('returns user input trimmed', async () => {
73
- mockQuestion.mockImplementation((_prompt, cb) => {
74
- cb(' user input ');
75
- });
76
- const result = await promptInput('Enter value');
77
- expect(result).toBe('user input');
78
- expect(mockClose).toHaveBeenCalled();
79
- });
80
- it('returns default value when input is empty', async () => {
81
- mockQuestion.mockImplementation((_prompt, cb) => {
82
- cb('');
83
- });
84
- const result = await promptInput('Enter value', 'default-val');
85
- expect(result).toBe('default-val');
86
- });
87
- it('includes default value hint in prompt', async () => {
88
- mockQuestion.mockImplementation((_prompt, cb) => {
89
- cb('answer');
90
- });
91
- await promptInput('Name', 'default');
92
- const promptText = mockQuestion.mock.calls[0][0];
93
- expect(promptText).toContain('[default]');
94
- });
95
- it('returns empty string when no input and no default', async () => {
96
- mockQuestion.mockImplementation((_prompt, cb) => {
97
- cb(' ');
98
- });
99
- const result = await promptInput('Enter');
100
- expect(result).toBe('');
101
- });
14
+ it('auto-selects preserves all properties on single choice', async () => {
15
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
16
+ const choice = { label: 'Test', id: 'abc', extra: true };
17
+ const result = await promptSelect('Select', [choice]);
18
+ expect(result).toBe(choice);
19
+ consoleSpy.mockRestore();
20
+ });
21
+ it('logs message and choice label when single choice', async () => {
22
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
23
+ await promptSelect('Pick one', [{ label: 'Only Option', value: 42 }]);
24
+ expect(consoleSpy).toHaveBeenCalledWith('Pick one: Only Option');
25
+ consoleSpy.mockRestore();
102
26
  });
103
27
  });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * JSON output utilities for agent-friendly CLI.
3
+ *
4
+ * When stdout is not a TTY (piped/captured by an agent), output switches
5
+ * to machine-readable JSON automatically. The --json flag forces it.
6
+ */
7
+ /**
8
+ * Determine whether to output JSON based on:
9
+ * 1. Explicit --json flag (true)
10
+ * 2. Explicit --no-json flag (false)
11
+ * 3. Auto-detect: non-TTY stdout means agent/pipe, output JSON
12
+ */
13
+ export declare function shouldOutputJson(jsonFlag: boolean | undefined): boolean;
14
+ /** Write a single JSON object to stdout (for one-shot commands). */
15
+ export declare function jsonOut(data: unknown): void;
16
+ /** Write one NDJSON line to stdout (for streaming commands). */
17
+ export declare function jsonLine(data: unknown): void;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * JSON output utilities for agent-friendly CLI.
3
+ *
4
+ * When stdout is not a TTY (piped/captured by an agent), output switches
5
+ * to machine-readable JSON automatically. The --json flag forces it.
6
+ */
7
+ /**
8
+ * Determine whether to output JSON based on:
9
+ * 1. Explicit --json flag (true)
10
+ * 2. Explicit --no-json flag (false)
11
+ * 3. Auto-detect: non-TTY stdout means agent/pipe, output JSON
12
+ */
13
+ export function shouldOutputJson(jsonFlag) {
14
+ if (jsonFlag === true)
15
+ return true;
16
+ if (jsonFlag === false)
17
+ return false;
18
+ return !process.stdout.isTTY;
19
+ }
20
+ /** Write a single JSON object to stdout (for one-shot commands). */
21
+ export function jsonOut(data) {
22
+ process.stdout.write(JSON.stringify(data) + '\n');
23
+ }
24
+ /** Write one NDJSON line to stdout (for streaming commands). */
25
+ export function jsonLine(data) {
26
+ process.stdout.write(JSON.stringify(data) + '\n');
27
+ }
@@ -2,3 +2,4 @@ export declare function promptSelect<T extends {
2
2
  label: string;
3
3
  }>(message: string, choices: T[]): Promise<T>;
4
4
  export declare function promptInput(message: string, defaultValue?: string): Promise<string>;
5
+ export declare function promptConfirm(message: string): Promise<boolean>;
@@ -1,4 +1,4 @@
1
- import { createInterface } from 'readline';
1
+ import { select, text, confirm, isCancel } from '@clack/prompts';
2
2
  export async function promptSelect(message, choices) {
3
3
  if (choices.length === 0) {
4
4
  throw new Error('No choices available');
@@ -7,28 +7,36 @@ export async function promptSelect(message, choices) {
7
7
  console.log(`${message}: ${choices[0].label}`);
8
8
  return choices[0];
9
9
  }
10
- console.log(message);
11
- for (let i = 0; i < choices.length; i++) {
12
- console.log(` ${i + 1}. ${choices[i].label}`);
13
- }
14
- const answer = await promptInput(`Select (1-${choices.length})`, '1');
15
- const index = parseInt(answer, 10) - 1;
16
- if (isNaN(index) || index < 0 || index >= choices.length) {
17
- console.error('Invalid selection.');
18
- process.exit(1);
10
+ const result = await select({
11
+ message,
12
+ options: choices.map((c, i) => ({
13
+ value: i,
14
+ label: c.label,
15
+ })),
16
+ });
17
+ if (isCancel(result)) {
18
+ console.log('Cancelled.');
19
+ process.exit(0);
19
20
  }
20
- return choices[index];
21
+ return choices[result];
21
22
  }
22
23
  export async function promptInput(message, defaultValue) {
23
- const rl = createInterface({
24
- input: process.stdin,
25
- output: process.stdout,
26
- });
27
- const suffix = defaultValue ? ` [${defaultValue}]` : '';
28
- return new Promise((resolve) => {
29
- rl.question(`${message}${suffix}: `, (answer) => {
30
- rl.close();
31
- resolve(answer.trim() || defaultValue || '');
32
- });
24
+ const result = await text({
25
+ message,
26
+ defaultValue,
27
+ placeholder: defaultValue,
33
28
  });
29
+ if (isCancel(result)) {
30
+ console.log('Cancelled.');
31
+ process.exit(0);
32
+ }
33
+ return result.trim() || defaultValue || '';
34
+ }
35
+ export async function promptConfirm(message) {
36
+ const result = await confirm({ message });
37
+ if (isCancel(result)) {
38
+ console.log('Cancelled.');
39
+ process.exit(0);
40
+ }
41
+ return result;
34
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.2.5",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Runwork <info@runwork.ai> (https://www.runwork.ai)",
@@ -41,10 +41,12 @@
41
41
  "release:full": "bun run release && bun run scripts/release-binaries.ts"
42
42
  },
43
43
  "dependencies": {
44
+ "@clack/prompts": "^1.1.0",
44
45
  "chokidar": "^4.0.0",
45
46
  "commander": "^13.0.0",
46
47
  "fflate": "^0.8.2",
47
- "open": "^10.0.0"
48
+ "open": "^10.0.0",
49
+ "picocolors": "^1.1.1"
48
50
  },
49
51
  "devDependencies": {
50
52
  "typescript": "^5.7.0",