runwork 0.2.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/bin/runwork.js +2 -0
  4. package/bundled-types/agents.d.ts +3 -0
  5. package/bundled-types/ai.d.ts +2 -0
  6. package/bundled-types/app-registry.d.ts +74 -0
  7. package/bundled-types/channels.d.ts +2 -0
  8. package/bundled-types/components.d.ts +1 -0
  9. package/bundled-types/core-agent.d.ts +265 -0
  10. package/bundled-types/core-ai.d.ts +170 -0
  11. package/bundled-types/core-base-agent.d.ts +89 -0
  12. package/bundled-types/core-channels.d.ts +42 -0
  13. package/bundled-types/core-components.d.ts +21 -0
  14. package/bundled-types/core-endpoints.d.ts +198 -0
  15. package/bundled-types/core-entities.d.ts +276 -0
  16. package/bundled-types/core-entity-do.d.ts +119 -0
  17. package/bundled-types/core-events.d.ts +52 -0
  18. package/bundled-types/core-file-storage.d.ts +216 -0
  19. package/bundled-types/core-integration-entities.d.ts +147 -0
  20. package/bundled-types/core-integrations.d.ts +143 -0
  21. package/bundled-types/core-routes.d.ts +33 -0
  22. package/bundled-types/core-scheduler.d.ts +180 -0
  23. package/bundled-types/core-utils.d.ts +85 -0
  24. package/bundled-types/core-workflow-cloudflare.d.ts +85 -0
  25. package/bundled-types/core-workflow-config.d.ts +18 -0
  26. package/bundled-types/core-workflow-coordinator.d.ts +78 -0
  27. package/bundled-types/core-workflow-instance.d.ts +82 -0
  28. package/bundled-types/core-workflow-types.d.ts +251 -0
  29. package/bundled-types/core-workflows.d.ts +75 -0
  30. package/bundled-types/core-workspace.d.ts +454 -0
  31. package/bundled-types/create-app.d.ts +57 -0
  32. package/bundled-types/endpoints.d.ts +2 -0
  33. package/bundled-types/entities.d.ts +5 -0
  34. package/bundled-types/events.d.ts +2 -0
  35. package/bundled-types/index.d.ts +19 -0
  36. package/bundled-types/integrations.d.ts +2 -0
  37. package/bundled-types/routes.d.ts +2 -0
  38. package/bundled-types/scheduler.d.ts +2 -0
  39. package/bundled-types/storage.d.ts +2 -0
  40. package/bundled-types/test-entry.d.ts +21 -0
  41. package/bundled-types/types.d.ts +1 -0
  42. package/bundled-types/vite.d.ts +23 -0
  43. package/bundled-types/workflows.d.ts +14 -0
  44. package/bundled-types/workspace.d.ts +2 -0
  45. package/dist/__tests__/types-manager.test.d.ts +1 -0
  46. package/dist/__tests__/types-manager.test.js +85 -0
  47. package/dist/api/__tests__/client.test.d.ts +1 -0
  48. package/dist/api/__tests__/client.test.js +264 -0
  49. package/dist/api/client.d.ts +72 -0
  50. package/dist/api/client.js +144 -0
  51. package/dist/auth/__tests__/store.test.d.ts +1 -0
  52. package/dist/auth/__tests__/store.test.js +103 -0
  53. package/dist/auth/browser.d.ts +2 -0
  54. package/dist/auth/browser.js +7 -0
  55. package/dist/auth/store.d.ts +5 -0
  56. package/dist/auth/store.js +33 -0
  57. package/dist/commands/clone.d.ts +2 -0
  58. package/dist/commands/clone.js +90 -0
  59. package/dist/commands/deploy.d.ts +2 -0
  60. package/dist/commands/deploy.js +43 -0
  61. package/dist/commands/dev.d.ts +2 -0
  62. package/dist/commands/dev.js +188 -0
  63. package/dist/commands/init.d.ts +2 -0
  64. package/dist/commands/init.js +87 -0
  65. package/dist/commands/login.d.ts +2 -0
  66. package/dist/commands/login.js +32 -0
  67. package/dist/commands/logout.d.ts +2 -0
  68. package/dist/commands/logout.js +13 -0
  69. package/dist/commands/logs.d.ts +2 -0
  70. package/dist/commands/logs.js +166 -0
  71. package/dist/generated/bundled-types.d.ts +1 -0
  72. package/dist/generated/bundled-types.js +44 -0
  73. package/dist/generated/version.d.ts +1 -0
  74. package/dist/generated/version.js +2 -0
  75. package/dist/git/__tests__/credentials.test.d.ts +1 -0
  76. package/dist/git/__tests__/credentials.test.js +128 -0
  77. package/dist/git/auto-commit.d.ts +3 -0
  78. package/dist/git/auto-commit.js +209 -0
  79. package/dist/git/credentials.d.ts +15 -0
  80. package/dist/git/credentials.js +65 -0
  81. package/dist/index.d.ts +1 -0
  82. package/dist/index.js +31 -0
  83. package/dist/logs/tailer.d.ts +13 -0
  84. package/dist/logs/tailer.js +135 -0
  85. package/dist/template/manifest.d.ts +17 -0
  86. package/dist/template/manifest.js +102 -0
  87. package/dist/types-manager.d.ts +1 -0
  88. package/dist/types-manager.js +35 -0
  89. package/dist/types.d.ts +27 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils/__tests__/prompt.test.d.ts +1 -0
  92. package/dist/utils/__tests__/prompt.test.js +103 -0
  93. package/dist/utils/fs.d.ts +5 -0
  94. package/dist/utils/fs.js +23 -0
  95. package/dist/utils/prompt.d.ts +4 -0
  96. package/dist/utils/prompt.js +34 -0
  97. package/dist/utils/zip.d.ts +1 -0
  98. package/dist/utils/zip.js +17 -0
  99. package/package.json +66 -0
