runwork 0.13.3 → 0.14.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/bundled-types/core-endpoints.d.ts +16 -4
- package/bundled-types/core-workflow-instance.d.ts +6 -0
- package/bundled-types/workflows.d.ts +1 -0
- package/dist/agents/claude-desktop-plugin-tree.js +23 -8
- package/dist/agents/claude-desktop.js +6 -3
- package/dist/agents/codex.js +15 -7
- package/dist/api/client.d.ts +5 -0
- package/dist/api/client.js +8 -0
- package/dist/commands/__tests__/mcp-entries.test.d.ts +1 -0
- package/dist/commands/__tests__/mcp-entries.test.js +48 -0
- package/dist/commands/deploy.js +101 -22
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +145 -155
- package/dist/commands/logs.js +10 -3
- package/dist/commands/mcp-entries.d.ts +13 -0
- package/dist/commands/mcp-entries.js +26 -0
- package/dist/commands/sync.js +6 -10
- package/dist/dev/__tests__/session.test.js +60 -0
- package/dist/dev/session.d.ts +8 -0
- package/dist/dev/session.js +4 -1
- package/dist/generated/bundled-types.js +3 -3
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/git/__tests__/auto-commit.test.js +59 -1
- package/dist/git/__tests__/deploy-guard.test.d.ts +1 -0
- package/dist/git/__tests__/deploy-guard.test.js +61 -0
- package/dist/git/auto-commit.d.ts +22 -0
- package/dist/git/auto-commit.js +72 -11
- package/dist/git/critical-files.d.ts +20 -0
- package/dist/git/critical-files.js +68 -0
- package/dist/git/deploy-guard.d.ts +53 -0
- package/dist/git/deploy-guard.js +78 -0
- package/dist/types.d.ts +3 -0
- package/dist/utils/agent-guidance.d.ts +2 -0
- package/dist/utils/ignore-matcher.d.ts +9 -0
- package/dist/utils/ignore-matcher.js +16 -0
- package/package.json +1 -1
package/dist/commands/dev.js
CHANGED
|
@@ -6,6 +6,7 @@ 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
8
|
import { syncWithRemote } from '../git/sync.js';
|
|
9
|
+
import { snapshotCriticalFiles, restoreMissingCriticalFiles, commitAndPushRestoredFiles } from '../git/critical-files.js';
|
|
9
10
|
import { ensureGitIdentity } from '../git/identity.js';
|
|
10
11
|
import { requireGit } from '../git/preflight.js';
|
|
11
12
|
import { startLogTailer } from '../logs/tailer.js';
|
|
@@ -43,69 +44,14 @@ function readConfig() {
|
|
|
43
44
|
}
|
|
44
45
|
return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
|
|
45
46
|
}
|
|
46
|
-
// Files the CLI cannot function without and which the user did not author.
|
|
47
|
-
// `.runwork.json` is the local app identity; `blueprint.json` is the app's
|
|
48
|
-
// canonical feature definition; `.gitignore` keeps caches out of git. If a
|
|
49
|
-
// sync from the remote silently removes any of these (which has happened
|
|
50
|
-
// when a server-side agent commits with a stale index — see
|
|
51
|
-
// worker/agents/git/git.ts), restoring from a pre-sync snapshot keeps the
|
|
52
|
-
// project usable and unblocks `runwork deploy`.
|
|
53
|
-
const CRITICAL_FILES = ['.runwork.json', 'blueprint.json', '.gitignore'];
|
|
54
|
-
function snapshotCriticalFiles(cwd) {
|
|
55
|
-
const snapshots = [];
|
|
56
|
-
for (const rel of CRITICAL_FILES) {
|
|
57
|
-
const abs = join(cwd, rel);
|
|
58
|
-
if (!existsSync(abs))
|
|
59
|
-
continue;
|
|
60
|
-
try {
|
|
61
|
-
snapshots.push({ path: rel, contents: readFileSync(abs, 'utf-8') });
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// Best-effort: skip unreadable files.
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return snapshots;
|
|
68
|
-
}
|
|
69
|
-
function restoreMissingCriticalFiles(cwd, snapshots) {
|
|
70
|
-
const restored = [];
|
|
71
|
-
for (const snap of snapshots) {
|
|
72
|
-
const abs = join(cwd, snap.path);
|
|
73
|
-
if (existsSync(abs))
|
|
74
|
-
continue;
|
|
75
|
-
try {
|
|
76
|
-
writeFileSync(abs, snap.contents, 'utf-8');
|
|
77
|
-
restored.push(snap.path);
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
// Best-effort: skip files we cannot write back.
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return restored;
|
|
84
|
-
}
|
|
85
|
-
function commitAndPushRestoredFiles(cwd, files) {
|
|
86
|
-
if (files.length === 0)
|
|
87
|
-
return false;
|
|
88
|
-
try {
|
|
89
|
-
execFileSync('git', ['add', '--', ...files], { cwd, stdio: 'pipe' });
|
|
90
|
-
execFileSync('git', ['commit', '-m', 'chore: restore critical files removed by sync'], { cwd, stdio: 'pipe' });
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
// `HEAD:main` so the push works regardless of local branch name.
|
|
97
|
-
execFileSync('git', ['push', 'runwork', 'HEAD:main'], { cwd, stdio: 'pipe' });
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// Push failures are non-fatal: the local copy is restored, and the
|
|
102
|
-
// restoration commit will be pushed with the next auto-sync cycle.
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
47
|
export async function execDev(options) {
|
|
107
48
|
const useJson = options?.json ?? false;
|
|
108
49
|
const mode = options?.mode ?? 'foreground';
|
|
50
|
+
// No-sync mode: hold the preview open but never auto-touch the user's
|
|
51
|
+
// git or working tree. No file watcher, no auto-commit, and no
|
|
52
|
+
// working-tree-mutating startup (template auto-update / syncWithRemote).
|
|
53
|
+
// The user drives git manually; `git push` updates the preview.
|
|
54
|
+
const noSync = options?.noSync ?? false;
|
|
109
55
|
requireGit('dev');
|
|
110
56
|
const config = readConfig();
|
|
111
57
|
const creds = requireAuth();
|
|
@@ -199,30 +145,34 @@ export async function execDev(options) {
|
|
|
199
145
|
}
|
|
200
146
|
}
|
|
201
147
|
}
|
|
202
|
-
// Download fresh template to ensure latest version
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
removeNestedGitDirs(cwd);
|
|
213
|
-
const newManifest = await generateManifest(cwd);
|
|
214
|
-
await saveManifest(cwd, newManifest);
|
|
148
|
+
// Download fresh template to ensure latest version. Skipped entirely in
|
|
149
|
+
// no-sync mode: this block mutates the working tree (`git checkout -- .`),
|
|
150
|
+
// which would fight the user's manual git workflow.
|
|
151
|
+
if (!noSync) {
|
|
152
|
+
if (useJson) {
|
|
153
|
+
jsonLine({ event: 'startup', phase: 'template_update', timestamp: ts() });
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
console.log(dim('Updating template...'));
|
|
157
|
+
}
|
|
215
158
|
try {
|
|
216
|
-
|
|
159
|
+
const zipData = await client.downloadSkeleton();
|
|
160
|
+
extractZip(zipData, cwd);
|
|
161
|
+
removeNestedGitDirs(cwd);
|
|
162
|
+
const newManifest = await generateManifest(cwd);
|
|
163
|
+
await saveManifest(cwd, newManifest);
|
|
164
|
+
try {
|
|
165
|
+
execFileSync('git', ['checkout', '--', '.'], { cwd, stdio: 'pipe' });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// May fail if no commits yet
|
|
169
|
+
}
|
|
217
170
|
}
|
|
218
171
|
catch {
|
|
219
|
-
|
|
172
|
+
if (!useJson)
|
|
173
|
+
console.warn(yellow('Template update failed. Continuing with current files.'));
|
|
220
174
|
}
|
|
221
175
|
}
|
|
222
|
-
catch {
|
|
223
|
-
if (!useJson)
|
|
224
|
-
console.warn(yellow('Template update failed. Continuing with current files.'));
|
|
225
|
-
}
|
|
226
176
|
// Populate type definitions
|
|
227
177
|
await populateTypes(cwd);
|
|
228
178
|
if (useJson)
|
|
@@ -239,89 +189,108 @@ export async function execDev(options) {
|
|
|
239
189
|
await populateSkill(cwd, client, config.appId);
|
|
240
190
|
if (useJson)
|
|
241
191
|
jsonLine({ event: 'startup', phase: 'skill_fetched', timestamp: ts() });
|
|
242
|
-
// Sync
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
192
|
+
// Sync. In no-sync mode we never mutate the working tree: instead of
|
|
193
|
+
// syncWithRemote (rebase/merge/commit/push) we do a best-effort,
|
|
194
|
+
// read-only `git fetch` so remote-tracking refs stay current and
|
|
195
|
+
// `runwork info` can still report ahead/behind accurately.
|
|
196
|
+
if (noSync) {
|
|
197
|
+
try {
|
|
198
|
+
execFileSync('git', ['fetch', 'runwork'], { cwd, stdio: 'pipe' });
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Offline, or the `runwork` remote does not exist yet. Non-fatal.
|
|
202
|
+
}
|
|
250
203
|
if (useJson) {
|
|
251
|
-
jsonLine({
|
|
252
|
-
event: 'sync_restored_critical_files',
|
|
253
|
-
files: restoredCriticalFiles,
|
|
254
|
-
pushed,
|
|
255
|
-
timestamp: ts(),
|
|
256
|
-
});
|
|
204
|
+
jsonLine({ event: 'startup', phase: 'fetch', timestamp: ts() });
|
|
257
205
|
}
|
|
258
206
|
else {
|
|
259
|
-
console.
|
|
260
|
-
if (!pushed) {
|
|
261
|
-
console.warn(dim(' Restoration committed locally but not pushed; will retry on next auto-sync.'));
|
|
262
|
-
}
|
|
207
|
+
console.log(dim('Fetched remote refs (no-sync mode).'));
|
|
263
208
|
}
|
|
264
209
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
210
|
+
else {
|
|
211
|
+
if (!useJson)
|
|
212
|
+
console.log(dim('Syncing...'));
|
|
213
|
+
const criticalSnapshot = snapshotCriticalFiles(cwd);
|
|
214
|
+
const syncResult = syncWithRemote(cwd);
|
|
215
|
+
const restoredCriticalFiles = restoreMissingCriticalFiles(cwd, criticalSnapshot);
|
|
216
|
+
if (restoredCriticalFiles.length > 0) {
|
|
217
|
+
const pushed = commitAndPushRestoredFiles(cwd, restoredCriticalFiles);
|
|
218
|
+
if (useJson) {
|
|
219
|
+
jsonLine({
|
|
220
|
+
event: 'sync_restored_critical_files',
|
|
221
|
+
files: restoredCriticalFiles,
|
|
222
|
+
pushed,
|
|
223
|
+
timestamp: ts(),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.warn(yellow(` Sync removed critical file(s); restored: ${restoredCriticalFiles.join(', ')}`));
|
|
228
|
+
if (!pushed) {
|
|
229
|
+
console.warn(dim(' Restoration committed locally but not pushed; will retry on next auto-sync.'));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (useJson) {
|
|
234
|
+
jsonLine({ event: 'startup', phase: 'sync', status: syncResult.status, pushed: syncResult.pushed, timestamp: ts() });
|
|
235
|
+
if (syncResult.status === 'sync-failed') {
|
|
236
|
+
if (syncResult.error === 'stash-conflict') {
|
|
237
|
+
jsonLine({
|
|
238
|
+
event: 'error',
|
|
239
|
+
phase: 'sync',
|
|
240
|
+
timestamp: ts(),
|
|
241
|
+
error: {
|
|
242
|
+
message: 'Merge conflicts detected after stash pop',
|
|
243
|
+
diagnosis: 'Local changes could not be automatically merged with the remote state. The stash pop resulted in conflicts that require manual resolution.',
|
|
244
|
+
suggestions: [
|
|
245
|
+
'Resolve the merge conflicts in the affected files',
|
|
246
|
+
'Run git add on resolved files, then git commit',
|
|
247
|
+
'Run runwork dev again after resolving conflicts',
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
269
253
|
jsonLine({
|
|
270
254
|
event: 'error',
|
|
271
255
|
phase: 'sync',
|
|
272
256
|
timestamp: ts(),
|
|
273
257
|
error: {
|
|
274
|
-
message:
|
|
275
|
-
diagnosis: '
|
|
258
|
+
message: syncResult.error ?? 'File sync failed',
|
|
259
|
+
diagnosis: 'Could not sync files to the sandbox. This usually means the sandbox session expired or the network connection was interrupted.',
|
|
276
260
|
suggestions: [
|
|
277
|
-
'
|
|
278
|
-
'
|
|
279
|
-
'Run runwork
|
|
261
|
+
'Run runwork dev again to start a fresh session',
|
|
262
|
+
'Check your network connection',
|
|
263
|
+
'Run runwork doctor to diagnose environment issues',
|
|
280
264
|
],
|
|
281
265
|
},
|
|
282
266
|
});
|
|
283
|
-
process.exit(1);
|
|
284
267
|
}
|
|
285
|
-
jsonLine({
|
|
286
|
-
event: 'error',
|
|
287
|
-
phase: 'sync',
|
|
288
|
-
timestamp: ts(),
|
|
289
|
-
error: {
|
|
290
|
-
message: syncResult.error ?? 'File sync failed',
|
|
291
|
-
diagnosis: 'Could not sync files to the sandbox. This usually means the sandbox session expired or the network connection was interrupted.',
|
|
292
|
-
suggestions: [
|
|
293
|
-
'Run runwork dev again to start a fresh session',
|
|
294
|
-
'Check your network connection',
|
|
295
|
-
'Run runwork doctor to diagnose environment issues',
|
|
296
|
-
],
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
268
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
269
|
+
else {
|
|
270
|
+
switch (syncResult.status) {
|
|
271
|
+
case 'skipped':
|
|
272
|
+
console.log(dim(' No commits yet. Skipping sync.'));
|
|
273
|
+
break;
|
|
274
|
+
case 'synced':
|
|
275
|
+
console.log(green(' Synced with Runwork.'));
|
|
276
|
+
break;
|
|
277
|
+
case 'merged':
|
|
278
|
+
console.log(yellow(' Merged with Runwork (histories diverged).'));
|
|
279
|
+
break;
|
|
280
|
+
case 'sync-failed':
|
|
281
|
+
if (syncResult.error === 'stash-conflict') {
|
|
282
|
+
console.error('Merge conflicts detected after stash pop. Resolve conflicts and restart.');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
console.warn(yellow(`Sync failed. Continuing...`));
|
|
286
|
+
if (syncResult.error) {
|
|
287
|
+
console.warn(dim(` ${syncResult.error}`));
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
if (!syncResult.pushed && syncResult.status !== 'skipped' && syncResult.status !== 'sync-failed') {
|
|
292
|
+
console.warn(yellow('Push failed. Continuing with current state...'));
|
|
293
|
+
}
|
|
325
294
|
}
|
|
326
295
|
}
|
|
327
296
|
// The session POST returns a snapshot of the preview URL at boot time.
|
|
@@ -341,6 +310,7 @@ export async function execDev(options) {
|
|
|
341
310
|
previewUrl: currentPreviewUrl,
|
|
342
311
|
cliVersion: VERSION,
|
|
343
312
|
mode,
|
|
313
|
+
noSync,
|
|
344
314
|
}));
|
|
345
315
|
// Emit session_started (JSON) or show banner (human)
|
|
346
316
|
if (useJson) {
|
|
@@ -457,15 +427,32 @@ export async function execDev(options) {
|
|
|
457
427
|
}
|
|
458
428
|
});
|
|
459
429
|
}
|
|
460
|
-
// Start file watcher
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
430
|
+
// Start file watcher. In no-sync mode we never watch the working tree:
|
|
431
|
+
// no fast-sync to the sandbox, no git auto-commit. The user drives git
|
|
432
|
+
// manually and `git push` is what updates the preview. We print a loud
|
|
433
|
+
// notice so it is unmistakable that edits are not flowing automatically.
|
|
434
|
+
if (noSync) {
|
|
435
|
+
if (useJson) {
|
|
436
|
+
jsonLine({ event: 'startup', phase: 'no_sync_mode', timestamp: ts() });
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
console.log('');
|
|
440
|
+
console.log(yellow(' Auto-sync is OFF (--no-sync).'));
|
|
441
|
+
console.log(dim(' The preview reflects pushed commits. Run `git push` to update it;'));
|
|
442
|
+
console.log(dim(' uncommitted or unpushed changes are not shown.'));
|
|
443
|
+
console.log('');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
await watchAndAutoCommit(process.cwd(), client, config.appId, {
|
|
448
|
+
onFastSync: useJson
|
|
449
|
+
? (count) => { jsonLine({ event: 'files_synced', count, target: 'preview', timestamp: ts() }); }
|
|
450
|
+
: (count) => { syncedCount += count; updateStatus(); },
|
|
451
|
+
onGitPush: useJson
|
|
452
|
+
? (count) => { jsonLine({ event: 'files_pushed', count, target: 'git', timestamp: ts() }); }
|
|
453
|
+
: (count) => { syncedCount += count; updateStatus(); },
|
|
454
|
+
});
|
|
455
|
+
}
|
|
469
456
|
// Start log tailing (unless --no-logs)
|
|
470
457
|
if (options?.logs !== false) {
|
|
471
458
|
logTailer = startLogTailer({
|
|
@@ -487,12 +474,14 @@ export const devCommand = new Command('dev')
|
|
|
487
474
|
.option('--logs-only-file', 'Write logs to file only, not terminal')
|
|
488
475
|
.option('--detach', 'Start the dev session in the background and exit. Logs go to .runwork/dev-{stdout,stderr}.log')
|
|
489
476
|
.option('--restart', 'Stop any existing dev session first, then start a fresh one')
|
|
477
|
+
.option('--no-sync', 'Hold the preview open without auto-syncing files or git; you push manually to update the preview')
|
|
490
478
|
// Hidden internal flag passed by `runwork dev --detach` when it spawns
|
|
491
479
|
// its detached child. Not for end users -- registered here only so
|
|
492
480
|
// commander does not throw "unknown option" when the child receives it.
|
|
493
481
|
.addOption(new Option(INTERNAL_DETACHED_CHILD_FLAG).hideHelp())
|
|
494
482
|
.action(async (options, command) => {
|
|
495
483
|
const globalJson = shouldOutputJson(command.optsWithGlobals().json);
|
|
484
|
+
const noSync = options.sync === false;
|
|
496
485
|
// The internal child marker is parsed manually because we want it
|
|
497
486
|
// hidden from --help and from commander's option list. Its presence
|
|
498
487
|
// means: "this process is the detached child of a `--detach` parent;
|
|
@@ -510,6 +499,7 @@ export const devCommand = new Command('dev')
|
|
|
510
499
|
json: globalJson,
|
|
511
500
|
mode: isChild ? 'detached' : 'foreground',
|
|
512
501
|
restart: options.restart,
|
|
502
|
+
noSync,
|
|
513
503
|
});
|
|
514
504
|
});
|
|
515
505
|
/**
|
package/dist/commands/logs.js
CHANGED
|
@@ -21,13 +21,20 @@ export const logsCommand = new Command('logs')
|
|
|
21
21
|
.option('--production', 'Show production runtime logs')
|
|
22
22
|
.option('--events', 'Show app activity/events')
|
|
23
23
|
.option('--all', 'Show both runtime logs and events')
|
|
24
|
-
.option('--follow', 'Poll for new logs every 3s')
|
|
24
|
+
.option('--follow', 'Poll for new logs every 3s (TTY only; ignored when piped or with --json)')
|
|
25
|
+
.option('--once', 'Fetch once and exit, even if --follow is set (forced in non-TTY/agent contexts)')
|
|
25
26
|
.option('--limit <n>', 'Number of log entries to show', '25')
|
|
26
27
|
.option('--level <level>', 'Filter by log level (production only)')
|
|
27
28
|
.option('--search <text>', 'Search log content (production only)')
|
|
28
29
|
.option('--type <type>', 'Filter by event type (events only)')
|
|
29
30
|
.action(async (opts, command) => {
|
|
30
31
|
const useJson = shouldOutputJson(command.optsWithGlobals().json);
|
|
32
|
+
// Follow mode blocks forever (until SIGINT/SIGTERM), which deadlocks
|
|
33
|
+
// non-interactive callers: piped shells, CI, and AI agents never send
|
|
34
|
+
// those signals and hang waiting for the command to exit. Only follow
|
|
35
|
+
// when explicitly requested AND stdout is an interactive TTY AND we are
|
|
36
|
+
// not emitting JSON. `--once` forces single-shot even on a TTY.
|
|
37
|
+
const shouldFollow = Boolean(opts.follow) && !opts.once && !useJson && Boolean(process.stdout.isTTY);
|
|
31
38
|
const config = readConfig();
|
|
32
39
|
const creds = requireAuth();
|
|
33
40
|
const client = new ApiClient(creds);
|
|
@@ -79,7 +86,7 @@ export const logsCommand = new Command('logs')
|
|
|
79
86
|
// Preview logs (default)
|
|
80
87
|
try {
|
|
81
88
|
const { stdout, stderr } = await client.getPreviewLogs(appId);
|
|
82
|
-
if (
|
|
89
|
+
if (shouldFollow && !isFirstPoll) {
|
|
83
90
|
// Incremental mode: only print new content since last poll
|
|
84
91
|
// Reset detection: if buffer was truncated (sandbox restart), reset cursor
|
|
85
92
|
if (stdout && stdout.length < lastStdoutLength) {
|
|
@@ -198,7 +205,7 @@ export const logsCommand = new Command('logs')
|
|
|
198
205
|
isFirstPoll = false;
|
|
199
206
|
};
|
|
200
207
|
await fetchAndPrint();
|
|
201
|
-
if (
|
|
208
|
+
if (shouldFollow) {
|
|
202
209
|
let stopped = false;
|
|
203
210
|
let timer;
|
|
204
211
|
const scheduleNext = () => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { McpServerConfig } from '../types.js';
|
|
2
|
+
import type { McpServerEntry } from '../agents/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build local-agent MCP entries for external (third-party) servers added to the
|
|
5
|
+
* workspace. Auth handling by `authType`:
|
|
6
|
+
* - 'header': inject the user's resolved per-server headers (decrypted server-side).
|
|
7
|
+
* - 'oauth': emit no auth header so the local agent runs its own browser flow.
|
|
8
|
+
* - 'none'/unset: no auth header.
|
|
9
|
+
*
|
|
10
|
+
* Critically, external entries never receive the Runwork platform API key. That key
|
|
11
|
+
* belongs only on the Runwork workspace-tools entry, which is added separately.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildExternalMcpEntries(mcpServers: McpServerConfig[], resolvedCredentials: Record<string, Record<string, string>>): McpServerEntry[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RUNWORK_MCP_PREFIX } from '../agents/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build local-agent MCP entries for external (third-party) servers added to the
|
|
4
|
+
* workspace. Auth handling by `authType`:
|
|
5
|
+
* - 'header': inject the user's resolved per-server headers (decrypted server-side).
|
|
6
|
+
* - 'oauth': emit no auth header so the local agent runs its own browser flow.
|
|
7
|
+
* - 'none'/unset: no auth header.
|
|
8
|
+
*
|
|
9
|
+
* Critically, external entries never receive the Runwork platform API key. That key
|
|
10
|
+
* belongs only on the Runwork workspace-tools entry, which is added separately.
|
|
11
|
+
*/
|
|
12
|
+
export function buildExternalMcpEntries(mcpServers, resolvedCredentials) {
|
|
13
|
+
return mcpServers
|
|
14
|
+
.filter(s => s.enabled)
|
|
15
|
+
.map(s => {
|
|
16
|
+
const resolved = s.authType === 'header' ? resolvedCredentials[s.id] : undefined;
|
|
17
|
+
const headers = resolved && Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
18
|
+
return {
|
|
19
|
+
name: `${RUNWORK_MCP_PREFIX}${s.name}`,
|
|
20
|
+
url: s.url,
|
|
21
|
+
transport: s.transport,
|
|
22
|
+
...(headers ? { headers } : {}),
|
|
23
|
+
description: `Runwork MCP server: ${s.name}. Provides tools for interacting with this workspace resource.`,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ApiClient } from '../api/client.js';
|
|
|
7
7
|
import { getAdapterBySlug, detectAgents } from '../agents/detect.js';
|
|
8
8
|
import { CodexAdapter } from '../agents/codex.js';
|
|
9
9
|
import { RUNWORK_MCP_PREFIX, RUNWORK_WORKSPACE_MCP_NAME } from '../agents/types.js';
|
|
10
|
+
import { buildExternalMcpEntries } from './mcp-entries.js';
|
|
10
11
|
import { RUNWORK_AGENT_DEFAULTS, AGENT_DEFAULTS_SCHEMA_VERSION } from '../agents/default-config.js';
|
|
11
12
|
import { resolveAgentDefaults } from '../agents/defaults-merge.js';
|
|
12
13
|
import { collectTelemetryEvents, printTelemetryVerbose, summarizeTelemetryForDryRun, } from './sync-telemetry.js';
|
|
@@ -135,7 +136,7 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
135
136
|
}
|
|
136
137
|
console.log(' Fetching workspace data...');
|
|
137
138
|
// Fetch latest data (includeContent=true gets all skill content in one request)
|
|
138
|
-
const [allSkills, mcpServers, externalSkills, registries, connectedIntegrations] = await Promise.all([
|
|
139
|
+
const [allSkills, mcpServers, externalSkills, registries, connectedIntegrations, resolvedMcpCredentials] = await Promise.all([
|
|
139
140
|
client.listWorkspaceSkills(state.workspaceId, true),
|
|
140
141
|
client.listMcpServers(state.workspaceId),
|
|
141
142
|
client.listExternalSkills(state.workspaceId),
|
|
@@ -143,6 +144,9 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
143
144
|
client.listConnectedIntegrations(state.workspaceId)
|
|
144
145
|
.then(list => list.map(i => i.canonicalId ?? i.integrationId))
|
|
145
146
|
.catch(() => []),
|
|
147
|
+
// Decrypted auth headers for header-auth servers, scoped to this user.
|
|
148
|
+
// Tolerate older backends that lack the endpoint.
|
|
149
|
+
client.resolveMcpCredentials(state.workspaceId).catch(() => ({})),
|
|
146
150
|
]);
|
|
147
151
|
const appCount = allSkills.filter(s => s.type === 'app').length;
|
|
148
152
|
const parts = [];
|
|
@@ -167,15 +171,7 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
167
171
|
}
|
|
168
172
|
// Build MCP entries (pull-only)
|
|
169
173
|
const baseUrl = credentials.baseUrl || 'https://runwork.ai';
|
|
170
|
-
const mcpEntries = mcpServers
|
|
171
|
-
.filter(s => s.enabled)
|
|
172
|
-
.map(s => ({
|
|
173
|
-
name: `${RUNWORK_MCP_PREFIX}${s.name}`,
|
|
174
|
-
url: s.url,
|
|
175
|
-
transport: s.transport,
|
|
176
|
-
headers: { Authorization: `Bearer ${credentials.apiKey}` },
|
|
177
|
-
description: `Runwork MCP server: ${s.name}. Provides tools for interacting with this workspace resource.`,
|
|
178
|
-
}));
|
|
174
|
+
const mcpEntries = buildExternalMcpEntries(mcpServers, resolvedMcpCredentials);
|
|
179
175
|
mcpEntries.push({
|
|
180
176
|
name: RUNWORK_WORKSPACE_MCP_NAME,
|
|
181
177
|
url: `${baseUrl}/api/workspaces/${state.workspaceId}/mcp`,
|
|
@@ -343,5 +343,65 @@ describe('session lifecycle primitives', () => {
|
|
|
343
343
|
});
|
|
344
344
|
expect(file.startedAt).toBe(12345);
|
|
345
345
|
});
|
|
346
|
+
it('defaults noSync to false when omitted', () => {
|
|
347
|
+
const file = buildSessionFile({
|
|
348
|
+
pid: 1,
|
|
349
|
+
sessionId: 's',
|
|
350
|
+
appId: 'a',
|
|
351
|
+
previewUrl: '',
|
|
352
|
+
cliVersion: '0.0.0',
|
|
353
|
+
mode: 'foreground',
|
|
354
|
+
deps: { bootTime: () => 0 },
|
|
355
|
+
});
|
|
356
|
+
expect(file.noSync).toBe(false);
|
|
357
|
+
});
|
|
358
|
+
it('sets noSync when provided', () => {
|
|
359
|
+
const file = buildSessionFile({
|
|
360
|
+
pid: 1,
|
|
361
|
+
sessionId: 's',
|
|
362
|
+
appId: 'a',
|
|
363
|
+
previewUrl: '',
|
|
364
|
+
cliVersion: '0.0.0',
|
|
365
|
+
mode: 'foreground',
|
|
366
|
+
noSync: true,
|
|
367
|
+
deps: { bootTime: () => 0 },
|
|
368
|
+
});
|
|
369
|
+
expect(file.noSync).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
describe('noSync field back-compat', () => {
|
|
373
|
+
it('round-trips noSync through write/read', () => {
|
|
374
|
+
const file = buildSessionFile({
|
|
375
|
+
pid: process.pid,
|
|
376
|
+
sessionId: 's',
|
|
377
|
+
appId: 'a',
|
|
378
|
+
previewUrl: 'https://x',
|
|
379
|
+
cliVersion: '0.0.0',
|
|
380
|
+
mode: 'detached',
|
|
381
|
+
noSync: true,
|
|
382
|
+
deps: { bootTime: () => 0 },
|
|
383
|
+
});
|
|
384
|
+
writeSessionFile(appDir, file);
|
|
385
|
+
expect(readSessionFile(appDir)?.noSync).toBe(true);
|
|
386
|
+
});
|
|
387
|
+
it('validates a v1-style file that lacks noSync and reads it back falsy', () => {
|
|
388
|
+
const { dir, file } = getSessionPaths(appDir);
|
|
389
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
390
|
+
// v1 file shape: every required field present, no noSync key at all.
|
|
391
|
+
const v1 = makeValidSessionFile();
|
|
392
|
+
// makeValidSessionFile does not add noSync, so this is already v1-shaped.
|
|
393
|
+
expect('noSync' in v1).toBe(false);
|
|
394
|
+
fs.writeFileSync(file, JSON.stringify(v1), 'utf-8');
|
|
395
|
+
const read = readSessionFile(appDir);
|
|
396
|
+
expect(read).not.toBeNull();
|
|
397
|
+
expect(read?.noSync).toBeFalsy();
|
|
398
|
+
});
|
|
399
|
+
it('rejects a file whose noSync field is the wrong type', () => {
|
|
400
|
+
const { dir, file } = getSessionPaths(appDir);
|
|
401
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
402
|
+
const bad = { ...makeValidSessionFile(), noSync: 'yes' };
|
|
403
|
+
fs.writeFileSync(file, JSON.stringify(bad), 'utf-8');
|
|
404
|
+
expect(readSessionFile(appDir)).toBeNull();
|
|
405
|
+
});
|
|
346
406
|
});
|
|
347
407
|
});
|
package/dist/dev/session.d.ts
CHANGED
|
@@ -33,6 +33,13 @@ export interface SessionFile {
|
|
|
33
33
|
bootTime: number;
|
|
34
34
|
cliVersion: string;
|
|
35
35
|
mode: SessionMode;
|
|
36
|
+
/**
|
|
37
|
+
* Whether this session runs in no-sync mode (no file watcher, no
|
|
38
|
+
* auto-commit, no working-tree-mutating startup). Optional for
|
|
39
|
+
* back-compat with v1 session files that predate the field; absent is
|
|
40
|
+
* treated as `false`.
|
|
41
|
+
*/
|
|
42
|
+
noSync?: boolean;
|
|
36
43
|
}
|
|
37
44
|
export type SessionStaleReason = 'malformed' | 'version-mismatch' | 'app-id-mismatch' | 'boot-time-mismatch' | 'pid-dead';
|
|
38
45
|
export type SessionState = {
|
|
@@ -153,6 +160,7 @@ export declare function buildSessionFile(input: {
|
|
|
153
160
|
previewUrl: string;
|
|
154
161
|
cliVersion: string;
|
|
155
162
|
mode: SessionMode;
|
|
163
|
+
noSync?: boolean;
|
|
156
164
|
startedAt?: number;
|
|
157
165
|
deps?: SessionDeps;
|
|
158
166
|
}): SessionFile;
|
package/dist/dev/session.js
CHANGED
|
@@ -111,7 +111,9 @@ function validateSessionFile(raw) {
|
|
|
111
111
|
typeof r.startedAt === 'number' &&
|
|
112
112
|
typeof r.bootTime === 'number' &&
|
|
113
113
|
typeof r.cliVersion === 'string' &&
|
|
114
|
-
(r.mode === 'foreground' || r.mode === 'detached')
|
|
114
|
+
(r.mode === 'foreground' || r.mode === 'detached') &&
|
|
115
|
+
// Optional for back-compat: v1 files lack the field. Accept absent or boolean.
|
|
116
|
+
(r.noSync === undefined || typeof r.noSync === 'boolean'));
|
|
115
117
|
}
|
|
116
118
|
/**
|
|
117
119
|
* Read and parse the session file. Returns `null` for a missing file or
|
|
@@ -248,5 +250,6 @@ export function buildSessionFile(input) {
|
|
|
248
250
|
bootTime: d.bootTime(),
|
|
249
251
|
cliVersion: input.cliVersion,
|
|
250
252
|
mode: input.mode,
|
|
253
|
+
noSync: input.noSync ?? false,
|
|
251
254
|
};
|
|
252
255
|
}
|