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.
- package/dist/api/client.d.ts +2 -1
- package/dist/api/client.js +4 -0
- package/dist/auth/login-flow.d.ts +10 -0
- package/dist/auth/login-flow.js +37 -0
- package/dist/commands/clone.d.ts +3 -0
- package/dist/commands/clone.js +32 -24
- package/dist/commands/deploy.js +24 -7
- package/dist/commands/dev.d.ts +5 -0
- package/dist/commands/dev.js +151 -45
- package/dist/commands/info.d.ts +2 -0
- package/dist/commands/info.js +432 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +29 -21
- package/dist/commands/integrations.js +19 -2
- package/dist/commands/login.js +10 -25
- package/dist/commands/logs.js +82 -23
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +53 -0
- package/dist/commands/welcome.d.ts +1 -0
- package/dist/commands/welcome.js +83 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/git/__tests__/sync.test.js +10 -8
- package/dist/git/auto-commit.d.ts +5 -1
- package/dist/git/auto-commit.js +15 -9
- package/dist/git/sync.js +46 -5
- package/dist/index.js +22 -1
- package/dist/logs/__tests__/tailer-format.test.d.ts +1 -0
- package/dist/logs/__tests__/tailer-format.test.js +43 -0
- package/dist/logs/tailer.d.ts +4 -0
- package/dist/logs/tailer.js +58 -10
- package/dist/types.d.ts +59 -0
- package/dist/ui/__tests__/banner.test.d.ts +1 -0
- package/dist/ui/__tests__/banner.test.js +82 -0
- package/dist/ui/__tests__/colors.test.d.ts +1 -0
- package/dist/ui/__tests__/colors.test.js +22 -0
- package/dist/ui/__tests__/keyboard.test.d.ts +1 -0
- package/dist/ui/__tests__/keyboard.test.js +30 -0
- package/dist/ui/__tests__/status-line.test.d.ts +1 -0
- package/dist/ui/__tests__/status-line.test.js +54 -0
- package/dist/ui/banner.d.ts +29 -0
- package/dist/ui/banner.js +118 -0
- package/dist/ui/colors.d.ts +4 -0
- package/dist/ui/colors.js +7 -0
- package/dist/ui/keyboard.d.ts +12 -0
- package/dist/ui/keyboard.js +57 -0
- package/dist/ui/status-line.d.ts +6 -0
- package/dist/ui/status-line.js +53 -0
- package/dist/utils/__tests__/output.test.d.ts +1 -0
- package/dist/utils/__tests__/output.test.js +38 -0
- package/dist/utils/__tests__/prompt.test.js +23 -99
- package/dist/utils/output.d.ts +17 -0
- package/dist/utils/output.js +27 -0
- package/dist/utils/prompt.d.ts +1 -0
- package/dist/utils/prompt.js +29 -21
- 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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
}
|
package/dist/utils/prompt.d.ts
CHANGED
|
@@ -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>;
|
package/dist/utils/prompt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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[
|
|
21
|
+
return choices[result];
|
|
21
22
|
}
|
|
22
23
|
export async function promptInput(message, defaultValue) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
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",
|