@@ -0,0 +1,128 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ vi.mock('child_process');
3
+ vi.mock('../../auth/store.js', () => ({
4
+ getCredentials: vi.fn(),
5
+ }));
6
+ const mockExecFileSync = (await import('child_process')).execFileSync;
7
+ const { getCredentials: mockGetCredentials } = await import('../../auth/store.js');
8
+ const { configureGitCredentials, removeGitCredentials, handleGitCredentialRequest, } = await import('../credentials.js');
9
+ describe('git/credentials', () => {
10
+ beforeEach(() => {
11
+ vi.clearAllMocks();
12
+ });
13
+ describe('configureGitCredentials()', () => {
14
+ it('calls git config with correct args', async () => {
15
+ await configureGitCredentials('https://runwork.ai/api/git/ws/app');
16
+ expect(mockExecFileSync).toHaveBeenCalledWith('git', [
17
+ 'config',
18
+ '--global',
19
+ 'credential.https://runwork.ai.helper',
20
+ '!runwork git-credential-helper',
21
+ ]);
22
+ });
23
+ it('extracts origin from full URL', async () => {
24
+ await configureGitCredentials('https://custom.runwork.dev/api/git/ws-1/app-1');
25
+ expect(mockExecFileSync).toHaveBeenCalledWith('git', [
26
+ 'config',
27
+ '--global',
28
+ 'credential.https://custom.runwork.dev.helper',
29
+ '!runwork git-credential-helper',
30
+ ]);
31
+ });
32
+ it('handles URL with port', async () => {
33
+ await configureGitCredentials('https://localhost:8787/api/git/ws/app');
34
+ expect(mockExecFileSync).toHaveBeenCalledWith('git', [
35
+ 'config',
36
+ '--global',
37
+ 'credential.https://localhost:8787.helper',
38
+ '!runwork git-credential-helper',
39
+ ]);
40
+ });
41
+ });
42
+ describe('removeGitCredentials()', () => {
43
+ it('calls git config --unset', async () => {
44
+ await removeGitCredentials('https://runwork.ai');
45
+ expect(mockExecFileSync).toHaveBeenCalledWith('git', [
46
+ 'config',
47
+ '--global',
48
+ '--unset',
49
+ 'credential.https://runwork.ai.helper',
50
+ ]);
51
+ });
52
+ it('ignores errors silently', async () => {
53
+ mockExecFileSync.mockImplementation(() => {
54
+ throw new Error('config key not found');
55
+ });
56
+ // Should not throw
57
+ await expect(removeGitCredentials('https://runwork.ai')).resolves.toBeUndefined();
58
+ });
59
+ });
60
+ describe('handleGitCredentialRequest()', () => {
61
+ it('ignores non-get actions (store)', async () => {
62
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
63
+ await handleGitCredentialRequest('store');
64
+ expect(writeSpy).not.toHaveBeenCalled();
65
+ writeSpy.mockRestore();
66
+ });
67
+ it('ignores non-get actions (erase)', async () => {
68
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
69
+ await handleGitCredentialRequest('erase');
70
+ expect(writeSpy).not.toHaveBeenCalled();
71
+ writeSpy.mockRestore();
72
+ });
73
+ it('exits with code 1 when no credentials stored', async () => {
74
+ vi.mocked(mockGetCredentials).mockReturnValue(null);
75
+ // Mock stdin to provide input then end
76
+ const mockStdin = {
77
+ setEncoding: vi.fn(),
78
+ on: vi.fn((event, cb) => {
79
+ if (event === 'data')
80
+ cb('protocol=https\nhost=runwork.ai\n');
81
+ if (event === 'end')
82
+ cb();
83
+ }),
84
+ readableEnded: false,
85
+ };
86
+ const originalStdin = process.stdin;
87
+ Object.defineProperty(process, 'stdin', { value: mockStdin, writable: true });
88
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
89
+ throw new Error('process.exit');
90
+ }));
91
+ await expect(handleGitCredentialRequest('get')).rejects.toThrow('process.exit');
92
+ expect(exitSpy).toHaveBeenCalledWith(1);
93
+ exitSpy.mockRestore();
94
+ Object.defineProperty(process, 'stdin', { value: originalStdin, writable: true });
95
+ });
96
+ it('outputs correct credential format when credentials exist', async () => {
97
+ vi.mocked(mockGetCredentials).mockReturnValue({
98
+ apiKey: 'my-api-key',
99
+ email: 'test@example.com',
100
+ baseUrl: 'https://runwork.ai',
101
+ });
102
+ const mockStdin = {
103
+ setEncoding: vi.fn(),
104
+ on: vi.fn((event, cb) => {
105
+ if (event === 'data')
106
+ cb('protocol=https\nhost=runwork.ai\n');
107
+ if (event === 'end')
108
+ cb();
109
+ }),
110
+ readableEnded: false,
111
+ };
112
+ const originalStdin = process.stdin;
113
+ Object.defineProperty(process, 'stdin', { value: mockStdin, writable: true });
114
+ const writes = [];
115
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((data) => {
116
+ writes.push(data.toString());
117
+ return true;
118
+ });
119
+ await handleGitCredentialRequest('get');
120
+ expect(writes).toContain('protocol=https\n');
121
+ expect(writes).toContain('host=runwork.ai\n');
122
+ expect(writes).toContain('username=runwork\n');
123
+ expect(writes).toContain('password=my-api-key\n');
124
+ writeSpy.mockRestore();
125
+ Object.defineProperty(process, 'stdin', { value: originalStdin, writable: true });
126
+ });
127
+ });
128
+ });
@@ -0,0 +1,3 @@
1
+ import type { ApiClient } from '../api/client.js';
2
+ export declare function watchAndAutoCommit(directory: string, client: ApiClient, appId: string): Promise<void>;
3
+ export declare function stopAutoCommit(): Promise<void>;
@@ -0,0 +1,209 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { readFileSync } from 'fs';
3
+ import { watch } from 'chokidar';
4
+ import { basename, join, relative } from 'path';
5
+ let watcher = null;
6
+ let fastSyncTimer = null;
7
+ let gitTimer = null;
8
+ let gitPushing = false;
9
+ let gitPendingAfterPush = false;
10
+ // Files awaiting fast sync: path -> 'changed' | 'deleted'
11
+ const pendingFastSync = new Map();
12
+ // Track files the user actually touched during this session (for git)
13
+ const changedFiles = new Set();
14
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.runwork']);
15
+ const SKIP_FILES = new Set(['.dev.vars']);
16
+ const SKIP_EXTENSIONS = new Set(['.log']);
17
+ // Binary extensions to skip in fast sync (git handles them fine)
18
+ const BINARY_EXTENSIONS = new Set([
19
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.avif', '.svg',
20
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
21
+ '.mp3', '.mp4', '.webm', '.ogg', '.wav',
22
+ '.zip', '.tar', '.gz', '.br',
23
+ '.pdf', '.wasm',
24
+ ]);
25
+ function isIgnored(filePath) {
26
+ const name = basename(filePath);
27
+ if (SKIP_DIRS.has(name))
28
+ return true;
29
+ if (SKIP_FILES.has(name))
30
+ return true;
31
+ const ext = name.lastIndexOf('.') >= 0 ? name.slice(name.lastIndexOf('.')) : '';
32
+ if (SKIP_EXTENSIONS.has(ext))
33
+ return true;
34
+ return false;
35
+ }
36
+ export async function watchAndAutoCommit(directory, client, appId) {
37
+ watcher = watch(directory, {
38
+ ignored: isIgnored,
39
+ persistent: true,
40
+ ignoreInitial: true,
41
+ awaitWriteFinish: {
42
+ stabilityThreshold: 300,
43
+ pollInterval: 100,
44
+ },
45
+ });
46
+ const onFileChange = (filePath) => {
47
+ const rel = relative(directory, filePath);
48
+ if (changedFiles.size === 0) {
49
+ console.log(`Changed: ${rel}`);
50
+ }
51
+ else {
52
+ console.log(`Changed: ${rel} (+${changedFiles.size} pending)`);
53
+ }
54
+ changedFiles.add(rel);
55
+ pendingFastSync.set(rel, 'changed');
56
+ scheduleFastSync(directory, client, appId);
57
+ scheduleGitCommit();
58
+ };
59
+ const onFileUnlink = (filePath) => {
60
+ const rel = relative(directory, filePath);
61
+ console.log(`Deleted: ${rel}`);
62
+ changedFiles.add(rel);
63
+ pendingFastSync.set(rel, 'deleted');
64
+ scheduleFastSync(directory, client, appId);
65
+ scheduleGitCommit();
66
+ };
67
+ watcher.on('add', onFileChange);
68
+ watcher.on('change', onFileChange);
69
+ watcher.on('unlink', onFileUnlink);
70
+ }
71
+ // ========================================
72
+ // FAST SYNC PATH (200ms debounce)
73
+ // ========================================
74
+ function scheduleFastSync(directory, client, appId) {
75
+ if (fastSyncTimer)
76
+ clearTimeout(fastSyncTimer);
77
+ fastSyncTimer = setTimeout(() => {
78
+ executeFastSync(directory, client, appId);
79
+ }, 200);
80
+ }
81
+ async function executeFastSync(directory, client, appId) {
82
+ if (pendingFastSync.size === 0)
83
+ return;
84
+ // Snapshot and clear
85
+ const snapshot = new Map(pendingFastSync);
86
+ pendingFastSync.clear();
87
+ const files = [];
88
+ const deletedFiles = [];
89
+ for (const [filePath, action] of snapshot) {
90
+ if (action === 'deleted') {
91
+ deletedFiles.push(filePath);
92
+ }
93
+ else {
94
+ // Skip binary files in fast sync — git path handles them
95
+ const ext = filePath.lastIndexOf('.') >= 0 ? filePath.slice(filePath.lastIndexOf('.')) : '';
96
+ if (BINARY_EXTENSIONS.has(ext.toLowerCase()))
97
+ continue;
98
+ try {
99
+ const contents = readFileSync(join(directory, filePath), 'utf-8');
100
+ files.push({ filePath, fileContents: contents });
101
+ }
102
+ catch {
103
+ // File may have been deleted between detection and read
104
+ }
105
+ }
106
+ }
107
+ if (files.length === 0 && deletedFiles.length === 0)
108
+ return;
109
+ const total = files.length + deletedFiles.length;
110
+ try {
111
+ await client.syncFiles(appId, files, deletedFiles.length > 0 ? deletedFiles : undefined);
112
+ console.log(` Synced ${total} file(s) to preview.`);
113
+ }
114
+ catch (error) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ console.warn(` Fast sync failed (git will handle it): ${message}`);
117
+ }
118
+ }
119
+ // ========================================
120
+ // GIT PATH (2s debounce)
121
+ // ========================================
122
+ function scheduleGitCommit() {
123
+ if (gitPushing) {
124
+ gitPendingAfterPush = true;
125
+ return;
126
+ }
127
+ if (gitTimer)
128
+ clearTimeout(gitTimer);
129
+ gitTimer = setTimeout(() => {
130
+ commitAndPush();
131
+ }, 2000);
132
+ }
133
+ function commitAndPush() {
134
+ try {
135
+ if (changedFiles.size === 0)
136
+ return;
137
+ gitPushing = true;
138
+ // Stage only files the user actually touched + already-tracked modified files
139
+ execFileSync('git', ['add', '-u'], { stdio: 'pipe' });
140
+ // Also stage specific files the user created/modified this session
141
+ const filesToAdd = Array.from(changedFiles);
142
+ changedFiles.clear();
143
+ if (filesToAdd.length > 0) {
144
+ const BATCH_SIZE = 50;
145
+ for (let i = 0; i < filesToAdd.length; i += BATCH_SIZE) {
146
+ const batch = filesToAdd.slice(i, i + BATCH_SIZE);
147
+ try {
148
+ execFileSync('git', ['add', '--', ...batch], { stdio: 'pipe' });
149
+ }
150
+ catch {
151
+ // Some files may have been deleted between detection and staging
152
+ }
153
+ }
154
+ }
155
+ // Check if anything was actually staged
156
+ const staged = execFileSync('git', ['diff', '--cached', '--name-only'], { encoding: 'utf-8' });
157
+ if (!staged.trim())
158
+ return;
159
+ const stagedFiles = staged.trim().split('\n').filter(Boolean);
160
+ // Commit
161
+ const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
162
+ execFileSync('git', ['commit', '-m', `dev: updated ${timestamp}`], { stdio: 'pipe' });
163
+ // Pull --rebase to integrate any remote changes, then push
164
+ try {
165
+ execFileSync('git', ['pull', '--rebase', 'runwork', 'main'], { stdio: 'pipe' });
166
+ }
167
+ catch {
168
+ try {
169
+ execFileSync('git', ['rebase', '--abort'], { stdio: 'pipe' });
170
+ }
171
+ catch { /* no rebase in progress */ }
172
+ console.warn('Auto-sync: rebase failed, skipping push. Run `git pull --rebase runwork main` manually.');
173
+ return;
174
+ }
175
+ try {
176
+ execFileSync('git', ['rev-parse', '--abbrev-ref', '@{u}'], { stdio: 'pipe' });
177
+ execFileSync('git', ['push', 'runwork', 'main'], { stdio: 'pipe' });
178
+ }
179
+ catch {
180
+ execFileSync('git', ['push', '-u', 'runwork', 'main'], { stdio: 'pipe' });
181
+ }
182
+ console.log(` Pushed ${stagedFiles.length} file(s) to git.`);
183
+ }
184
+ catch (error) {
185
+ const message = error instanceof Error ? error.message : String(error);
186
+ console.warn(`Auto-sync failed: ${message}`);
187
+ }
188
+ finally {
189
+ gitPushing = false;
190
+ if (gitPendingAfterPush) {
191
+ gitPendingAfterPush = false;
192
+ scheduleGitCommit();
193
+ }
194
+ }
195
+ }
196
+ export async function stopAutoCommit() {
197
+ if (fastSyncTimer) {
198
+ clearTimeout(fastSyncTimer);
199
+ fastSyncTimer = null;
200
+ }
201
+ if (gitTimer) {
202
+ clearTimeout(gitTimer);
203
+ gitTimer = null;
204
+ }
205
+ if (watcher) {
206
+ await watcher.close();
207
+ watcher = null;
208
+ }
209
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Configure git to use the runwork credential helper for our remote.
3
+ * Called by `runwork login` and `runwork init`.
4
+ */
5
+ export declare function configureGitCredentials(remoteUrl: string): Promise<void>;
6
+ /**
7
+ * Remove the git credential helper configuration for our remote.
8
+ */
9
+ export declare function removeGitCredentials(baseUrl: string): Promise<void>;
10
+ /**
11
+ * Git credential helper protocol handler.
12
+ * Git calls: runwork git-credential-helper get
13
+ * We read stdin for host/protocol, write username/password to stdout.
14
+ */
15
+ export declare function handleGitCredentialRequest(action: string): Promise<void>;
@@ -0,0 +1,65 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { getCredentials } from '../auth/store.js';
3
+ /**
4
+ * Configure git to use the runwork credential helper for our remote.
5
+ * Called by `runwork login` and `runwork init`.
6
+ */
7
+ export async function configureGitCredentials(remoteUrl) {
8
+ const origin = new URL(remoteUrl).origin;
9
+ execFileSync('git', [
10
+ 'config', '--global',
11
+ `credential.${origin}.helper`,
12
+ '!runwork git-credential-helper',
13
+ ]);
14
+ }
15
+ /**
16
+ * Remove the git credential helper configuration for our remote.
17
+ */
18
+ export async function removeGitCredentials(baseUrl) {
19
+ const origin = new URL(baseUrl).origin;
20
+ try {
21
+ execFileSync('git', [
22
+ 'config', '--global', '--unset',
23
+ `credential.${origin}.helper`,
24
+ ]);
25
+ }
26
+ catch {
27
+ // Ignore errors if the config key doesn't exist
28
+ }
29
+ }
30
+ /**
31
+ * Git credential helper protocol handler.
32
+ * Git calls: runwork git-credential-helper get
33
+ * We read stdin for host/protocol, write username/password to stdout.
34
+ */
35
+ export async function handleGitCredentialRequest(action) {
36
+ if (action !== 'get') {
37
+ return; // Only 'get' needs handling. 'store' and 'erase' are no-ops.
38
+ }
39
+ const input = await readStdin();
40
+ const lines = input.trim().split('\n');
41
+ const params = {};
42
+ for (const line of lines) {
43
+ const [key, value] = line.split('=', 2);
44
+ if (key && value)
45
+ params[key] = value;
46
+ }
47
+ const creds = getCredentials();
48
+ if (!creds) {
49
+ process.exit(1);
50
+ }
51
+ process.stdout.write(`protocol=${params.protocol || 'https'}\n`);
52
+ process.stdout.write(`host=${params.host}\n`);
53
+ process.stdout.write(`username=runwork\n`);
54
+ process.stdout.write(`password=${creds.apiKey}\n`);
55
+ }
56
+ function readStdin() {
57
+ return new Promise((resolve) => {
58
+ let data = '';
59
+ process.stdin.setEncoding('utf8');
60
+ process.stdin.on('data', (chunk) => { data += chunk; });
61
+ process.stdin.on('end', () => resolve(data));
62
+ if (process.stdin.readableEnded)
63
+ resolve(data);
64
+ });
65
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ import { Command } from 'commander';
2
+ import { loginCommand } from './commands/login.js';
3
+ import { initCommand } from './commands/init.js';
4
+ import { cloneCommand } from './commands/clone.js';
5
+ import { devCommand } from './commands/dev.js';
6
+ import { deployCommand } from './commands/deploy.js';
7
+ import { logsCommand } from './commands/logs.js';
8
+ import { logoutCommand } from './commands/logout.js';
9
+ import { handleGitCredentialRequest } from './git/credentials.js';
10
+ import { VERSION } from './generated/version.js';
11
+ const program = new Command();
12
+ program
13
+ .name('runwork')
14
+ .description('Runwork CLI - local development for Runwork apps')
15
+ .version(VERSION);
16
+ program.addCommand(loginCommand);
17
+ program.addCommand(initCommand);
18
+ program.addCommand(cloneCommand);
19
+ program.addCommand(devCommand);
20
+ program.addCommand(deployCommand);
21
+ program.addCommand(logsCommand);
22
+ program.addCommand(logoutCommand);
23
+ const credentialHelper = program
24
+ .command('git-credential-helper', { hidden: true })
25
+ .argument('<action>', 'Credential action (get/store/erase)')
26
+ .description('Git credential helper (called by git, not directly)')
27
+ .action(async (action) => {
28
+ await handleGitCredentialRequest(action);
29
+ });
30
+ credentialHelper.helpOption(false);
31
+ program.parse();
@@ -0,0 +1,13 @@
1
+ import type { ApiClient } from '../api/client.js';
2
+ interface TailerOptions {
3
+ appId: string;
4
+ client: ApiClient;
5
+ projectDir: string;
6
+ toTerminal: boolean;
7
+ toFile: boolean;
8
+ intervalMs?: number;
9
+ }
10
+ export declare function startLogTailer(options: TailerOptions): {
11
+ stop: () => void;
12
+ };
13
+ export {};
@@ -0,0 +1,135 @@
1
+ import { appendFileSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ const LOG_FILE = '.runwork/logs.txt';
4
+ function formatTime() {
5
+ const now = new Date();
6
+ return [
7
+ String(now.getHours()).padStart(2, '0'),
8
+ String(now.getMinutes()).padStart(2, '0'),
9
+ String(now.getSeconds()).padStart(2, '0'),
10
+ ].join(':');
11
+ }
12
+ function formatLogLine(tag, line) {
13
+ return `[${formatTime()}] [${tag}] ${line}`;
14
+ }
15
+ export function startLogTailer(options) {
16
+ const { appId, client, projectDir, toTerminal, toFile, intervalMs = 5000, } = options;
17
+ const logFilePath = join(projectDir, LOG_FILE);
18
+ // Ensure .runwork/ directory exists and truncate log file with header
19
+ if (toFile) {
20
+ mkdirSync(dirname(logFilePath), { recursive: true });
21
+ writeFileSync(logFilePath, `# Runwork dev logs - started ${new Date().toISOString()}\n\n`, 'utf-8');
22
+ }
23
+ let lastStdoutLength = 0;
24
+ let lastStderrLength = 0;
25
+ let lastEventTimestamp;
26
+ let seenEventIds = new Set();
27
+ let stopped = false;
28
+ let timer;
29
+ function writeLine(line) {
30
+ if (toTerminal) {
31
+ console.log(line);
32
+ }
33
+ if (toFile) {
34
+ try {
35
+ appendFileSync(logFilePath, line + '\n', 'utf-8');
36
+ }
37
+ catch {
38
+ // Silently ignore file write errors
39
+ }
40
+ }
41
+ }
42
+ async function poll() {
43
+ if (stopped)
44
+ return;
45
+ try {
46
+ // Poll preview logs (stdout/stderr)
47
+ const logs = await client.getPreviewLogs(appId);
48
+ // Reset detection: if buffer shrank (sandbox restart), reset cursor
49
+ if (logs.stdout && logs.stdout.length < lastStdoutLength) {
50
+ lastStdoutLength = 0;
51
+ }
52
+ if (logs.stderr && logs.stderr.length < lastStderrLength) {
53
+ lastStderrLength = 0;
54
+ }
55
+ if (logs.stdout && logs.stdout.length > lastStdoutLength) {
56
+ const newContent = logs.stdout.slice(lastStdoutLength);
57
+ const lines = newContent.split('\n').filter(Boolean);
58
+ for (const line of lines) {
59
+ writeLine(formatLogLine('RUNTIME', line));
60
+ }
61
+ lastStdoutLength = logs.stdout.length;
62
+ }
63
+ if (logs.stderr && logs.stderr.length > lastStderrLength) {
64
+ const newContent = logs.stderr.slice(lastStderrLength);
65
+ const lines = newContent.split('\n').filter(Boolean);
66
+ for (const line of lines) {
67
+ writeLine(formatLogLine('ERROR', line));
68
+ }
69
+ lastStderrLength = logs.stderr.length;
70
+ }
71
+ }
72
+ catch {
73
+ // Silently ignore - sandbox may not be ready yet
74
+ }
75
+ try {
76
+ // Poll app events
77
+ const eventOptions = { limit: 20 };
78
+ // We don't use `before` for pagination here; instead we filter by timestamp
79
+ const result = await client.getAppEvents(appId, eventOptions);
80
+ if (result.events && result.events.length > 0) {
81
+ // Events are returned newest-first; process in chronological order
82
+ const sorted = [...result.events].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
83
+ for (const event of sorted) {
84
+ // Skip events we've already seen (by ID, or by timestamp for older events)
85
+ if (seenEventIds.has(event.id)) {
86
+ continue;
87
+ }
88
+ if (lastEventTimestamp && event.timestamp < lastEventTimestamp) {
89
+ continue;
90
+ }
91
+ let detail = event.summary || event.content || event.type;
92
+ // Include structured metadata when available
93
+ if (event.metadata) {
94
+ const meta = event.metadata;
95
+ if (meta.statusCode) {
96
+ detail += ` (status: ${meta.statusCode})`;
97
+ }
98
+ if (meta.error) {
99
+ detail += ` - ${meta.error}`;
100
+ }
101
+ if (meta.path) {
102
+ detail += ` [${meta.path}]`;
103
+ }
104
+ }
105
+ writeLine(formatLogLine('EVENT', `${event.type}: ${detail}`));
106
+ seenEventIds.add(event.id);
107
+ }
108
+ // Update cursor to the latest event timestamp and prune old IDs
109
+ const latest = sorted[sorted.length - 1];
110
+ if (latest && (!lastEventTimestamp || latest.timestamp > lastEventTimestamp)) {
111
+ // Clear IDs from previous timestamps since we won't see them again
112
+ seenEventIds = new Set(sorted.filter(e => e.timestamp === latest.timestamp).map(e => e.id));
113
+ lastEventTimestamp = latest.timestamp;
114
+ }
115
+ }
116
+ }
117
+ catch {
118
+ // Silently ignore - events endpoint may not be available
119
+ }
120
+ if (!stopped) {
121
+ timer = setTimeout(poll, intervalMs);
122
+ }
123
+ }
124
+ // Initial poll after a short delay to let sandbox start
125
+ timer = setTimeout(poll, 2000);
126
+ return {
127
+ stop() {
128
+ stopped = true;
129
+ if (timer) {
130
+ clearTimeout(timer);
131
+ timer = undefined;
132
+ }
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,17 @@
1
+ export interface TemplateManifest {
2
+ version: 1;
3
+ files: Record<string, string>;
4
+ }
5
+ export declare function generateManifest(dir: string): Promise<TemplateManifest>;
6
+ export declare function saveManifest(dir: string, manifest: TemplateManifest): Promise<void>;
7
+ export declare function loadManifest(dir: string): Promise<TemplateManifest | null>;
8
+ /**
9
+ * Detect files the user edited outside of `runwork dev`.
10
+ *
11
+ * Two sources of edits:
12
+ * 1. Git-tracked files with uncommitted changes (git diff + git diff --cached)
13
+ * 2. Untracked files that aren't in the template manifest (user-created files)
14
+ *
15
+ * Already-committed files are NOT flagged -- git already knows about them.
16
+ */
17
+ export declare function detectUserEdits(dir: string, manifest: TemplateManifest): Promise<string[]>;