runwork 0.3.0 → 0.4.1
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/api/client.d.ts +2 -1
- package/dist/api/client.js +4 -0
- package/dist/commands/deploy.js +24 -7
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +134 -96
- package/dist/commands/info.d.ts +2 -0
- package/dist/commands/info.js +432 -0
- package/dist/commands/integrations.js +19 -2
- package/dist/commands/logs.js +82 -23
- package/dist/commands/open.js +11 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.js +6 -1
- package/dist/logs/tailer.d.ts +1 -0
- package/dist/logs/tailer.js +16 -5
- package/dist/types.d.ts +58 -0
- package/dist/utils/__tests__/output.test.d.ts +1 -0
- package/dist/utils/__tests__/output.test.js +38 -0
- package/dist/utils/output.d.ts +17 -0
- package/dist/utils/output.js +27 -0
- package/package.json +1 -1
package/dist/api/client.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/api/client.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/commands/dev.d.ts
CHANGED
package/dist/commands/dev.js
CHANGED
|
@@ -15,6 +15,7 @@ import { getDevBanner, getKeyboardHints, getInfoPanel } from '../ui/banner.js';
|
|
|
15
15
|
import { createStatusLine } from '../ui/status-line.js';
|
|
16
16
|
import { createKeyboardListener } from '../ui/keyboard.js';
|
|
17
17
|
import { bold, dim, green, yellow, cyan } from '../ui/colors.js';
|
|
18
|
+
import { shouldOutputJson, jsonLine } from '../utils/output.js';
|
|
18
19
|
async function populateSkill(projectDir, client, appId) {
|
|
19
20
|
try {
|
|
20
21
|
const { skill } = await client.getAppSkill(appId);
|
|
@@ -34,18 +35,25 @@ function readConfig() {
|
|
|
34
35
|
return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
|
|
35
36
|
}
|
|
36
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 async function execDev(options) {
|
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
// Download fresh template to ensure latest version
|
|
60
|
-
|
|
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,116 +85,139 @@ export async function execDev(options) {
|
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
catch {
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
// Fetch SKILL.md (best-effort
|
|
103
|
+
// Fetch SKILL.md (best-effort)
|
|
85
104
|
await populateSkill(cwd, client, config.appId);
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
if (useJson)
|
|
106
|
+
jsonLine({ event: 'startup', phase: 'skill_fetched', timestamp: ts() });
|
|
107
|
+
// Sync
|
|
108
|
+
if (!useJson)
|
|
109
|
+
console.log(dim('Syncing...'));
|
|
88
110
|
const syncResult = syncWithRemote(cwd);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
console.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
if (useJson) {
|
|
112
|
+
jsonLine({ event: 'startup', phase: 'sync', status: syncResult.status, pushed: syncResult.pushed, timestamp: ts() });
|
|
113
|
+
}
|
|
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
|
+
}
|
|
139
|
+
}
|
|
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);
|
|
121
153
|
let syncedCount = 0;
|
|
122
154
|
let logFilter = 'all';
|
|
123
155
|
function updateStatus() {
|
|
156
|
+
if (!statusLine)
|
|
157
|
+
return;
|
|
124
158
|
const filterLabel = logFilter === 'all' ? 'All' : logFilter === 'events' ? 'Events' : 'Runtime';
|
|
125
|
-
const sep = '\x1b[90m\u2502\x1b[39m';
|
|
159
|
+
const sep = '\x1b[90m\u2502\x1b[39m';
|
|
126
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`);
|
|
127
161
|
}
|
|
128
|
-
|
|
129
|
-
|
|
162
|
+
if (statusLine)
|
|
163
|
+
updateStatus();
|
|
130
164
|
let logTailer;
|
|
131
|
-
const keyboard = createKeyboardListener();
|
|
132
|
-
// Register cleanup before starting watcher to avoid race
|
|
165
|
+
const keyboard = useJson ? null : createKeyboardListener();
|
|
133
166
|
const cleanup = async () => {
|
|
134
|
-
|
|
167
|
+
if (!useJson)
|
|
168
|
+
console.log('\nStopping...');
|
|
135
169
|
logTailer?.stop();
|
|
136
170
|
await stopAutoCommit();
|
|
137
|
-
statusLine
|
|
138
|
-
keyboard
|
|
171
|
+
statusLine?.destroy();
|
|
172
|
+
keyboard?.stop();
|
|
139
173
|
process.exit(0);
|
|
140
174
|
};
|
|
141
175
|
process.on('SIGINT', cleanup);
|
|
142
176
|
process.on('SIGTERM', cleanup);
|
|
143
|
-
keyboard
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
}
|
|
177
213
|
// Start file watcher
|
|
178
214
|
await watchAndAutoCommit(process.cwd(), client, config.appId, {
|
|
179
|
-
onFastSync
|
|
180
|
-
|
|
181
|
-
updateStatus();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
syncedCount += count;
|
|
185
|
-
updateStatus();
|
|
186
|
-
},
|
|
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(); },
|
|
187
221
|
});
|
|
188
222
|
// Start log tailing (unless --no-logs)
|
|
189
223
|
if (options?.logs !== false) {
|
|
@@ -194,13 +228,17 @@ export async function execDev(options) {
|
|
|
194
228
|
toTerminal: !options?.logsOnlyFile,
|
|
195
229
|
toFile: true,
|
|
196
230
|
getFilter: () => logFilter,
|
|
231
|
+
useJson,
|
|
197
232
|
});
|
|
198
233
|
}
|
|
199
234
|
// Keep alive
|
|
200
235
|
await new Promise(() => { });
|
|
201
236
|
}
|
|
202
237
|
export const devCommand = new Command('dev')
|
|
203
|
-
.description('Start local development
|
|
238
|
+
.description('Start local development with live sync, preview sandbox, and file watching')
|
|
204
239
|
.option('--no-logs', 'Disable automatic log tailing')
|
|
205
240
|
.option('--logs-only-file', 'Write logs to file only, not terminal')
|
|
206
|
-
.action(async (options) =>
|
|
241
|
+
.action(async (options, command) => {
|
|
242
|
+
const globalJson = shouldOutputJson(command.optsWithGlobals().json);
|
|
243
|
+
await execDev({ ...options, json: globalJson });
|
|
244
|
+
});
|