runwork 0.2.5 → 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.
- package/dist/auth/login-flow.d.ts +10 -0
- package/dist/auth/login-flow.js +37 -0
- package/dist/commands/clone.d.ts +3 -0
- package/dist/commands/clone.js +32 -24
- package/dist/commands/dev.d.ts +4 -0
- package/dist/commands/dev.js +89 -21
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +29 -21
- package/dist/commands/login.js +10 -25
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +43 -0
- package/dist/commands/welcome.d.ts +1 -0
- package/dist/commands/welcome.js +83 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/git/__tests__/sync.test.js +10 -8
- package/dist/git/auto-commit.d.ts +5 -1
- package/dist/git/auto-commit.js +15 -9
- package/dist/git/sync.js +46 -5
- package/dist/index.js +16 -0
- package/dist/logs/__tests__/tailer-format.test.d.ts +1 -0
- package/dist/logs/__tests__/tailer-format.test.js +43 -0
- package/dist/logs/tailer.d.ts +3 -0
- package/dist/logs/tailer.js +47 -10
- package/dist/types.d.ts +1 -0
- package/dist/ui/__tests__/banner.test.d.ts +1 -0
- package/dist/ui/__tests__/banner.test.js +82 -0
- package/dist/ui/__tests__/colors.test.d.ts +1 -0
- package/dist/ui/__tests__/colors.test.js +22 -0
- package/dist/ui/__tests__/keyboard.test.d.ts +1 -0
- package/dist/ui/__tests__/keyboard.test.js +30 -0
- package/dist/ui/__tests__/status-line.test.d.ts +1 -0
- package/dist/ui/__tests__/status-line.test.js +54 -0
- package/dist/ui/banner.d.ts +29 -0
- package/dist/ui/banner.js +118 -0
- package/dist/ui/colors.d.ts +4 -0
- package/dist/ui/colors.js +7 -0
- package/dist/ui/keyboard.d.ts +12 -0
- package/dist/ui/keyboard.js +57 -0
- package/dist/ui/status-line.d.ts +6 -0
- package/dist/ui/status-line.js +53 -0
- package/dist/utils/__tests__/prompt.test.js +23 -99
- package/dist/utils/prompt.d.ts +1 -0
- package/dist/utils/prompt.js +29 -21
- 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
|
+
}
|
package/dist/commands/clone.d.ts
CHANGED
|
@@ -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;
|
package/dist/commands/clone.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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: ${
|
|
89
|
-
|
|
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
|
});
|
package/dist/commands/dev.d.ts
CHANGED
package/dist/commands/dev.js
CHANGED
|
@@ -11,6 +11,10 @@ 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';
|
|
14
18
|
async function populateSkill(projectDir, client, appId) {
|
|
15
19
|
try {
|
|
16
20
|
const { skill } = await client.getAppSkill(appId);
|
|
@@ -29,11 +33,7 @@ function readConfig() {
|
|
|
29
33
|
}
|
|
30
34
|
return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
|
|
31
35
|
}
|
|
32
|
-
export
|
|
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) => {
|
|
36
|
+
export async function execDev(options) {
|
|
37
37
|
const config = readConfig();
|
|
38
38
|
const creds = requireAuth();
|
|
39
39
|
const client = new ApiClient(creds);
|
|
@@ -57,7 +57,7 @@ export const devCommand = new Command('dev')
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
// Download fresh template to ensure latest version
|
|
60
|
-
console.log('Updating template...');
|
|
60
|
+
console.log(dim('Updating template...'));
|
|
61
61
|
try {
|
|
62
62
|
const zipData = await client.downloadSkeleton();
|
|
63
63
|
extractZip(zipData, cwd);
|
|
@@ -74,65 +74,133 @@ export const devCommand = new Command('dev')
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
catch {
|
|
77
|
-
console.warn('Template update failed. Continuing with current files.');
|
|
77
|
+
console.warn(yellow('Template update failed. Continuing with current files.'));
|
|
78
78
|
}
|
|
79
79
|
// Populate type definitions
|
|
80
80
|
await populateTypes(cwd);
|
|
81
81
|
// Start dev session first - this triggers DO onStart which may create remote commits
|
|
82
|
-
console.log('Starting dev session...');
|
|
82
|
+
console.log(dim('Starting dev session...'));
|
|
83
83
|
const session = await client.startDevSession(config.appId);
|
|
84
|
-
console.log(`Preview: ${session.previewUrl}`);
|
|
85
84
|
// Fetch SKILL.md (best-effort, after session so DO registries are available)
|
|
86
85
|
await populateSkill(cwd, client, config.appId);
|
|
87
86
|
// Sync AFTER starting session so we pick up any commits the DO created
|
|
88
|
-
console.log('Syncing
|
|
87
|
+
console.log(dim('Syncing...'));
|
|
89
88
|
const syncResult = syncWithRemote(cwd);
|
|
90
89
|
switch (syncResult.status) {
|
|
91
90
|
case 'skipped':
|
|
92
|
-
console.log('No commits yet. Skipping sync.');
|
|
91
|
+
console.log(dim(' No commits yet. Skipping sync.'));
|
|
93
92
|
break;
|
|
94
93
|
case 'synced':
|
|
95
|
-
console.log('Synced with Runwork.');
|
|
94
|
+
console.log(green(' Synced with Runwork.'));
|
|
96
95
|
break;
|
|
97
96
|
case 'merged':
|
|
98
|
-
console.log('Merged with Runwork (histories diverged).');
|
|
97
|
+
console.log(yellow(' Merged with Runwork (histories diverged).'));
|
|
99
98
|
break;
|
|
100
99
|
case 'sync-failed':
|
|
101
100
|
if (syncResult.error === 'stash-conflict') {
|
|
102
101
|
console.error('Merge conflicts detected after stash pop. Resolve conflicts and restart.');
|
|
103
102
|
process.exit(1);
|
|
104
103
|
}
|
|
105
|
-
console.warn(
|
|
104
|
+
console.warn(yellow(`Sync failed. Continuing...`));
|
|
105
|
+
if (syncResult.error) {
|
|
106
|
+
console.warn(dim(` ${syncResult.error}`));
|
|
107
|
+
}
|
|
106
108
|
break;
|
|
107
109
|
}
|
|
108
110
|
if (!syncResult.pushed && syncResult.status !== 'skipped' && syncResult.status !== 'sync-failed') {
|
|
109
|
-
console.warn('Push failed. Continuing with current state...');
|
|
111
|
+
console.warn(yellow('Push failed. Continuing with current state...'));
|
|
112
|
+
}
|
|
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`);
|
|
110
127
|
}
|
|
128
|
+
updateStatus();
|
|
111
129
|
// Declare logTailer before cleanup so cleanup can reference it
|
|
112
130
|
let logTailer;
|
|
131
|
+
const keyboard = createKeyboardListener();
|
|
113
132
|
// Register cleanup before starting watcher to avoid race
|
|
114
133
|
const cleanup = async () => {
|
|
115
134
|
console.log('\nStopping...');
|
|
116
135
|
logTailer?.stop();
|
|
117
136
|
await stopAutoCommit();
|
|
137
|
+
statusLine.destroy();
|
|
138
|
+
keyboard.stop(); // Last - stopping stdin can affect process exit
|
|
118
139
|
process.exit(0);
|
|
119
140
|
};
|
|
120
141
|
process.on('SIGINT', cleanup);
|
|
121
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
|
+
});
|
|
122
177
|
// Start file watcher
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
});
|
|
125
188
|
// Start log tailing (unless --no-logs)
|
|
126
|
-
if (options
|
|
189
|
+
if (options?.logs !== false) {
|
|
127
190
|
logTailer = startLogTailer({
|
|
128
191
|
appId: config.appId,
|
|
129
192
|
client,
|
|
130
193
|
projectDir: cwd,
|
|
131
|
-
toTerminal: !options
|
|
194
|
+
toTerminal: !options?.logsOnlyFile,
|
|
132
195
|
toFile: true,
|
|
196
|
+
getFilter: () => logFilter,
|
|
133
197
|
});
|
|
134
|
-
console.log('Log tailing active. Logs written to .runwork/logs.txt');
|
|
135
198
|
}
|
|
136
199
|
// Keep alive
|
|
137
200
|
await new Promise(() => { });
|
|
138
|
-
}
|
|
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));
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -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;
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/commands/login.js
CHANGED
|
@@ -1,32 +1,17 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
console.log(`
|
|
16
|
-
console.log('
|
|
17
|
-
|
|
18
|
-
|
|
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,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>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { getCredentials } from '../auth/store.js';
|
|
2
|
+
import { performLogin } from '../auth/login-flow.js';
|
|
3
|
+
import { ApiClient } from '../api/client.js';
|
|
4
|
+
import { promptSelect, promptInput, promptConfirm } from '../utils/prompt.js';
|
|
5
|
+
import { getWelcomeBanner, runAgentWizard } from '../ui/banner.js';
|
|
6
|
+
import { cyan, dim, green } from '../ui/colors.js';
|
|
7
|
+
export async function runWelcomeWizard() {
|
|
8
|
+
// Step 1: Welcome
|
|
9
|
+
console.log(getWelcomeBanner());
|
|
10
|
+
// Step 2: Login
|
|
11
|
+
let creds = getCredentials();
|
|
12
|
+
if (creds) {
|
|
13
|
+
console.log(green(` Already logged in as ${creds.email}.`));
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log('Opening your browser to log in...');
|
|
17
|
+
console.log('');
|
|
18
|
+
creds = await performLogin();
|
|
19
|
+
}
|
|
20
|
+
// Step 3: Init or Clone
|
|
21
|
+
console.log('');
|
|
22
|
+
const action = await promptSelect('What would you like to do?', [
|
|
23
|
+
{ label: 'Create a new app', value: 'init' },
|
|
24
|
+
{ label: 'Clone an existing app', value: 'clone' },
|
|
25
|
+
]);
|
|
26
|
+
const client = new ApiClient(creds);
|
|
27
|
+
let appDir;
|
|
28
|
+
if (action.value === 'init') {
|
|
29
|
+
const appName = await promptInput('App name');
|
|
30
|
+
if (!appName) {
|
|
31
|
+
console.error('App name is required.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const workspaces = await client.listWorkspaces();
|
|
35
|
+
if (workspaces.length === 0) {
|
|
36
|
+
console.error('No workspaces found. Create one at runwork.ai first.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
let workspace;
|
|
40
|
+
if (workspaces.length === 1) {
|
|
41
|
+
workspace = workspaces[0];
|
|
42
|
+
console.log(dim(` Using workspace: ${workspace.name}`));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log('A workspace is where your apps live. Pick one:');
|
|
47
|
+
const choice = await promptSelect('Select workspace:', workspaces.map(w => ({ label: w.name, value: w })));
|
|
48
|
+
workspace = choice.value;
|
|
49
|
+
}
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(`Creating ${cyan(appName)} in workspace ${cyan(workspace.name)}...`);
|
|
52
|
+
const { execInit } = await import('./init.js');
|
|
53
|
+
appDir = await execInit(client, appName, workspace);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const apps = await client.listApps();
|
|
57
|
+
if (apps.length === 0) {
|
|
58
|
+
console.error('No apps found. Choose "Create a new app" instead.');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const choice = await promptSelect('Select app to clone:', apps.map(a => ({ label: `${a.name} (${a.workspaceName})`, value: a })));
|
|
62
|
+
const { execClone } = await import('./clone.js');
|
|
63
|
+
appDir = await execClone(client, choice.value);
|
|
64
|
+
}
|
|
65
|
+
// Step 4: Agent guidance
|
|
66
|
+
await runAgentWizard(appDir);
|
|
67
|
+
// Step 5: Offer to start dev
|
|
68
|
+
console.log('');
|
|
69
|
+
const startDev = await promptConfirm('Start developing now?');
|
|
70
|
+
if (startDev) {
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log(dim(` Tip: Open another terminal and launch your AI agent in ${cyan(appDir)}`));
|
|
73
|
+
console.log('');
|
|
74
|
+
process.chdir(appDir);
|
|
75
|
+
const { execDev } = await import('./dev.js');
|
|
76
|
+
await execDev();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const slug = appDir.split('/').pop() || appDir;
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log(`Next: ${cyan(`cd ${slug} && runwork dev`)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.3.0";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-types.ts -- do not edit
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.3.0";
|
|
@@ -165,7 +165,7 @@ describe('syncWithRemote()', () => {
|
|
|
165
165
|
expect(existsSync(join(local, 'local-file.txt'))).toBe(true);
|
|
166
166
|
expect(existsSync(join(local, 'remote-only.txt'))).toBe(true);
|
|
167
167
|
});
|
|
168
|
-
it('
|
|
168
|
+
it('auto-resolves conflicting unrelated histories by accepting remote', () => {
|
|
169
169
|
const { local, remote } = createTestRepo();
|
|
170
170
|
// Local modifies a file
|
|
171
171
|
commitFile(local, 'shared.txt', 'local-line-1\nlocal-line-2\n', 'local init');
|
|
@@ -178,8 +178,10 @@ describe('syncWithRemote()', () => {
|
|
|
178
178
|
execFileSync('git', ['remote', 'add', 'origin', remote], { cwd: other });
|
|
179
179
|
execFileSync('git', ['push', 'origin', 'main'], { cwd: other, stdio: 'pipe' });
|
|
180
180
|
const result = syncWithRemote(local);
|
|
181
|
-
//
|
|
182
|
-
expect(result.status).toBe('
|
|
181
|
+
// Conflicts auto-resolved with -X theirs (remote wins for first sync)
|
|
182
|
+
expect(result.status).toBe('merged');
|
|
183
|
+
// Remote version should win
|
|
184
|
+
expect(readFileSync(join(local, 'shared.txt'), 'utf-8')).toBe('remote-line-1\nremote-line-2\n');
|
|
183
185
|
});
|
|
184
186
|
it('stashes and restores dirty working tree during sync', () => {
|
|
185
187
|
const { local, remote } = createTestRepo();
|
|
@@ -316,7 +318,7 @@ describe('syncWithRemote() edge cases', () => {
|
|
|
316
318
|
// But local repo should be intact
|
|
317
319
|
expect(readFileSync(join(local, 'a.txt'), 'utf-8')).toBe('hello');
|
|
318
320
|
});
|
|
319
|
-
it('leaves repo clean
|
|
321
|
+
it('leaves repo clean after auto-resolving conflicting unrelated histories', () => {
|
|
320
322
|
const { local, remote } = createTestRepo();
|
|
321
323
|
commitFile(local, 'shared.txt', 'local version\n', 'local init');
|
|
322
324
|
// Create conflicting remote history
|
|
@@ -328,14 +330,14 @@ describe('syncWithRemote() edge cases', () => {
|
|
|
328
330
|
execFileSync('git', ['remote', 'add', 'origin', remote], { cwd: other });
|
|
329
331
|
execFileSync('git', ['push', 'origin', 'main'], { cwd: other, stdio: 'pipe' });
|
|
330
332
|
const result = syncWithRemote(local);
|
|
331
|
-
|
|
333
|
+
// Auto-resolved with -X theirs
|
|
334
|
+
expect(result.status).toBe('merged');
|
|
332
335
|
// Repo should NOT be in a rebase or merge state
|
|
333
336
|
expect(existsSync(join(local, '.git', 'MERGE_HEAD'))).toBe(false);
|
|
334
337
|
expect(existsSync(join(local, '.git', 'rebase-merge'))).toBe(false);
|
|
335
338
|
expect(existsSync(join(local, '.git', 'rebase-apply'))).toBe(false);
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
expect(fileContent).toBe('local version\n');
|
|
339
|
+
// Remote version wins
|
|
340
|
+
expect(readFileSync(join(local, 'shared.txt'), 'utf-8')).toBe('remote version\n');
|
|
339
341
|
});
|
|
340
342
|
it('handles merge conflict during rebase on related histories (same file edited both sides)', () => {
|
|
341
343
|
const { local, remote } = createTestRepo();
|