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
@@ -1,4 +1,4 @@
1
- import type { Credentials, AppInfo, WorkspaceInfo, DevSession } from '../types.js';
1
+ import type { Credentials, AppInfo, WorkspaceInfo, DevSession, WorkspaceAllData } from '../types.js';
2
2
  export declare class ApiClient {
3
3
  private baseUrl;
4
4
  private apiKey;
@@ -90,5 +90,6 @@ export declare class ApiClient {
90
90
  * Both canonical ({baseUrl}/api/git/{workspaceId}/{appId}) and
91
91
  * subdomain ({slug}.runwork.ai/api/git/{appId}) formats are supported.
92
92
  */
93
+ getWorkspaceAll(workspaceId: string): Promise<WorkspaceAllData>;
93
94
  getGitRemoteUrl(workspaceId: string, appId: string): string;
94
95
  }
@@ -147,6 +147,10 @@ export class ApiClient {
147
147
  * Both canonical ({baseUrl}/api/git/{workspaceId}/{appId}) and
148
148
  * subdomain ({slug}.runwork.ai/api/git/{appId}) formats are supported.
149
149
  */
150
+ async getWorkspaceAll(workspaceId) {
151
+ const res = await this.request(`/api/workspaces/${workspaceId}/all`);
152
+ return res.data;
153
+ }
150
154
  getGitRemoteUrl(workspaceId, appId) {
151
155
  return `${this.baseUrl}/api/git/${workspaceId}/${appId}`;
152
156
  }
@@ -0,0 +1,10 @@
1
+ import type { Credentials } from '../types.js';
2
+ /**
3
+ * Performs the full browser-based login flow:
4
+ * 1. Initiates login, opens browser
5
+ * 2. Polls for completion
6
+ * 3. Saves credentials and configures git
7
+ *
8
+ * Returns the credentials on success, exits process on timeout.
9
+ */
10
+ export declare function performLogin(baseUrl?: string): Promise<Credentials>;
@@ -0,0 +1,37 @@
1
+ import { ApiClient } from '../api/client.js';
2
+ import { saveCredentials } from './store.js';
3
+ import { configureGitCredentials } from '../git/credentials.js';
4
+ const DEFAULT_BASE_URL = 'https://runwork.ai';
5
+ /**
6
+ * Performs the full browser-based login flow:
7
+ * 1. Initiates login, opens browser
8
+ * 2. Polls for completion
9
+ * 3. Saves credentials and configures git
10
+ *
11
+ * Returns the credentials on success, exits process on timeout.
12
+ */
13
+ export async function performLogin(baseUrl) {
14
+ const url = baseUrl || DEFAULT_BASE_URL;
15
+ const client = new ApiClient({ apiKey: '', email: '', baseUrl: url });
16
+ const { sessionId, loginUrl } = await client.initiateLogin();
17
+ // Open browser
18
+ const open = await import('open');
19
+ await open.default(loginUrl);
20
+ console.log(`If browser didn't open, visit: ${loginUrl}`);
21
+ console.log('');
22
+ console.log('Waiting for authentication...');
23
+ const maxAttempts = 60;
24
+ const pollInterval = 5000;
25
+ for (let i = 0; i < maxAttempts; i++) {
26
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
27
+ const result = await client.pollLogin(sessionId);
28
+ if (result) {
29
+ saveCredentials(result);
30
+ await configureGitCredentials(result.baseUrl || url);
31
+ console.log(`Logged in as ${result.email}`);
32
+ return result;
33
+ }
34
+ }
35
+ console.error('Login timed out. Please try again.');
36
+ process.exit(1);
37
+ }
@@ -1,2 +1,5 @@
1
1
  import { Command } from 'commander';
2
+ import { ApiClient } from '../api/client.js';
3
+ import type { AppInfo } from '../types.js';
4
+ export declare function execClone(client: ApiClient, app: AppInfo, directory?: string): Promise<string>;
2
5
  export declare const cloneCommand: Command;
@@ -1,37 +1,18 @@
1
1
  import { Command } from 'commander';
2
2
  import { execFileSync } from 'child_process';
3
3
  import { writeFileSync, mkdirSync, existsSync } from 'fs';
4
- import { join } from 'path';
4
+ import { join, resolve } from 'path';
5
5
  import { requireAuth } from '../auth/store.js';
6
6
  import { ApiClient } from '../api/client.js';
7
7
  import { promptSelect } from '../utils/prompt.js';
8
8
  import { generateManifest, saveManifest } from '../template/manifest.js';
9
9
  import { extractZip } from '../utils/zip.js';
10
10
  import { removeNestedGitDirs } from '../utils/fs.js';
11
- export const cloneCommand = new Command('clone')
12
- .description('Clone a Runwork app to local development')
13
- .argument('[appId]', 'App ID to clone (interactive if omitted)')
14
- .argument('[directory]', 'Target directory')
15
- .action(async (appId, directory) => {
16
- const creds = requireAuth();
17
- const client = new ApiClient(creds);
18
- let app;
19
- if (appId) {
20
- app = await client.getApp(appId);
21
- }
22
- else {
23
- const apps = await client.listApps();
24
- if (apps.length === 0) {
25
- console.error('No apps found.');
26
- process.exit(1);
27
- }
28
- const choice = await promptSelect('Select app to clone:', apps.map(a => ({ label: `${a.name} (${a.workspaceName})`, value: a })));
29
- app = choice.value;
30
- }
11
+ import { runAgentWizard } from '../ui/banner.js';
12
+ export async function execClone(client, app, directory) {
31
13
  const slug = app.slug || app.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
32
14
  const dir = directory || slug;
33
15
  const remoteUrl = client.getGitRemoteUrl(app.workspaceId, app.id);
34
- console.log(`Cloning "${app.name}"...`);
35
16
  // Step 1: Extract skeleton template as the base layer
36
17
  console.log('Downloading project template...');
37
18
  mkdirSync(dir, { recursive: true });
@@ -70,6 +51,7 @@ export const cloneCommand = new Command('clone')
70
51
  // Write .runwork.json config
71
52
  const config = {
72
53
  workspaceId: app.workspaceId,
54
+ workspaceName: app.workspaceName,
73
55
  appId: app.id,
74
56
  appName: app.name,
75
57
  };
@@ -85,6 +67,32 @@ export const cloneCommand = new Command('clone')
85
67
  // App may be new with no registries yet
86
68
  }
87
69
  console.log(`\nApp "${app.name}" cloned to ${dir}/`);
88
- console.log(`Remote: ${remoteUrl}`);
89
- console.log(`\nNext: cd ${dir} && runwork dev`);
70
+ console.log(`Remote: ${client.getGitRemoteUrl(app.workspaceId, app.id)}`);
71
+ return resolve(dir);
72
+ }
73
+ export const cloneCommand = new Command('clone')
74
+ .description('Clone a Runwork app to local development')
75
+ .argument('[appId]', 'App ID to clone (interactive if omitted)')
76
+ .argument('[directory]', 'Target directory')
77
+ .action(async (appId, directory) => {
78
+ const creds = requireAuth();
79
+ const client = new ApiClient(creds);
80
+ let app;
81
+ if (appId) {
82
+ app = await client.getApp(appId);
83
+ }
84
+ else {
85
+ const apps = await client.listApps();
86
+ if (apps.length === 0) {
87
+ console.error('No apps found.');
88
+ process.exit(1);
89
+ }
90
+ const choice = await promptSelect('Select app to clone:', apps.map(a => ({ label: `${a.name} (${a.workspaceName})`, value: a })));
91
+ app = choice.value;
92
+ }
93
+ console.log(`Cloning "${app.name}"...`);
94
+ const dir = await execClone(client, app, directory);
95
+ const slug = dir.split('/').pop() || dir;
96
+ await runAgentWizard(dir);
97
+ console.log(`Next: cd ${slug} && runwork dev`);
90
98
  });
@@ -3,10 +3,16 @@ import { execFileSync } from 'child_process';
3
3
  import { readFileSync, existsSync } from 'fs';
4
4
  import { requireAuth } from '../auth/store.js';
5
5
  import { ApiClient } from '../api/client.js';
6
+ import { shouldOutputJson, jsonOut } from '../utils/output.js';
6
7
  export const deployCommand = new Command('deploy')
7
- .description('Deploy the current app to Runwork')
8
- .action(async () => {
8
+ .description('Deploy the current app to production')
9
+ .action(async (_opts, command) => {
10
+ const useJson = shouldOutputJson(command.optsWithGlobals().json);
9
11
  if (!existsSync('.runwork.json')) {
12
+ if (useJson) {
13
+ process.stderr.write(JSON.stringify({ error: 'No .runwork.json found. Run `runwork init` first.' }) + '\n');
14
+ process.exit(1);
15
+ }
10
16
  console.error('No .runwork.json found. Run `runwork init` first.');
11
17
  process.exit(1);
12
18
  }
@@ -23,21 +29,32 @@ export const deployCommand = new Command('deploy')
23
29
  // No commits yet
24
30
  }
25
31
  if (repoHasCommits) {
26
- console.log('Syncing...');
32
+ if (!useJson)
33
+ console.log('Syncing...');
27
34
  try {
28
- execFileSync('git', ['pull', '--rebase', 'runwork', 'main'], { stdio: 'inherit' });
35
+ execFileSync('git', ['pull', '--rebase', 'runwork', 'main'], { stdio: useJson ? 'pipe' : 'inherit' });
29
36
  }
30
37
  catch {
31
- console.warn('Pull failed. Continuing...');
38
+ if (!useJson)
39
+ console.warn('Pull failed. Continuing...');
32
40
  }
33
- execFileSync('git', ['push', 'runwork', 'main'], { stdio: 'inherit' });
41
+ execFileSync('git', ['push', 'runwork', 'main'], { stdio: useJson ? 'pipe' : 'inherit' });
34
42
  }
35
43
  else {
44
+ if (useJson) {
45
+ process.stderr.write(JSON.stringify({ error: 'No commits to deploy. Commit your changes first.' }) + '\n');
46
+ process.exit(1);
47
+ }
36
48
  console.error('No commits to deploy. Commit your changes first.');
37
49
  process.exit(1);
38
50
  }
39
51
  // Trigger deploy
40
- console.log('Deploying...');
52
+ if (!useJson)
53
+ console.log('Deploying...');
41
54
  const result = await client.triggerDeploy(config.appId);
55
+ if (useJson) {
56
+ jsonOut({ deployed: true, url: result.deploymentUrl });
57
+ return;
58
+ }
42
59
  console.log(`Deployed: ${result.deploymentUrl}`);
43
60
  });
@@ -1,2 +1,7 @@
1
1
  import { Command } from 'commander';
2
+ export declare function execDev(options?: {
3
+ logs?: boolean;
4
+ logsOnlyFile?: boolean;
5
+ json?: boolean;
6
+ }): Promise<void>;
2
7
  export declare const devCommand: Command;
@@ -11,6 +11,11 @@ import { populateTypes } from '../types-manager.js';
11
11
  import { loadManifest, generateManifest, saveManifest, detectUserEdits } from '../template/manifest.js';
12
12
  import { extractZip } from '../utils/zip.js';
13
13
  import { removeNestedGitDirs } from '../utils/fs.js';
14
+ import { getDevBanner, getKeyboardHints, getInfoPanel } from '../ui/banner.js';
15
+ import { createStatusLine } from '../ui/status-line.js';
16
+ import { createKeyboardListener } from '../ui/keyboard.js';
17
+ import { bold, dim, green, yellow, cyan } from '../ui/colors.js';
18
+ import { shouldOutputJson, jsonLine } from '../utils/output.js';
14
19
  async function populateSkill(projectDir, client, appId) {
15
20
  try {
16
21
  const { skill } = await client.getAppSkill(appId);
@@ -29,23 +34,26 @@ function readConfig() {
29
34
  }
30
35
  return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
31
36
  }
32
- export const devCommand = new Command('dev')
33
- .description('Start local development server with live sync')
34
- .option('--no-logs', 'Disable automatic log tailing')
35
- .option('--logs-only-file', 'Write logs to file only, not terminal')
36
- .action(async (options) => {
37
+ export async function execDev(options) {
38
+ const useJson = options?.json ?? false;
37
39
  const config = readConfig();
38
40
  const creds = requireAuth();
39
41
  const client = new ApiClient(creds);
40
42
  const cwd = process.cwd();
43
+ const ts = () => new Date().toISOString();
41
44
  // Detect user edits made outside of `runwork dev`
42
45
  const oldManifest = await loadManifest(cwd);
43
46
  if (oldManifest) {
44
47
  const userEdits = await detectUserEdits(cwd, oldManifest);
45
48
  if (userEdits.length > 0) {
46
- console.log(`Detected ${userEdits.length} file(s) edited outside dev session:`);
47
- for (const file of userEdits) {
48
- console.log(` ${file}`);
49
+ if (useJson) {
50
+ jsonLine({ event: 'startup', phase: 'user_edits_detected', files: userEdits, timestamp: ts() });
51
+ }
52
+ else {
53
+ console.log(`Detected ${userEdits.length} file(s) edited outside dev session:`);
54
+ for (const file of userEdits) {
55
+ console.log(` ${file}`);
56
+ }
49
57
  }
50
58
  try {
51
59
  execFileSync('git', ['add', '--', ...userEdits], { stdio: 'pipe' });
@@ -57,15 +65,18 @@ export const devCommand = new Command('dev')
57
65
  }
58
66
  }
59
67
  // Download fresh template to ensure latest version
60
- console.log('Updating template...');
68
+ if (useJson) {
69
+ jsonLine({ event: 'startup', phase: 'template_update', timestamp: ts() });
70
+ }
71
+ else {
72
+ console.log(dim('Updating template...'));
73
+ }
61
74
  try {
62
75
  const zipData = await client.downloadSkeleton();
63
76
  extractZip(zipData, cwd);
64
77
  removeNestedGitDirs(cwd);
65
- // Generate new manifest from fresh template (before restoring user files)
66
78
  const newManifest = await generateManifest(cwd);
67
79
  await saveManifest(cwd, newManifest);
68
- // Restore user-edited files from git on top of fresh template
69
80
  try {
70
81
  execFileSync('git', ['checkout', '--', '.'], { cwd, stdio: 'pipe' });
71
82
  }
@@ -74,65 +85,160 @@ export const devCommand = new Command('dev')
74
85
  }
75
86
  }
76
87
  catch {
77
- console.warn('Template update failed. Continuing with current files.');
88
+ if (!useJson)
89
+ console.warn(yellow('Template update failed. Continuing with current files.'));
78
90
  }
79
91
  // Populate type definitions
80
92
  await populateTypes(cwd);
81
- // Start dev session first - this triggers DO onStart which may create remote commits
82
- console.log('Starting dev session...');
93
+ if (useJson)
94
+ jsonLine({ event: 'startup', phase: 'types_populated', timestamp: ts() });
95
+ // Start dev session
96
+ if (useJson) {
97
+ jsonLine({ event: 'startup', phase: 'dev_session', timestamp: ts() });
98
+ }
99
+ else {
100
+ console.log(dim('Starting dev session...'));
101
+ }
83
102
  const session = await client.startDevSession(config.appId);
84
- console.log(`Preview: ${session.previewUrl}`);
85
- // Fetch SKILL.md (best-effort, after session so DO registries are available)
103
+ // Fetch SKILL.md (best-effort)
86
104
  await populateSkill(cwd, client, config.appId);
87
- // Sync AFTER starting session so we pick up any commits the DO created
88
- console.log('Syncing with Runwork...');
105
+ if (useJson)
106
+ jsonLine({ event: 'startup', phase: 'skill_fetched', timestamp: ts() });
107
+ // Sync
108
+ if (!useJson)
109
+ console.log(dim('Syncing...'));
89
110
  const syncResult = syncWithRemote(cwd);
90
- switch (syncResult.status) {
91
- case 'skipped':
92
- console.log('No commits yet. Skipping sync.');
93
- break;
94
- case 'synced':
95
- console.log('Synced with Runwork.');
96
- break;
97
- case 'merged':
98
- console.log('Merged with Runwork (histories diverged).');
99
- break;
100
- case 'sync-failed':
101
- if (syncResult.error === 'stash-conflict') {
102
- console.error('Merge conflicts detected after stash pop. Resolve conflicts and restart.');
103
- process.exit(1);
104
- }
105
- console.warn('Pull failed (remote may not have commits yet). Continuing...');
106
- break;
111
+ if (useJson) {
112
+ jsonLine({ event: 'startup', phase: 'sync', status: syncResult.status, pushed: syncResult.pushed, timestamp: ts() });
107
113
  }
108
- if (!syncResult.pushed && syncResult.status !== 'skipped' && syncResult.status !== 'sync-failed') {
109
- console.warn('Push failed. Continuing with current state...');
114
+ else {
115
+ switch (syncResult.status) {
116
+ case 'skipped':
117
+ console.log(dim(' No commits yet. Skipping sync.'));
118
+ break;
119
+ case 'synced':
120
+ console.log(green(' Synced with Runwork.'));
121
+ break;
122
+ case 'merged':
123
+ console.log(yellow(' Merged with Runwork (histories diverged).'));
124
+ break;
125
+ case 'sync-failed':
126
+ if (syncResult.error === 'stash-conflict') {
127
+ console.error('Merge conflicts detected after stash pop. Resolve conflicts and restart.');
128
+ process.exit(1);
129
+ }
130
+ console.warn(yellow(`Sync failed. Continuing...`));
131
+ if (syncResult.error) {
132
+ console.warn(dim(` ${syncResult.error}`));
133
+ }
134
+ break;
135
+ }
136
+ if (!syncResult.pushed && syncResult.status !== 'skipped' && syncResult.status !== 'sync-failed') {
137
+ console.warn(yellow('Push failed. Continuing with current state...'));
138
+ }
110
139
  }
111
- // Declare logTailer before cleanup so cleanup can reference it
140
+ // Emit session_started (JSON) or show banner (human)
141
+ if (useJson) {
142
+ jsonLine({ event: 'session_started', previewUrl: session.previewUrl, appName: config.appName, timestamp: ts() });
143
+ }
144
+ else {
145
+ console.log(getDevBanner({ appName: config.appName, previewUrl: session.previewUrl, workspaceName: config.workspaceName }));
146
+ console.log(getKeyboardHints());
147
+ console.log('');
148
+ console.log(dim(' ─────────────────────────────────────────────'));
149
+ console.log('');
150
+ }
151
+ // Status line and keyboard only in human mode
152
+ const statusLine = useJson ? null : createStatusLine(process.stdout);
153
+ let syncedCount = 0;
154
+ let logFilter = 'all';
155
+ function updateStatus() {
156
+ if (!statusLine)
157
+ return;
158
+ const filterLabel = logFilter === 'all' ? 'All' : logFilter === 'events' ? 'Events' : 'Runtime';
159
+ const sep = '\x1b[90m\u2502\x1b[39m';
160
+ statusLine.update(`${bold(config.appName)} ${sep} ${green(session.previewUrl)} ${sep} ${cyan(`${syncedCount} synced`)} ${sep} ${filterLabel} ${sep} \x1b[90mo\x1b[39m:open \x1b[90mi\x1b[39m:info \x1b[90ma/e/r\x1b[39m:filter \x1b[90mq\x1b[39m:quit`);
161
+ }
162
+ if (statusLine)
163
+ updateStatus();
112
164
  let logTailer;
113
- // Register cleanup before starting watcher to avoid race
165
+ const keyboard = useJson ? null : createKeyboardListener();
114
166
  const cleanup = async () => {
115
- console.log('\nStopping...');
167
+ if (!useJson)
168
+ console.log('\nStopping...');
116
169
  logTailer?.stop();
117
170
  await stopAutoCommit();
171
+ statusLine?.destroy();
172
+ keyboard?.stop();
118
173
  process.exit(0);
119
174
  };
120
175
  process.on('SIGINT', cleanup);
121
176
  process.on('SIGTERM', cleanup);
177
+ if (keyboard) {
178
+ keyboard.start((action) => {
179
+ switch (action) {
180
+ case 'o':
181
+ case 'p':
182
+ import('open').then(m => m.default(session.previewUrl));
183
+ break;
184
+ case 'a':
185
+ logFilter = 'all';
186
+ console.log(dim(' Showing all logs.'));
187
+ updateStatus();
188
+ break;
189
+ case 'e':
190
+ logFilter = 'events';
191
+ console.log(dim(' Showing events only.'));
192
+ updateStatus();
193
+ break;
194
+ case 'r':
195
+ logFilter = 'runtime';
196
+ console.log(dim(' Showing runtime logs only.'));
197
+ updateStatus();
198
+ break;
199
+ case 'i':
200
+ console.log(getInfoPanel({
201
+ appName: config.appName,
202
+ previewUrl: session.previewUrl,
203
+ workspaceName: config.workspaceName,
204
+ directory: cwd,
205
+ }));
206
+ break;
207
+ case 'quit':
208
+ cleanup();
209
+ break;
210
+ }
211
+ });
212
+ }
122
213
  // Start file watcher
123
- console.log('Watching for file changes...');
124
- await watchAndAutoCommit(process.cwd(), client, config.appId);
214
+ await watchAndAutoCommit(process.cwd(), client, config.appId, {
215
+ onFastSync: useJson
216
+ ? (count) => { jsonLine({ event: 'files_synced', count, target: 'preview', timestamp: ts() }); }
217
+ : (count) => { syncedCount += count; updateStatus(); },
218
+ onGitPush: useJson
219
+ ? (count) => { jsonLine({ event: 'files_pushed', count, target: 'git', timestamp: ts() }); }
220
+ : (count) => { syncedCount += count; updateStatus(); },
221
+ });
125
222
  // Start log tailing (unless --no-logs)
126
- if (options.logs !== false) {
223
+ if (options?.logs !== false) {
127
224
  logTailer = startLogTailer({
128
225
  appId: config.appId,
129
226
  client,
130
227
  projectDir: cwd,
131
- toTerminal: !options.logsOnlyFile,
228
+ toTerminal: !options?.logsOnlyFile,
132
229
  toFile: true,
230
+ getFilter: () => logFilter,
231
+ useJson,
133
232
  });
134
- console.log('Log tailing active. Logs written to .runwork/logs.txt');
135
233
  }
136
234
  // Keep alive
137
235
  await new Promise(() => { });
236
+ }
237
+ export const devCommand = new Command('dev')
238
+ .description('Start local development with live sync, preview sandbox, and file watching')
239
+ .option('--no-logs', 'Disable automatic log tailing')
240
+ .option('--logs-only-file', 'Write logs to file only, not terminal')
241
+ .action(async (options, command) => {
242
+ const globalJson = shouldOutputJson(command.optsWithGlobals().json);
243
+ await execDev({ ...options, json: globalJson });
138
244
  });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const infoCommand: Command;