runwork 0.3.0 → 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.
@@ -0,0 +1,432 @@
1
+ import { Command } from 'commander';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { shouldOutputJson, jsonOut } from '../utils/output.js';
5
+ import { bold, cyan, dim, green, yellow, gray } from '../ui/colors.js';
6
+ import { VERSION } from '../generated/version.js';
7
+ import { requireAuth } from '../auth/store.js';
8
+ import { ApiClient } from '../api/client.js';
9
+ function readConfig() {
10
+ if (!existsSync('.runwork.json')) {
11
+ console.error('No .runwork.json found. Run `runwork init` first.');
12
+ process.exit(1);
13
+ }
14
+ return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
15
+ }
16
+ function readBlueprint(cwd) {
17
+ const blueprintPath = join(cwd, 'blueprint.json');
18
+ if (!existsSync(blueprintPath))
19
+ return null;
20
+ try {
21
+ return JSON.parse(readFileSync(blueprintPath, 'utf-8'));
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ function mergeSources(blueprintItems, serverItems, getServerName, appId) {
28
+ const map = new Map();
29
+ for (const bp of blueprintItems) {
30
+ map.set(bp.name, { sources: ['blueprint'], blueprintExtra: bp.extra });
31
+ }
32
+ for (const serverItem of serverItems) {
33
+ if (serverItem.appId !== appId)
34
+ continue;
35
+ const name = getServerName(serverItem);
36
+ const mode = serverItem.deploymentMode;
37
+ const source = mode === 'production' ? 'production' : 'preview';
38
+ const existing = map.get(name);
39
+ if (existing) {
40
+ existing.sources.push(source);
41
+ existing.serverItem = serverItem;
42
+ }
43
+ else {
44
+ map.set(name, { sources: [source], serverItem: serverItem });
45
+ }
46
+ }
47
+ return map;
48
+ }
49
+ function buildRegistries(blueprint, serverData, appId) {
50
+ // Entities
51
+ const bpEntities = (blueprint?.entities ?? []).map(e => ({
52
+ name: e.entityName,
53
+ extra: e.schema ? { schema: e.schema } : {},
54
+ }));
55
+ const entityMap = mergeSources(bpEntities, serverData?.entities ?? [], (e) => e.entityName, appId);
56
+ const entities = Array.from(entityMap.entries()).map(([name, v]) => ({
57
+ name,
58
+ sources: v.sources,
59
+ ...(v.blueprintExtra?.schema ? { schema: v.blueprintExtra.schema } :
60
+ v.serverItem && 'schema' in v.serverItem && v.serverItem.schema ? { schema: v.serverItem.schema } : {}),
61
+ }));
62
+ // Scheduled jobs (blueprint may use either key)
63
+ const bpSchedules = (blueprint?.scheduledJobs ?? blueprint?.schedules ?? []).map(s => ({
64
+ name: s.name,
65
+ extra: { schedule: s.schedule, description: s.description },
66
+ }));
67
+ const scheduleMap = mergeSources(bpSchedules, serverData?.schedules ?? [], (s) => s.name, appId);
68
+ const scheduledJobs = Array.from(scheduleMap.entries()).map(([name, v]) => {
69
+ const serverItem = v.serverItem;
70
+ return {
71
+ name,
72
+ sources: v.sources,
73
+ schedule: v.blueprintExtra?.schedule ?? serverItem?.schedule,
74
+ description: v.blueprintExtra?.description ?? serverItem?.description,
75
+ };
76
+ });
77
+ // Workflows
78
+ const bpWorkflows = (blueprint?.workflows ?? []).map(w => ({
79
+ name: w.name,
80
+ extra: { description: w.description },
81
+ }));
82
+ const workflowMap = mergeSources(bpWorkflows, serverData?.workflows ?? [], (w) => w.name, appId);
83
+ const workflows = Array.from(workflowMap.entries()).map(([name, v]) => {
84
+ const serverItem = v.serverItem;
85
+ return {
86
+ name,
87
+ sources: v.sources,
88
+ description: v.blueprintExtra?.description ?? serverItem?.description,
89
+ };
90
+ });
91
+ // Agents
92
+ const bpAgents = (blueprint?.agents ?? []).map(a => ({
93
+ name: a.name,
94
+ extra: { type: a.type, description: a.description },
95
+ }));
96
+ const agentMap = mergeSources(bpAgents, serverData?.agents ?? [], (a) => a.name, appId);
97
+ const agents = Array.from(agentMap.entries()).map(([name, v]) => {
98
+ const serverItem = v.serverItem;
99
+ return {
100
+ name,
101
+ sources: v.sources,
102
+ type: v.blueprintExtra?.type ?? serverItem?.type,
103
+ description: v.blueprintExtra?.description ?? serverItem?.description,
104
+ };
105
+ });
106
+ // Public endpoints (blueprint may use either key)
107
+ const bpEndpoints = (blueprint?.publicEndpoints ?? blueprint?.endpoints ?? []).map(e => ({
108
+ name: `${e.method ?? 'ANY'} ${e.path}`,
109
+ extra: { path: e.path, method: e.method, description: e.description },
110
+ }));
111
+ const endpointMap = mergeSources(bpEndpoints, serverData?.endpoints ?? [], (e) => `${e.method ?? 'ANY'} ${e.path}`, appId);
112
+ const publicEndpoints = Array.from(endpointMap.entries()).map(([, v]) => {
113
+ const serverItem = v.serverItem;
114
+ return {
115
+ path: v.blueprintExtra?.path ?? serverItem?.path ?? '',
116
+ method: v.blueprintExtra?.method ?? serverItem?.method,
117
+ sources: v.sources,
118
+ description: v.blueprintExtra?.description ?? serverItem?.description,
119
+ };
120
+ });
121
+ // Components
122
+ const bpComponents = (blueprint?.components ?? []).map(c => ({
123
+ name: c.componentName,
124
+ extra: {},
125
+ }));
126
+ const componentMap = mergeSources(bpComponents, serverData?.components ?? [], (c) => c.componentName, appId);
127
+ const components = Array.from(componentMap.entries()).map(([name, v]) => ({
128
+ name,
129
+ sources: v.sources,
130
+ }));
131
+ // File storage
132
+ const bpHasStorage = blueprint
133
+ ? typeof blueprint.fileStorage === 'boolean'
134
+ ? blueprint.fileStorage
135
+ : typeof blueprint.fileStorage === 'object'
136
+ ? (blueprint.fileStorage.enabled !== false)
137
+ : false
138
+ : false;
139
+ const serverStorages = (serverData?.fileStorages ?? []).filter(fs => fs.appId === appId);
140
+ const storageSources = [];
141
+ if (bpHasStorage)
142
+ storageSources.push('blueprint');
143
+ for (const ss of serverStorages) {
144
+ const mode = ss.deploymentMode;
145
+ if (mode === 'production')
146
+ storageSources.push('production');
147
+ else
148
+ storageSources.push('preview');
149
+ }
150
+ const fileStorage = {
151
+ enabled: bpHasStorage || serverStorages.length > 0,
152
+ sources: storageSources,
153
+ };
154
+ return { entities, workflows, scheduledJobs, agents, publicEndpoints, components, fileStorage };
155
+ }
156
+ const CLI_COMMANDS = [
157
+ {
158
+ name: 'info',
159
+ description: 'Show app context, registries, integrations, and CLI reference',
160
+ usage: 'runwork info [--json]',
161
+ examples: [
162
+ 'runwork info --json',
163
+ "runwork info --json | jq '.registries.entities'",
164
+ "runwork info --json | jq '.preview.url'",
165
+ ],
166
+ },
167
+ {
168
+ name: 'dev',
169
+ description: 'Start local development with live sync, preview sandbox, and file watching',
170
+ usage: 'runwork dev [--json] [--no-logs]',
171
+ examples: ['runwork dev', 'runwork dev --json'],
172
+ },
173
+ {
174
+ name: 'deploy',
175
+ description: 'Deploy the current app to production',
176
+ usage: 'runwork deploy [--json]',
177
+ examples: ['runwork deploy', 'runwork deploy --json'],
178
+ },
179
+ {
180
+ name: 'logs',
181
+ description: 'View app logs and events from preview or production',
182
+ usage: 'runwork logs [--json] [--events] [--production] [--follow] [--type <type>] [--search <text>]',
183
+ examples: [
184
+ 'runwork logs --json',
185
+ 'runwork logs --events --json',
186
+ 'runwork logs --events --type failed --json',
187
+ 'runwork logs --production --json',
188
+ 'runwork logs --follow --json',
189
+ ],
190
+ },
191
+ {
192
+ name: 'open',
193
+ description: 'Open preview URL or dashboard in browser. In JSON mode, outputs the URL without opening the browser.',
194
+ usage: 'runwork open [preview|dashboard] [--json]',
195
+ examples: ['runwork open', 'runwork open dashboard', 'runwork open --json'],
196
+ },
197
+ {
198
+ name: 'integrations search',
199
+ description: 'Search available integrations from the platform catalog (3200+ integrations)',
200
+ usage: 'runwork integrations search <query> [--json] [--limit <n>]',
201
+ examples: [
202
+ 'runwork integrations search stripe --json',
203
+ "runwork integrations search 'google drive' --json --limit 5",
204
+ ],
205
+ },
206
+ {
207
+ name: 'integrations list',
208
+ description: 'Show integrations connected to the current workspace',
209
+ usage: 'runwork integrations list [--json]',
210
+ examples: ['runwork integrations list --json'],
211
+ },
212
+ {
213
+ name: 'login',
214
+ description: 'Authenticate with Runwork platform (interactive -- requires browser)',
215
+ usage: 'runwork login [--base-url <url>]',
216
+ examples: ['runwork login'],
217
+ note: 'Not available in JSON mode -- requires interactive browser OAuth flow',
218
+ },
219
+ {
220
+ name: 'init',
221
+ description: 'Initialize a new Runwork app (interactive)',
222
+ usage: 'runwork init [name]',
223
+ examples: ['runwork init my-app'],
224
+ note: 'Requires interactive prompts for workspace selection. Provide app name as argument to reduce prompts.',
225
+ },
226
+ {
227
+ name: 'clone',
228
+ description: 'Clone an existing Runwork app to a local directory for development',
229
+ usage: 'runwork clone [appId] [directory]',
230
+ examples: ['runwork clone abc-123 ./my-app'],
231
+ note: 'Provide appId argument to skip interactive selection',
232
+ },
233
+ {
234
+ name: 'upgrade',
235
+ description: 'Upgrade Runwork CLI to the latest version. Use --check to see if an update is available without installing.',
236
+ usage: 'runwork upgrade [--check]',
237
+ examples: ['runwork upgrade', 'runwork upgrade --check'],
238
+ },
239
+ {
240
+ name: 'logout',
241
+ description: 'Remove stored credentials and git credential configuration',
242
+ usage: 'runwork logout',
243
+ examples: ['runwork logout'],
244
+ },
245
+ ];
246
+ function formatSources(sources) {
247
+ return sources
248
+ .map(s => {
249
+ if (s === 'blueprint')
250
+ return cyan(s);
251
+ if (s === 'preview')
252
+ return yellow(s);
253
+ if (s === 'production')
254
+ return green(s);
255
+ return s;
256
+ })
257
+ .join(dim(', '));
258
+ }
259
+ function printHumanOutput(data) {
260
+ console.log('');
261
+ console.log(bold('Runwork App Info'));
262
+ console.log('');
263
+ const pad = (label) => label.padEnd(12);
264
+ console.log(` ${dim(pad('App:'))}${bold(data.app.name)}`);
265
+ console.log(` ${dim(pad('Workspace:'))}${data.workspace.name || data.workspace.id}`);
266
+ if (data.preview.url) {
267
+ console.log(` ${dim(pad('Preview:'))}${green(data.preview.url)} ${dim('(running)')}`);
268
+ }
269
+ else {
270
+ console.log(` ${dim(pad('Preview:'))}${dim('(not running)')}`);
271
+ }
272
+ console.log(` ${dim(pad('Production:'))}${dim('(not available)')}`);
273
+ console.log('');
274
+ // Integrations
275
+ if (data.integrations === null) {
276
+ console.log(bold('Integrations:'));
277
+ console.log(` ${dim('(unavailable -- API error or not authenticated)')}`);
278
+ }
279
+ else if (data.integrations.length === 0) {
280
+ console.log(bold('Integrations:'));
281
+ console.log(` ${dim('(none connected)')}`);
282
+ }
283
+ else {
284
+ console.log(bold('Integrations:'));
285
+ for (const int of data.integrations) {
286
+ const statusColor = int.status === 'connected' ? green : yellow;
287
+ console.log(` ${int.id.padEnd(20)} ${statusColor(int.status)}`);
288
+ }
289
+ }
290
+ console.log('');
291
+ // Registries
292
+ if (data.registries === null) {
293
+ // Skip entirely if unavailable
294
+ }
295
+ else {
296
+ const reg = data.registries;
297
+ const hasAny = reg.entities.length > 0 || reg.workflows.length > 0 ||
298
+ reg.scheduledJobs.length > 0 || reg.agents.length > 0 ||
299
+ reg.publicEndpoints.length > 0 || reg.components.length > 0 ||
300
+ reg.fileStorage.enabled;
301
+ if (!hasAny) {
302
+ // No registries -- skip the section entirely
303
+ }
304
+ else {
305
+ console.log(bold('Registries:'));
306
+ if (reg.entities.length > 0) {
307
+ console.log(` ${bold('Entities:')}`);
308
+ for (const e of reg.entities) {
309
+ console.log(` ${e.name.padEnd(24)} ${formatSources(e.sources)}`);
310
+ }
311
+ console.log('');
312
+ }
313
+ if (reg.workflows.length > 0) {
314
+ console.log(` ${bold('Workflows:')}`);
315
+ for (const w of reg.workflows) {
316
+ const desc = w.description ? ` ${dim(w.description)}` : '';
317
+ console.log(` ${w.name.padEnd(24)} ${formatSources(w.sources)}${desc}`);
318
+ }
319
+ console.log('');
320
+ }
321
+ if (reg.scheduledJobs.length > 0) {
322
+ console.log(` ${bold('Scheduled Jobs:')}`);
323
+ for (const s of reg.scheduledJobs) {
324
+ const schedule = s.schedule ? ` ${gray(s.schedule)}` : '';
325
+ console.log(` ${s.name.padEnd(24)} ${formatSources(s.sources)}${schedule}`);
326
+ }
327
+ console.log('');
328
+ }
329
+ if (reg.agents.length > 0) {
330
+ console.log(` ${bold('Agents:')}`);
331
+ for (const a of reg.agents) {
332
+ const type = a.type ? ` ${dim(a.type)}` : '';
333
+ console.log(` ${a.name.padEnd(24)} ${formatSources(a.sources)}${type}`);
334
+ }
335
+ console.log('');
336
+ }
337
+ if (reg.publicEndpoints.length > 0) {
338
+ console.log(` ${bold('Public Endpoints:')}`);
339
+ for (const e of reg.publicEndpoints) {
340
+ const label = e.method ? `${e.method} ${e.path}` : e.path;
341
+ console.log(` ${label.padEnd(24)} ${formatSources(e.sources)}`);
342
+ }
343
+ console.log('');
344
+ }
345
+ if (reg.components.length > 0) {
346
+ console.log(` ${bold('Components:')}`);
347
+ for (const c of reg.components) {
348
+ console.log(` ${c.name.padEnd(24)} ${formatSources(c.sources)}`);
349
+ }
350
+ console.log('');
351
+ }
352
+ if (reg.fileStorage.enabled) {
353
+ console.log(` ${bold('File Storage:')} ${formatSources(reg.fileStorage.sources)}`);
354
+ console.log('');
355
+ }
356
+ } // end hasAny
357
+ }
358
+ // CLI commands
359
+ console.log(bold('CLI Commands:'));
360
+ for (const cmd of data.cli.commands) {
361
+ console.log(` ${cyan(('runwork ' + cmd.name).padEnd(36))} ${dim(cmd.description)}`);
362
+ }
363
+ console.log('');
364
+ console.log(dim(` CLI version: ${data.cli.version}`));
365
+ console.log('');
366
+ }
367
+ export const infoCommand = new Command('info')
368
+ .description('Show app context, registries, and CLI reference (use --json for agent discovery)')
369
+ .action(async (_opts, command) => {
370
+ const asJson = shouldOutputJson(command.optsWithGlobals().json);
371
+ const config = readConfig();
372
+ const blueprint = readBlueprint(process.cwd());
373
+ const creds = requireAuth();
374
+ const client = new ApiClient(creds);
375
+ // Preview status -- read-only, never starts a session
376
+ let preview = { url: null, active: false };
377
+ try {
378
+ const status = await client.getDevStatus(config.appId);
379
+ if (status.previewUrl) {
380
+ preview = { url: status.previewUrl, active: true };
381
+ }
382
+ }
383
+ catch {
384
+ // No active session -- that's fine
385
+ }
386
+ // Integrations
387
+ let integrations = null;
388
+ try {
389
+ const raw = await client.listConnectedIntegrations(config.workspaceId);
390
+ integrations = raw.map(i => ({
391
+ id: i.canonicalId ?? i.integrationId,
392
+ status: i.status ?? 'connected',
393
+ }));
394
+ }
395
+ catch {
396
+ process.stderr.write('Warning: failed to fetch integrations\n');
397
+ }
398
+ // Workspace registries
399
+ let serverData = null;
400
+ try {
401
+ serverData = await client.getWorkspaceAll(config.workspaceId);
402
+ }
403
+ catch {
404
+ process.stderr.write('Warning: failed to fetch workspace registries\n');
405
+ }
406
+ const registries = buildRegistries(blueprint, serverData, config.appId);
407
+ const output = {
408
+ app: {
409
+ id: config.appId,
410
+ name: config.appName,
411
+ slug: config.appName,
412
+ },
413
+ workspace: {
414
+ id: config.workspaceId,
415
+ name: config.workspaceName ?? config.workspaceId,
416
+ },
417
+ preview,
418
+ production: { url: null, deployed: false },
419
+ integrations,
420
+ registries,
421
+ cli: {
422
+ version: VERSION,
423
+ commands: CLI_COMMANDS,
424
+ },
425
+ };
426
+ if (asJson) {
427
+ jsonOut(output);
428
+ }
429
+ else {
430
+ printHumanOutput(output);
431
+ }
432
+ });
@@ -2,6 +2,7 @@ import { Command } from 'commander';
2
2
  import { requireAuth } from '../auth/store.js';
3
3
  import { ApiClient } from '../api/client.js';
4
4
  import { readFileSync, existsSync } from 'fs';
5
+ import { shouldOutputJson, jsonOut } from '../utils/output.js';
5
6
  function readConfig() {
6
7
  if (!existsSync('.runwork.json')) {
7
8
  console.error('No .runwork.json found. Run `runwork init` first.');
@@ -13,12 +14,18 @@ const searchCommand = new Command('search')
13
14
  .description('Search available integrations from the platform catalog')
14
15
  .argument('<query>', 'Search query (e.g., "google drive", "hubspot", "slack")')
15
16
  .option('--limit <n>', 'Maximum results to show', '20')
16
- .action(async (query, opts) => {
17
+ .action(async (query, opts, command) => {
18
+ const useJson = shouldOutputJson(command.optsWithGlobals().json);
17
19
  const credentials = requireAuth();
18
20
  const client = new ApiClient(credentials);
19
21
  const limit = parseInt(opts.limit, 10);
20
22
  try {
21
23
  const { results } = await client.searchIntegrations(query, limit);
24
+ if (useJson) {
25
+ const items = results.map(({ provider: _p, ...rest }) => rest);
26
+ jsonOut({ results: items });
27
+ return;
28
+ }
22
29
  if (results.length === 0) {
23
30
  console.log(`No integrations found for "${query}".`);
24
31
  console.log('Try broader terms (e.g., "calendar" instead of "google calendar").');
@@ -56,12 +63,22 @@ const searchCommand = new Command('search')
56
63
  });
57
64
  const listCommand = new Command('list')
58
65
  .description('List connected workspace integrations')
59
- .action(async () => {
66
+ .action(async (_opts, command) => {
67
+ const useJson = shouldOutputJson(command.optsWithGlobals().json);
60
68
  const credentials = requireAuth();
61
69
  const client = new ApiClient(credentials);
62
70
  const config = readConfig();
63
71
  try {
64
72
  const integrations = await client.listConnectedIntegrations(config.workspaceId);
73
+ if (useJson) {
74
+ const items = integrations.map(i => ({
75
+ id: i.canonicalId || i.integrationId,
76
+ status: i.status || 'connected',
77
+ createdAt: i.createdAt || null,
78
+ }));
79
+ jsonOut({ items });
80
+ return;
81
+ }
65
82
  if (integrations.length === 0) {
66
83
  console.log('No integrations connected in this workspace.');
67
84
  console.log('Connect integrations at https://runwork.ai/workspace-settings');
@@ -2,6 +2,7 @@ import { Command } from 'commander';
2
2
  import { readFileSync, existsSync } from 'fs';
3
3
  import { requireAuth } from '../auth/store.js';
4
4
  import { ApiClient } from '../api/client.js';
5
+ import { shouldOutputJson, jsonLine } from '../utils/output.js';
5
6
  function readConfig() {
6
7
  if (!existsSync('.runwork.json')) {
7
8
  console.error('No .runwork.json found. Run `runwork init` first.');
@@ -25,7 +26,8 @@ export const logsCommand = new Command('logs')
25
26
  .option('--level <level>', 'Filter by log level (production only)')
26
27
  .option('--search <text>', 'Search log content (production only)')
27
28
  .option('--type <type>', 'Filter by event type (events only)')
28
- .action(async (opts) => {
29
+ .action(async (opts, command) => {
30
+ const useJson = shouldOutputJson(command.optsWithGlobals().json);
29
31
  const config = readConfig();
30
32
  const creds = requireAuth();
31
33
  const client = new ApiClient(creds);
@@ -50,15 +52,22 @@ export const logsCommand = new Command('logs')
50
52
  level: opts.level,
51
53
  search: opts.search,
52
54
  });
53
- if (logs.length === 0) {
54
- console.log('No production logs found.');
55
+ if (useJson) {
56
+ for (const entry of logs) {
57
+ jsonLine(entry);
58
+ }
55
59
  }
56
60
  else {
57
- for (const entry of logs) {
58
- console.log(JSON.stringify(entry));
61
+ if (logs.length === 0) {
62
+ console.log('No production logs found.');
59
63
  }
60
- if (pagination?.hasMore) {
61
- console.log(`... ${pagination.total ? `${pagination.total} total entries` : 'more entries available'}`);
64
+ else {
65
+ for (const entry of logs) {
66
+ console.log(JSON.stringify(entry));
67
+ }
68
+ if (pagination?.hasMore) {
69
+ console.log(`... ${pagination.total ? `${pagination.total} total entries` : 'more entries available'}`);
70
+ }
62
71
  }
63
72
  }
64
73
  }
@@ -81,25 +90,61 @@ export const logsCommand = new Command('logs')
81
90
  }
82
91
  if (stdout && stdout.length > lastStdoutLength) {
83
92
  const newContent = stdout.slice(lastStdoutLength);
84
- process.stdout.write(newContent);
93
+ if (useJson) {
94
+ for (const line of newContent.split('\n')) {
95
+ if (line) {
96
+ jsonLine({ type: 'runtime', message: line, timestamp: new Date().toISOString() });
97
+ }
98
+ }
99
+ }
100
+ else {
101
+ process.stdout.write(newContent);
102
+ }
85
103
  }
86
104
  if (stderr && stderr.length > lastStderrLength) {
87
105
  const newContent = stderr.slice(lastStderrLength);
88
- process.stderr.write(newContent);
106
+ if (useJson) {
107
+ for (const line of newContent.split('\n')) {
108
+ if (line) {
109
+ jsonLine({ type: 'error', message: line, timestamp: new Date().toISOString() });
110
+ }
111
+ }
112
+ }
113
+ else {
114
+ process.stderr.write(newContent);
115
+ }
89
116
  }
90
117
  lastStdoutLength = stdout ? stdout.length : 0;
91
118
  lastStderrLength = stderr ? stderr.length : 0;
92
119
  }
93
120
  else {
94
121
  // First poll or non-follow: print everything
95
- if (stdout) {
96
- console.log(stdout);
122
+ if (useJson) {
123
+ if (stdout) {
124
+ for (const line of stdout.split('\n')) {
125
+ if (line) {
126
+ jsonLine({ type: 'runtime', message: line, timestamp: new Date().toISOString() });
127
+ }
128
+ }
129
+ }
130
+ if (stderr) {
131
+ for (const line of stderr.split('\n')) {
132
+ if (line) {
133
+ jsonLine({ type: 'error', message: line, timestamp: new Date().toISOString() });
134
+ }
135
+ }
136
+ }
97
137
  }
98
- if (stderr) {
99
- console.error(stderr);
100
- }
101
- if (!stdout && !stderr) {
102
- console.log('No preview logs available.');
138
+ else {
139
+ if (stdout) {
140
+ console.log(stdout);
141
+ }
142
+ if (stderr) {
143
+ console.error(stderr);
144
+ }
145
+ if (!stdout && !stderr) {
146
+ console.log('No preview logs available.');
147
+ }
103
148
  }
104
149
  // Initialize cursors for subsequent follow polls
105
150
  lastStdoutLength = stdout ? stdout.length : 0;
@@ -112,7 +157,7 @@ export const logsCommand = new Command('logs')
112
157
  }
113
158
  }
114
159
  if (showEvents) {
115
- if (showRuntime && isFirstPoll) {
160
+ if (!useJson && showRuntime && isFirstPoll) {
116
161
  console.log('\n--- Events ---\n');
117
162
  }
118
163
  try {
@@ -120,15 +165,29 @@ export const logsCommand = new Command('logs')
120
165
  limit,
121
166
  type: opts.type,
122
167
  });
123
- if (events.length === 0 && isFirstPoll) {
124
- console.log('No events found.');
168
+ if (useJson) {
169
+ for (const event of events) {
170
+ jsonLine({
171
+ type: 'event',
172
+ eventType: event.type,
173
+ source: event.source,
174
+ detail: event.summary || event.content,
175
+ metadata: event.metadata || {},
176
+ timestamp: event.timestamp,
177
+ });
178
+ }
125
179
  }
126
180
  else {
127
- for (const event of events) {
128
- console.log(formatEvent(event));
181
+ if (events.length === 0 && isFirstPoll) {
182
+ console.log('No events found.');
129
183
  }
130
- if (hasMore && isFirstPoll) {
131
- console.log(`... ${totalCount} total events`);
184
+ else {
185
+ for (const event of events) {
186
+ console.log(formatEvent(event));
187
+ }
188
+ if (hasMore && isFirstPoll) {
189
+ console.log(`... ${totalCount} total events`);
190
+ }
132
191
  }
133
192
  }
134
193
  }
@@ -3,10 +3,12 @@ import { readFileSync, existsSync } from 'fs';
3
3
  import { requireAuth } from '../auth/store.js';
4
4
  import { ApiClient } from '../api/client.js';
5
5
  import { cyan } from '../ui/colors.js';
6
+ import { shouldOutputJson, jsonOut } from '../utils/output.js';
6
7
  export const openCommand = new Command('open')
7
8
  .description('Open app preview or dashboard in browser')
8
9
  .argument('[target]', 'What to open: preview (default), dashboard', 'preview')
9
- .action(async (target) => {
10
+ .action(async (target, _opts, command) => {
11
+ const useJson = shouldOutputJson(command.optsWithGlobals().json);
10
12
  if (!existsSync('.runwork.json')) {
11
13
  console.error('No .runwork.json found. Run `runwork init` first.');
12
14
  process.exit(1);
@@ -26,12 +28,20 @@ export const openCommand = new Command('open')
26
28
  const session = await client.startDevSession(config.appId);
27
29
  previewUrl = session.previewUrl;
28
30
  }
31
+ if (useJson) {
32
+ jsonOut({ target: 'preview', url: previewUrl });
33
+ return;
34
+ }
29
35
  console.log(`Opening preview: ${cyan(previewUrl)}`);
30
36
  await open.default(previewUrl);
31
37
  break;
32
38
  }
33
39
  case 'dashboard': {
34
40
  const url = `${creds.baseUrl}/apps/${config.appId}`;
41
+ if (useJson) {
42
+ jsonOut({ target: 'dashboard', url });
43
+ return;
44
+ }
35
45
  console.log(`Opening dashboard: ${cyan(url)}`);
36
46
  await open.default(url);
37
47
  break;
@@ -1 +1 @@
1
- export declare const VERSION = "0.3.0";
1
+ export declare const VERSION = "0.4.0";