runwork 0.2.4 → 0.3.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 (52) hide show
  1. package/dist/auth/login-flow.d.ts +10 -0
  2. package/dist/auth/login-flow.js +37 -0
  3. package/dist/commands/clone.d.ts +3 -0
  4. package/dist/commands/clone.js +32 -24
  5. package/dist/commands/dev.d.ts +4 -0
  6. package/dist/commands/dev.js +103 -85
  7. package/dist/commands/init.d.ts +3 -0
  8. package/dist/commands/init.js +29 -21
  9. package/dist/commands/login.js +10 -25
  10. package/dist/commands/open.d.ts +2 -0
  11. package/dist/commands/open.js +43 -0
  12. package/dist/commands/welcome.d.ts +1 -0
  13. package/dist/commands/welcome.js +83 -0
  14. package/dist/generated/version.d.ts +1 -1
  15. package/dist/generated/version.js +1 -1
  16. package/dist/git/__tests__/auto-commit.test.d.ts +1 -0
  17. package/dist/git/__tests__/auto-commit.test.js +373 -0
  18. package/dist/git/__tests__/manifest.test.d.ts +1 -0
  19. package/dist/git/__tests__/manifest.test.js +377 -0
  20. package/dist/git/__tests__/sync.test.d.ts +1 -0
  21. package/dist/git/__tests__/sync.test.js +405 -0
  22. package/dist/git/auto-commit.d.ts +6 -1
  23. package/dist/git/auto-commit.js +29 -13
  24. package/dist/git/sync.d.ts +15 -0
  25. package/dist/git/sync.js +157 -0
  26. package/dist/index.js +16 -0
  27. package/dist/logs/__tests__/tailer-format.test.d.ts +1 -0
  28. package/dist/logs/__tests__/tailer-format.test.js +43 -0
  29. package/dist/logs/tailer.d.ts +3 -0
  30. package/dist/logs/tailer.js +47 -10
  31. package/dist/template/manifest.js +24 -4
  32. package/dist/types.d.ts +1 -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__/prompt.test.js +23 -99
  50. package/dist/utils/prompt.d.ts +1 -0
  51. package/dist/utils/prompt.js +29 -21
  52. package/package.json +4 -2
@@ -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
  });
@@ -1,2 +1,6 @@
1
1
  import { Command } from 'commander';
2
+ export declare function execDev(options?: {
3
+ logs?: boolean;
4
+ logsOnlyFile?: boolean;
5
+ }): Promise<void>;
2
6
  export declare const devCommand: Command;
@@ -1,15 +1,20 @@
1
1
  import { Command } from 'commander';
2
2
  import { execFileSync } from 'child_process';
3
- import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
3
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { requireAuth } from '../auth/store.js';
6
6
  import { ApiClient } from '../api/client.js';
7
7
  import { watchAndAutoCommit, stopAutoCommit } from '../git/auto-commit.js';
8
+ import { syncWithRemote } from '../git/sync.js';
8
9
  import { startLogTailer } from '../logs/tailer.js';
9
10
  import { populateTypes } from '../types-manager.js';
10
11
  import { loadManifest, generateManifest, saveManifest, detectUserEdits } from '../template/manifest.js';
11
12
  import { extractZip } from '../utils/zip.js';
12
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';
13
18
  async function populateSkill(projectDir, client, appId) {
14
19
  try {
15
20
  const { skill } = await client.getAppSkill(appId);
@@ -28,30 +33,7 @@ function readConfig() {
28
33
  }
29
34
  return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
30
35
  }
31
- function hasCommits() {
32
- try {
33
- execFileSync('git', ['rev-parse', 'HEAD'], { stdio: 'pipe' });
34
- return true;
35
- }
36
- catch {
37
- return false;
38
- }
39
- }
40
- function hasTrackedChanges() {
41
- try {
42
- // Only check tracked files (modified/deleted/staged) - not untracked (??) files
43
- const output = execFileSync('git', ['status', '--porcelain'], { encoding: 'utf-8' });
44
- return output.trim().split('\n').some(line => line.length > 0 && !line.startsWith('??'));
45
- }
46
- catch {
47
- return false;
48
- }
49
- }
50
- export const devCommand = new Command('dev')
51
- .description('Start local development server with live sync')
52
- .option('--no-logs', 'Disable automatic log tailing')
53
- .option('--logs-only-file', 'Write logs to file only, not terminal')
54
- .action(async (options) => {
36
+ export async function execDev(options) {
55
37
  const config = readConfig();
56
38
  const creds = requireAuth();
57
39
  const client = new ApiClient(creds);
@@ -75,7 +57,7 @@ export const devCommand = new Command('dev')
75
57
  }
76
58
  }
77
59
  // Download fresh template to ensure latest version
78
- console.log('Updating template...');
60
+ console.log(dim('Updating template...'));
79
61
  try {
80
62
  const zipData = await client.downloadSkeleton();
81
63
  extractZip(zipData, cwd);
@@ -92,97 +74,133 @@ export const devCommand = new Command('dev')
92
74
  }
93
75
  }
94
76
  catch {
95
- console.warn('Template update failed. Continuing with current files.');
77
+ console.warn(yellow('Template update failed. Continuing with current files.'));
96
78
  }
97
79
  // Populate type definitions
98
80
  await populateTypes(cwd);
99
81
  // Start dev session first - this triggers DO onStart which may create remote commits
100
- console.log('Starting dev session...');
82
+ console.log(dim('Starting dev session...'));
101
83
  const session = await client.startDevSession(config.appId);
102
- console.log(`Preview: ${session.previewUrl}`);
103
84
  // Fetch SKILL.md (best-effort, after session so DO registries are available)
104
85
  await populateSkill(cwd, client, config.appId);
105
86
  // Sync AFTER starting session so we pick up any commits the DO created
106
- if (hasCommits()) {
107
- console.log('Syncing with Runwork...');
108
- const dirty = hasTrackedChanges();
109
- if (dirty) {
110
- execFileSync('git', ['stash', 'push', '-m', 'runwork-dev-sync'], { stdio: 'inherit' });
111
- }
112
- try {
113
- // Fetch first, then remove untracked skeleton files that conflict
114
- // with the remote before rebasing. Skeleton files are ephemeral
115
- // (re-downloaded each session) so the server's versions take precedence.
116
- // User-edited files are already tracked/committed at this point.
117
- execFileSync('git', ['fetch', 'runwork', 'main'], { stdio: 'inherit' });
118
- try {
119
- const remoteFiles = execFileSync('git', ['ls-tree', '-r', '--name-only', 'runwork/main'], { encoding: 'utf-8' }).trim().split('\n');
120
- const untrackedOutput = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], { encoding: 'utf-8' }).trim();
121
- const untracked = new Set(untrackedOutput.split('\n').filter(Boolean));
122
- for (const file of remoteFiles) {
123
- if (untracked.has(file)) {
124
- try {
125
- unlinkSync(join(cwd, file));
126
- }
127
- catch { /* already gone */ }
128
- }
129
- }
130
- }
131
- catch { /* best effort */ }
132
- execFileSync('git', ['rebase', 'runwork/main'], { stdio: 'inherit' });
133
- }
134
- catch {
135
- try {
136
- execFileSync('git', ['rebase', '--abort'], { stdio: 'pipe' });
137
- }
138
- catch { /* no rebase in progress */ }
139
- console.warn('Pull failed (remote may not have commits yet). Continuing...');
140
- }
141
- if (dirty) {
142
- try {
143
- execFileSync('git', ['stash', 'pop'], { stdio: 'inherit' });
144
- }
145
- catch {
87
+ console.log(dim('Syncing...'));
88
+ const syncResult = syncWithRemote(cwd);
89
+ switch (syncResult.status) {
90
+ case 'skipped':
91
+ console.log(dim(' No commits yet. Skipping sync.'));
92
+ break;
93
+ case 'synced':
94
+ console.log(green(' Synced with Runwork.'));
95
+ break;
96
+ case 'merged':
97
+ console.log(yellow(' Merged with Runwork (histories diverged).'));
98
+ break;
99
+ case 'sync-failed':
100
+ if (syncResult.error === 'stash-conflict') {
146
101
  console.error('Merge conflicts detected after stash pop. Resolve conflicts and restart.');
147
102
  process.exit(1);
148
103
  }
149
- }
150
- // Push local changes
151
- try {
152
- execFileSync('git', ['push', 'runwork', 'main'], { stdio: 'inherit' });
153
- }
154
- catch {
155
- console.warn('Push failed. Continuing with current state...');
156
- }
104
+ console.warn(yellow(`Sync failed. Continuing...`));
105
+ if (syncResult.error) {
106
+ console.warn(dim(` ${syncResult.error}`));
107
+ }
108
+ break;
109
+ }
110
+ if (!syncResult.pushed && syncResult.status !== 'skipped' && syncResult.status !== 'sync-failed') {
111
+ console.warn(yellow('Push failed. Continuing with current state...'));
157
112
  }
158
- else {
159
- console.log('No commits yet. Skipping sync.');
113
+ // Show banner
114
+ console.log(getDevBanner({ appName: config.appName, previewUrl: session.previewUrl, workspaceName: config.workspaceName }));
115
+ console.log(getKeyboardHints());
116
+ console.log('');
117
+ console.log(dim(' ─────────────────────────────────────────────'));
118
+ console.log('');
119
+ // Status line tracks sync activity
120
+ const statusLine = createStatusLine(process.stdout);
121
+ let syncedCount = 0;
122
+ let logFilter = 'all';
123
+ function updateStatus() {
124
+ const filterLabel = logFilter === 'all' ? 'All' : logFilter === 'events' ? 'Events' : 'Runtime';
125
+ const sep = '\x1b[90m\u2502\x1b[39m'; // dim │
126
+ 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`);
160
127
  }
128
+ updateStatus();
161
129
  // Declare logTailer before cleanup so cleanup can reference it
162
130
  let logTailer;
131
+ const keyboard = createKeyboardListener();
163
132
  // Register cleanup before starting watcher to avoid race
164
133
  const cleanup = async () => {
165
134
  console.log('\nStopping...');
166
135
  logTailer?.stop();
167
136
  await stopAutoCommit();
137
+ statusLine.destroy();
138
+ keyboard.stop(); // Last - stopping stdin can affect process exit
168
139
  process.exit(0);
169
140
  };
170
141
  process.on('SIGINT', cleanup);
171
142
  process.on('SIGTERM', cleanup);
143
+ keyboard.start((action) => {
144
+ switch (action) {
145
+ case 'o':
146
+ case 'p':
147
+ import('open').then(m => m.default(session.previewUrl));
148
+ break;
149
+ case 'a':
150
+ logFilter = 'all';
151
+ console.log(dim(' Showing all logs.'));
152
+ updateStatus();
153
+ break;
154
+ case 'e':
155
+ logFilter = 'events';
156
+ console.log(dim(' Showing events only.'));
157
+ updateStatus();
158
+ break;
159
+ case 'r':
160
+ logFilter = 'runtime';
161
+ console.log(dim(' Showing runtime logs only.'));
162
+ updateStatus();
163
+ break;
164
+ case 'i':
165
+ console.log(getInfoPanel({
166
+ appName: config.appName,
167
+ previewUrl: session.previewUrl,
168
+ workspaceName: config.workspaceName,
169
+ directory: cwd,
170
+ }));
171
+ break;
172
+ case 'quit':
173
+ cleanup();
174
+ break;
175
+ }
176
+ });
172
177
  // Start file watcher
173
- console.log('Watching for file changes...');
174
- await watchAndAutoCommit(process.cwd(), client, config.appId);
178
+ await watchAndAutoCommit(process.cwd(), client, config.appId, {
179
+ onFastSync(count) {
180
+ syncedCount += count;
181
+ updateStatus();
182
+ },
183
+ onGitPush(count) {
184
+ syncedCount += count;
185
+ updateStatus();
186
+ },
187
+ });
175
188
  // Start log tailing (unless --no-logs)
176
- if (options.logs !== false) {
189
+ if (options?.logs !== false) {
177
190
  logTailer = startLogTailer({
178
191
  appId: config.appId,
179
192
  client,
180
193
  projectDir: cwd,
181
- toTerminal: !options.logsOnlyFile,
194
+ toTerminal: !options?.logsOnlyFile,
182
195
  toFile: true,
196
+ getFilter: () => logFilter,
183
197
  });
184
- console.log('Log tailing active. Logs written to .runwork/logs.txt');
185
198
  }
186
199
  // Keep alive
187
200
  await new Promise(() => { });
188
- });
201
+ }
202
+ export const devCommand = new Command('dev')
203
+ .description('Start local development server with live sync')
204
+ .option('--no-logs', 'Disable automatic log tailing')
205
+ .option('--logs-only-file', 'Write logs to file only, not terminal')
206
+ .action(async (options) => execDev(options));
@@ -1,2 +1,5 @@
1
1
  import { Command } from 'commander';
2
+ import { ApiClient } from '../api/client.js';
3
+ import type { WorkspaceInfo } from '../types.js';
4
+ export declare function execInit(client: ApiClient, appName: string, workspace: WorkspaceInfo): Promise<string>;
2
5
  export declare const initCommand: Command;
@@ -1,32 +1,15 @@
1
1
  import { Command } from 'commander';
2
2
  import { execFileSync } from 'child_process';
3
3
  import { writeFileSync, existsSync, mkdirSync } 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, promptInput } 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 initCommand = new Command('init')
12
- .description('Initialize a new Runwork app')
13
- .argument('[name]', 'App name')
14
- .action(async (name) => {
15
- const creds = requireAuth();
16
- const client = new ApiClient(creds);
17
- const appName = name || await promptInput('App name');
18
- if (!appName) {
19
- console.error('App name is required.');
20
- process.exit(1);
21
- }
22
- const workspaces = await client.listWorkspaces();
23
- if (workspaces.length === 0) {
24
- console.error('No workspaces found. Create one at runwork.ai first.');
25
- process.exit(1);
26
- }
27
- const workspaceChoice = await promptSelect('Select workspace:', workspaces.map(w => ({ label: w.name, value: w })));
28
- const workspace = workspaceChoice.value;
29
- console.log(`Initializing "${appName}" in workspace "${workspace.name}"...`);
11
+ import { runAgentWizard } from '../ui/banner.js';
12
+ export async function execInit(client, appName, workspace) {
30
13
  const app = await client.initApp(workspace.id, appName);
31
14
  const slug = app.slug || appName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
32
15
  const dir = slug;
@@ -52,6 +35,7 @@ export const initCommand = new Command('init')
52
35
  // Write .runwork.json AFTER template extraction so it doesn't get overwritten
53
36
  const config = {
54
37
  workspaceId: workspace.id,
38
+ workspaceName: workspace.name,
55
39
  appId: app.id,
56
40
  appName: app.name,
57
41
  };
@@ -83,5 +67,29 @@ export const initCommand = new Command('init')
83
67
  execFileSync('git', ['push', '-u', 'runwork', 'main'], { cwd: dir, stdio: 'inherit' });
84
68
  console.log(`\nApp "${app.name}" initialized in ${dir}/`);
85
69
  console.log(`Remote: ${remoteUrl}`);
86
- console.log(`\nNext: cd ${dir} && runwork dev`);
70
+ return resolve(dir);
71
+ }
72
+ export const initCommand = new Command('init')
73
+ .description('Initialize a new Runwork app')
74
+ .argument('[name]', 'App name')
75
+ .action(async (name) => {
76
+ const creds = requireAuth();
77
+ const client = new ApiClient(creds);
78
+ const appName = name || await promptInput('App name');
79
+ if (!appName) {
80
+ console.error('App name is required.');
81
+ process.exit(1);
82
+ }
83
+ const workspaces = await client.listWorkspaces();
84
+ if (workspaces.length === 0) {
85
+ console.error('No workspaces found. Create one at runwork.ai first.');
86
+ process.exit(1);
87
+ }
88
+ const workspaceChoice = await promptSelect('Select workspace:', workspaces.map(w => ({ label: w.name, value: w })));
89
+ const workspace = workspaceChoice.value;
90
+ console.log(`Initializing "${appName}" in workspace "${workspace.name}"...`);
91
+ const dir = await execInit(client, appName, workspace);
92
+ const slug = dir.split('/').pop() || dir;
93
+ await runAgentWizard(dir);
94
+ console.log(`Next: cd ${slug} && runwork dev`);
87
95
  });
@@ -1,32 +1,17 @@
1
1
  import { Command } from 'commander';
2
- import { ApiClient } from '../api/client.js';
3
- import { saveCredentials } from '../auth/store.js';
4
- import { configureGitCredentials } from '../git/credentials.js';
2
+ import { performLogin } from '../auth/login-flow.js';
3
+ import { bold, cyan } from '../ui/colors.js';
5
4
  export const loginCommand = new Command('login')
6
5
  .description('Authenticate with Runwork platform')
7
6
  .option('--base-url <url>', 'Platform URL', 'https://runwork.ai')
8
7
  .action(async (options) => {
9
- const client = new ApiClient({ apiKey: '', email: '', baseUrl: options.baseUrl });
10
8
  console.log('Opening browser for authentication...');
11
- const { sessionId, loginUrl } = await client.initiateLogin();
12
- // Open browser
13
- const open = await import('open');
14
- await open.default(loginUrl);
15
- console.log(`If browser didn't open, visit: ${loginUrl}`);
16
- console.log('Waiting for authentication...');
17
- // Poll for up to 5 minutes
18
- const maxAttempts = 60;
19
- const pollInterval = 5000;
20
- for (let i = 0; i < maxAttempts; i++) {
21
- await new Promise(resolve => setTimeout(resolve, pollInterval));
22
- const result = await client.pollLogin(sessionId);
23
- if (result) {
24
- saveCredentials(result);
25
- await configureGitCredentials(result.baseUrl || options.baseUrl);
26
- console.log(`Logged in as ${result.email}`);
27
- return;
28
- }
29
- }
30
- console.error('Login timed out. Please try again.');
31
- process.exit(1);
9
+ await performLogin(options.baseUrl);
10
+ console.log('');
11
+ console.log(bold('What next?'));
12
+ console.log('');
13
+ console.log(` ${cyan('runwork init')} Create a new app`);
14
+ console.log(` ${cyan('runwork clone')} Clone an existing app`);
15
+ console.log(` ${cyan('runwork dev')} Start developing (inside an app directory)`);
16
+ console.log('');
32
17
  });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const openCommand: Command;
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { requireAuth } from '../auth/store.js';
4
+ import { ApiClient } from '../api/client.js';
5
+ import { cyan } from '../ui/colors.js';
6
+ export const openCommand = new Command('open')
7
+ .description('Open app preview or dashboard in browser')
8
+ .argument('[target]', 'What to open: preview (default), dashboard', 'preview')
9
+ .action(async (target) => {
10
+ if (!existsSync('.runwork.json')) {
11
+ console.error('No .runwork.json found. Run `runwork init` first.');
12
+ process.exit(1);
13
+ }
14
+ const config = JSON.parse(readFileSync('.runwork.json', 'utf-8'));
15
+ const creds = requireAuth();
16
+ const client = new ApiClient(creds);
17
+ const open = await import('open');
18
+ switch (target) {
19
+ case 'preview': {
20
+ let previewUrl;
21
+ try {
22
+ const status = await client.getDevStatus(config.appId);
23
+ previewUrl = status.previewUrl;
24
+ }
25
+ catch {
26
+ const session = await client.startDevSession(config.appId);
27
+ previewUrl = session.previewUrl;
28
+ }
29
+ console.log(`Opening preview: ${cyan(previewUrl)}`);
30
+ await open.default(previewUrl);
31
+ break;
32
+ }
33
+ case 'dashboard': {
34
+ const url = `${creds.baseUrl}/apps/${config.appId}`;
35
+ console.log(`Opening dashboard: ${cyan(url)}`);
36
+ await open.default(url);
37
+ break;
38
+ }
39
+ default:
40
+ console.error(`Unknown target: ${target}. Use "preview" or "dashboard".`);
41
+ process.exit(1);
42
+ }
43
+ });
@@ -0,0 +1 @@
1
+ export declare function runWelcomeWizard(): Promise<void>;