runwork 0.10.3 → 0.11.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/agents/__tests__/claude-code-managed-block.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-code-managed-block.test.js +97 -0
- package/dist/agents/__tests__/claude-code-stats.test.js +1 -0
- package/dist/agents/__tests__/codex-minimum-permissions.test.d.ts +1 -0
- package/dist/agents/__tests__/codex-minimum-permissions.test.js +82 -0
- package/dist/agents/__tests__/cursor-merge.test.d.ts +1 -0
- package/dist/agents/__tests__/cursor-merge.test.js +66 -0
- package/dist/agents/__tests__/defaults-merge.test.d.ts +1 -0
- package/dist/agents/__tests__/defaults-merge.test.js +268 -0
- package/dist/agents/claude-code.d.ts +5 -0
- package/dist/agents/claude-code.js +30 -1
- package/dist/agents/cursor.d.ts +23 -1
- package/dist/agents/cursor.js +43 -4
- package/dist/agents/default-config.d.ts +30 -0
- package/dist/agents/default-config.js +67 -0
- package/dist/agents/defaults-merge.d.ts +72 -0
- package/dist/agents/defaults-merge.js +131 -0
- package/dist/agents/types.d.ts +31 -2
- package/dist/commands/clone.js +1 -1
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/dev.js +22 -18
- package/dist/commands/init.js +1 -1
- package/dist/commands/sync.js +146 -59
- package/dist/dev/__tests__/detach.test.js +77 -1
- package/dist/dev/detach.d.ts +23 -0
- package/dist/dev/detach.js +45 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/git/__tests__/credentials.test.js +1 -1
- package/dist/git/auto-commit.js +1 -1
- package/dist/git/credentials.js +1 -1
- package/dist/git/identity.js +1 -1
- package/dist/git/preflight.js +1 -1
- package/dist/git/sync.js +1 -1
- package/dist/health/checks.js +1 -1
- package/dist/template/manifest.js +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/ui/banner.js +1 -1
- package/dist/utils/agent-guidance.d.ts +13 -0
- package/dist/utils/agent-guidance.js +22 -7
- package/dist/utils/subprocess.d.ts +19 -0
- package/dist/utils/subprocess.js +27 -0
- package/dist/utils/which.js +1 -1
- package/package.json +1 -1
package/dist/commands/sync.js
CHANGED
|
@@ -7,6 +7,8 @@ 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 { RUNWORK_AGENT_DEFAULTS, AGENT_DEFAULTS_SCHEMA_VERSION } from '../agents/default-config.js';
|
|
11
|
+
import { resolveAgentDefaults } from '../agents/defaults-merge.js';
|
|
10
12
|
import { collectTelemetryEvents, printTelemetryVerbose, summarizeTelemetryForDryRun, } from './sync-telemetry.js';
|
|
11
13
|
import { generateIntroSkill, generateInstructionHint, buildAppSkillDescription } from '../agents/intro-skill.js';
|
|
12
14
|
import { computeSyncPlan } from '../sync/change-detect.js';
|
|
@@ -346,85 +348,170 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
346
348
|
}
|
|
347
349
|
}
|
|
348
350
|
}
|
|
349
|
-
// Pull
|
|
351
|
+
// Pull team config from server. A failure here must not stop the
|
|
352
|
+
// unified user-scope write below, which applies network and minimum-permission
|
|
353
|
+
// floors regardless of whether team config was fetched.
|
|
350
354
|
let teamInstructionsApplied = false;
|
|
351
355
|
let agentConfigsApplied = 0;
|
|
356
|
+
let teamInstructions;
|
|
357
|
+
let agentConfigs;
|
|
352
358
|
try {
|
|
353
359
|
const onboardingConfig = await client.getOnboardingConfig(state.workspaceId);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
360
|
+
teamInstructions = onboardingConfig?.config?.teamInstructions ?? undefined;
|
|
361
|
+
agentConfigs = onboardingConfig?.config?.agentConfigs ?? undefined;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Team config pull is non-fatal — proceed with no team-managed overrides.
|
|
365
|
+
}
|
|
366
|
+
// Write team instructions and/or per-agent extra instructions.
|
|
367
|
+
for (const adapter of adapters) {
|
|
368
|
+
if (!adapter.writeTeamInstructions)
|
|
369
|
+
continue;
|
|
370
|
+
const agentExtra = agentConfigs?.[adapter.slug]?.extraInstructions;
|
|
371
|
+
const parts = [teamInstructions, agentExtra].filter(Boolean);
|
|
372
|
+
if (parts.length === 0)
|
|
373
|
+
continue;
|
|
374
|
+
const fullInstructions = parts.join('\n\n');
|
|
375
|
+
for (const scope of scopes) {
|
|
376
|
+
try {
|
|
377
|
+
await adapter.writeTeamInstructions(fullInstructions, scope);
|
|
378
|
+
teamInstructionsApplied = true;
|
|
379
|
+
console.log(` [${adapter.name}] Updated team instructions (${scope})`);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// Best-effort per adapter/scope
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Apply per-agent project-scope config (team overrides only).
|
|
387
|
+
// User scope is handled below in the unified defaults+network+minimum block.
|
|
388
|
+
if (agentConfigs) {
|
|
358
389
|
for (const adapter of adapters) {
|
|
359
|
-
|
|
390
|
+
const agentConfig = agentConfigs[adapter.slug];
|
|
391
|
+
if (!agentConfig || !adapter.writeAgentConfig)
|
|
360
392
|
continue;
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
if (parts.length === 0)
|
|
393
|
+
const { extraInstructions: _, ...configWithoutInstructions } = agentConfig;
|
|
394
|
+
if (Object.keys(configWithoutInstructions).length === 0)
|
|
364
395
|
continue;
|
|
365
|
-
const fullInstructions = parts.join('\n\n');
|
|
366
396
|
for (const scope of scopes) {
|
|
397
|
+
if (scope === 'user')
|
|
398
|
+
continue; // handled in unified block below
|
|
367
399
|
try {
|
|
368
|
-
await adapter.
|
|
369
|
-
|
|
370
|
-
|
|
400
|
+
await adapter.writeAgentConfig(configWithoutInstructions, scope);
|
|
401
|
+
agentConfigsApplied++;
|
|
402
|
+
const configKeys = Object.keys(configWithoutInstructions).join(', ');
|
|
403
|
+
console.log(` [${adapter.name}] Updated agent config: ${configKeys} (${scope})`);
|
|
371
404
|
}
|
|
372
405
|
catch {
|
|
373
406
|
// Best-effort per adapter/scope
|
|
374
407
|
}
|
|
375
408
|
}
|
|
376
409
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
410
|
+
}
|
|
411
|
+
// Unified user-scope write: merge baked defaults with team config, network
|
|
412
|
+
// allowlist, and minimum permissions into a single writeAgentConfig call
|
|
413
|
+
// per adapter. The merge layer tracks injected defaults in state.agentDefaults
|
|
414
|
+
// so user-removed entries become sticky opt-outs.
|
|
415
|
+
if (scopes.includes('user')) {
|
|
416
|
+
const networkDomains = ['runwork.ai', '*.runwork.ai'];
|
|
417
|
+
try {
|
|
418
|
+
const baseHost = new URL(baseUrl).hostname;
|
|
419
|
+
if (baseHost !== 'runwork.ai' && !baseHost.endsWith('.runwork.ai')) {
|
|
420
|
+
networkDomains.push(baseHost, `*.${baseHost}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch { /* use defaults */ }
|
|
424
|
+
// Order arrays list each agent's valid values from most restrictive to most
|
|
425
|
+
// permissive. The floor logic upgrades anything strictly below `minimum`,
|
|
426
|
+
// including unknown values (-1 < minimumIdx). That makes it critical to
|
|
427
|
+
// include the agent's most-permissive option in `order` — otherwise a user
|
|
428
|
+
// who explicitly set, say, `sandbox_mode = "danger-full-access"` gets
|
|
429
|
+
// silently downgraded to `workspace-write`.
|
|
430
|
+
const minimumPermissions = [
|
|
431
|
+
{ field: 'approval_policy', order: ['untrusted', 'on-request', 'on-failure', 'never'], minimum: 'on-request' },
|
|
432
|
+
{ field: 'sandbox_mode', order: ['read-only', 'workspace-write', 'danger-full-access'], minimum: 'workspace-write' },
|
|
433
|
+
];
|
|
434
|
+
// agentDefaults lifecycle: full `runwork uninstall` deletes ~/.runwork
|
|
435
|
+
// (state and all per-agent defaults state with it). Re-running `runwork
|
|
436
|
+
// setup` overwrites setup.json without agentDefaults so every remaining
|
|
437
|
+
// tool bootstraps fresh on the next sync. refreshConfiguredAgents only
|
|
438
|
+
// adds slugs (never removes), so we don't need orphan-entry pruning here.
|
|
439
|
+
if (!state.agentDefaults)
|
|
440
|
+
state.agentDefaults = {};
|
|
441
|
+
for (const adapter of adapters) {
|
|
442
|
+
if (!adapter.writeAgentConfig)
|
|
443
|
+
continue;
|
|
444
|
+
const slug = adapter.slug;
|
|
445
|
+
const baked = RUNWORK_AGENT_DEFAULTS[slug];
|
|
446
|
+
const agentState = state.agentDefaults[slug];
|
|
447
|
+
const team = agentConfigs?.[slug];
|
|
448
|
+
// onDisk = undefined signals "removal detection is not possible" — used for
|
|
449
|
+
// markerless adapters (Cursor) and for any adapter without readManagedBlock.
|
|
450
|
+
// The resolver treats undefined differently from an empty array: it skips
|
|
451
|
+
// opt-out fabrication entirely and trusts the baseline mechanism in the
|
|
452
|
+
// adapter to preserve user edits via subtraction.
|
|
453
|
+
let onDisk;
|
|
454
|
+
if (baked && adapter.readManagedBlock) {
|
|
455
|
+
try {
|
|
456
|
+
onDisk = await adapter.readManagedBlock('user');
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
onDisk = undefined;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const resolved = resolveAgentDefaults({
|
|
463
|
+
baked,
|
|
464
|
+
state: agentState,
|
|
465
|
+
onDisk,
|
|
466
|
+
team: team?.permissionRules,
|
|
467
|
+
});
|
|
468
|
+
const mergedConfig = {
|
|
469
|
+
modelPreference: team?.modelPreference,
|
|
470
|
+
permissionRules: (resolved.managedAllow.length || resolved.managedDeny.length || team?.permissionRules?.defaultMode)
|
|
471
|
+
? {
|
|
472
|
+
...(resolved.managedAllow.length ? { allow: resolved.managedAllow } : {}),
|
|
473
|
+
...(resolved.managedDeny.length ? { deny: resolved.managedDeny } : {}),
|
|
474
|
+
...(team?.permissionRules?.defaultMode ? { defaultMode: team.permissionRules.defaultMode } : {}),
|
|
393
475
|
}
|
|
394
|
-
|
|
395
|
-
|
|
476
|
+
: undefined,
|
|
477
|
+
networkAllowlist: networkDomains,
|
|
478
|
+
minimumPermissions,
|
|
479
|
+
};
|
|
480
|
+
try {
|
|
481
|
+
const baseline = agentState?.lastInjected;
|
|
482
|
+
await adapter.writeAgentConfig(mergedConfig, 'user', baseline);
|
|
483
|
+
if (baked) {
|
|
484
|
+
const nextState = {
|
|
485
|
+
lastInjected: {
|
|
486
|
+
allow: resolved.applicableAllow,
|
|
487
|
+
deny: resolved.applicableDeny,
|
|
488
|
+
},
|
|
489
|
+
userOptOuts: {
|
|
490
|
+
allow: resolved.newOptOutsAllow,
|
|
491
|
+
deny: resolved.newOptOutsDeny,
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
state.agentDefaults[slug] = nextState;
|
|
495
|
+
// Diagnostic log line (per "invisible / sync log only" policy)
|
|
496
|
+
if (resolved.bootstrapped) {
|
|
497
|
+
console.log(` [${adapter.name}] Bootstrapped default-rule tracking (defaults apply on next sync)`);
|
|
498
|
+
}
|
|
499
|
+
else if (resolved.applicableAllow.length || resolved.applicableDeny.length || resolved.removalsThisSync.allow || resolved.removalsThisSync.deny) {
|
|
500
|
+
const totalRemovals = resolved.removalsThisSync.allow + resolved.removalsThisSync.deny;
|
|
501
|
+
const removalNote = totalRemovals > 0
|
|
502
|
+
? ` (${totalRemovals} new opt-out${totalRemovals > 1 ? 's' : ''} honored)`
|
|
503
|
+
: '';
|
|
504
|
+
console.log(` [${adapter.name}] Applied defaults: ${resolved.applicableAllow.length} allow, ${resolved.applicableDeny.length} deny${removalNote}`);
|
|
396
505
|
}
|
|
397
506
|
}
|
|
507
|
+
if (team)
|
|
508
|
+
agentConfigsApplied++;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// Best-effort per adapter
|
|
398
512
|
}
|
|
399
513
|
}
|
|
400
|
-
|
|
401
|
-
catch {
|
|
402
|
-
// Team config pull is non-fatal
|
|
403
|
-
}
|
|
404
|
-
// Ensure sandbox network access for agents that run in sandboxed environments (e.g. Cursor)
|
|
405
|
-
const networkDomains = ['runwork.ai', '*.runwork.ai'];
|
|
406
|
-
try {
|
|
407
|
-
const baseHost = new URL(baseUrl).hostname;
|
|
408
|
-
if (baseHost !== 'runwork.ai' && !baseHost.endsWith('.runwork.ai')) {
|
|
409
|
-
networkDomains.push(baseHost, `*.${baseHost}`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
catch { /* use defaults */ }
|
|
413
|
-
for (const adapter of adapters) {
|
|
414
|
-
if (!adapter.writeAgentConfig)
|
|
415
|
-
continue;
|
|
416
|
-
try {
|
|
417
|
-
await adapter.writeAgentConfig({
|
|
418
|
-
networkAllowlist: networkDomains,
|
|
419
|
-
minimumPermissions: [
|
|
420
|
-
{ field: 'approval_policy', order: ['always', 'untrusted', 'on-request', 'never'], minimum: 'on-request' },
|
|
421
|
-
{ field: 'sandbox_mode', order: ['full', 'read-only', 'workspace-write', 'off'], minimum: 'workspace-write' },
|
|
422
|
-
],
|
|
423
|
-
}, 'user');
|
|
424
|
-
}
|
|
425
|
-
catch {
|
|
426
|
-
// Best-effort
|
|
427
|
-
}
|
|
514
|
+
state.agentDefaultsVersion = AGENT_DEFAULTS_SCHEMA_VERSION;
|
|
428
515
|
}
|
|
429
516
|
// Register ~/.runwork as a project in the Codex desktop app (best-effort).
|
|
430
517
|
// Only attempts when Codex adapter is configured and the desktop app is closed.
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import { pollForSession, runAsDetachedParent, isInternalDetachedChild, stripInternalChildFlag, INTERNAL_DETACHED_CHILD_FLAG, } from '../detach.js';
|
|
5
|
+
import { buildChildArgs, pollForSession, runAsDetachedParent, isInternalDetachedChild, looksLikeBunStandaloneArtifact, stripInternalChildFlag, INTERNAL_DETACHED_CHILD_FLAG, } from '../detach.js';
|
|
6
6
|
import { buildSessionFile, getSessionPaths, writeSessionFile, } from '../session.js';
|
|
7
7
|
function createTmpAppDir() {
|
|
8
8
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'runwork-detach-test-'));
|
|
@@ -32,6 +32,82 @@ describe('isInternalDetachedChild() / stripInternalChildFlag()', () => {
|
|
|
32
32
|
expect(stripInternalChildFlag(['dev', '--detach'])).toEqual(['dev', '--detach']);
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
|
+
describe('looksLikeBunStandaloneArtifact()', () => {
|
|
36
|
+
it('detects Bun-on-Windows virtual-FS paths (forward slashes)', () => {
|
|
37
|
+
expect(looksLikeBunStandaloneArtifact('B:/~BUN/root/runwork-windows-x64.exe')).toBe(true);
|
|
38
|
+
expect(looksLikeBunStandaloneArtifact('C:/~BUN/root/foo.exe')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
it('detects Bun-on-Windows virtual-FS paths (backslashes)', () => {
|
|
41
|
+
expect(looksLikeBunStandaloneArtifact('B:\\~BUN\\root\\runwork.exe')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it('detects bunfs prefix on POSIX (defensive)', () => {
|
|
44
|
+
expect(looksLikeBunStandaloneArtifact('/$bunfs/root/runwork')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('does not flag normal paths or arguments', () => {
|
|
47
|
+
expect(looksLikeBunStandaloneArtifact('dev')).toBe(false);
|
|
48
|
+
expect(looksLikeBunStandaloneArtifact('--detach')).toBe(false);
|
|
49
|
+
expect(looksLikeBunStandaloneArtifact('/usr/local/bin/runwork')).toBe(false);
|
|
50
|
+
expect(looksLikeBunStandaloneArtifact('C:/Users/Oytun/.runwork/bin/runwork.exe')).toBe(false);
|
|
51
|
+
expect(looksLikeBunStandaloneArtifact('node')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('buildChildArgs()', () => {
|
|
55
|
+
it('Node script invocation: forwards script path + user args, appends marker', () => {
|
|
56
|
+
// node /path/to/dist/index.js dev --detach
|
|
57
|
+
const argv = ['node', '/path/to/dist/index.js', 'dev', '--detach'];
|
|
58
|
+
expect(buildChildArgs(argv)).toEqual([
|
|
59
|
+
'/path/to/dist/index.js', 'dev', '--detach', INTERNAL_DETACHED_CHILD_FLAG,
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
it('Bun standalone macOS/Linux: forwards user args directly', () => {
|
|
63
|
+
// /usr/local/bin/runwork dev --detach
|
|
64
|
+
const argv = ['/usr/local/bin/runwork', 'dev', '--detach'];
|
|
65
|
+
expect(buildChildArgs(argv)).toEqual([
|
|
66
|
+
'dev', '--detach', INTERNAL_DETACHED_CHILD_FLAG,
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
it('Bun standalone Windows: drops the virtual-FS argv[1] before forwarding', () => {
|
|
70
|
+
// The exact failure from the field. Bun-on-Windows injects argv[1]
|
|
71
|
+
// = "B:/~BUN/root/runwork-windows-x64.exe". We must NOT forward it
|
|
72
|
+
// -- the child Bun runtime will inject its own.
|
|
73
|
+
const argv = [
|
|
74
|
+
'C:\\Users\\Oytun\\.runwork\\bin\\runwork.exe',
|
|
75
|
+
'B:/~BUN/root/runwork-windows-x64.exe',
|
|
76
|
+
'dev',
|
|
77
|
+
'--detach',
|
|
78
|
+
];
|
|
79
|
+
expect(buildChildArgs(argv)).toEqual([
|
|
80
|
+
'dev', '--detach', INTERNAL_DETACHED_CHILD_FLAG,
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
it('strips an existing internal-child flag and re-adds exactly one', () => {
|
|
84
|
+
const argv = [
|
|
85
|
+
'/usr/local/bin/runwork',
|
|
86
|
+
'dev',
|
|
87
|
+
'--detach',
|
|
88
|
+
INTERNAL_DETACHED_CHILD_FLAG,
|
|
89
|
+
INTERNAL_DETACHED_CHILD_FLAG,
|
|
90
|
+
];
|
|
91
|
+
const out = buildChildArgs(argv);
|
|
92
|
+
expect(out.filter((a) => a === INTERNAL_DETACHED_CHILD_FLAG)).toHaveLength(1);
|
|
93
|
+
});
|
|
94
|
+
it('preserves the order of user args and only mutates argv[0..1]', () => {
|
|
95
|
+
const argv = [
|
|
96
|
+
'C:\\bin\\runwork.exe',
|
|
97
|
+
'B:/~BUN/root/runwork-windows-x64.exe',
|
|
98
|
+
'dev',
|
|
99
|
+
'--restart',
|
|
100
|
+
'--detach',
|
|
101
|
+
'--json',
|
|
102
|
+
];
|
|
103
|
+
expect(buildChildArgs(argv)).toEqual([
|
|
104
|
+
'dev', '--restart', '--detach', '--json', INTERNAL_DETACHED_CHILD_FLAG,
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
it('handles an empty argv tail (just the binary)', () => {
|
|
108
|
+
expect(buildChildArgs(['runwork'])).toEqual([INTERNAL_DETACHED_CHILD_FLAG]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
35
111
|
describe('pollForSession()', () => {
|
|
36
112
|
let appDir;
|
|
37
113
|
beforeEach(() => {
|
package/dist/dev/detach.d.ts
CHANGED
|
@@ -162,3 +162,26 @@ export declare function isInternalDetachedChild(argv?: readonly string[]): boole
|
|
|
162
162
|
* misconfigured wrapper).
|
|
163
163
|
*/
|
|
164
164
|
export declare function stripInternalChildFlag(args: readonly string[]): string[];
|
|
165
|
+
/**
|
|
166
|
+
* Detect a Bun standalone virtual-filesystem path. Bun's compile mode
|
|
167
|
+
* on Windows injects the in-bundle script path as `process.argv[1]`
|
|
168
|
+
* (e.g., `B:/~BUN/root/runwork-windows-x64.exe`). When we self-spawn,
|
|
169
|
+
* the child Bun runtime re-injects an equivalent entry on its own --
|
|
170
|
+
* forwarding ours causes a duplicate that downstream parsers (commander
|
|
171
|
+
* here) misread as a stray positional command. macOS and Linux Bun
|
|
172
|
+
* standalone do NOT inject this entry, but the prefix is documented in
|
|
173
|
+
* Bun source as `/$bunfs/` if it ever appears, so we detect that too
|
|
174
|
+
* defensively.
|
|
175
|
+
*/
|
|
176
|
+
export declare function looksLikeBunStandaloneArtifact(p: string): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Construct the args we should forward to the spawned child so it
|
|
179
|
+
* re-runs the same `runwork dev` invocation as the parent. Drops
|
|
180
|
+
* elements that the child runtime will re-inject on its own (notably
|
|
181
|
+
* the Bun-on-Windows virtual-FS path) and drops any pre-existing copy
|
|
182
|
+
* of the internal-child marker before we re-add exactly one.
|
|
183
|
+
*
|
|
184
|
+
* Pure function for testability -- accepts the parent's argv and returns
|
|
185
|
+
* what to hand to `spawn`. Real callers pass `process.argv`.
|
|
186
|
+
*/
|
|
187
|
+
export declare function buildChildArgs(parentArgv: readonly string[]): string[];
|
package/dist/dev/detach.js
CHANGED
|
@@ -245,3 +245,48 @@ export function isInternalDetachedChild(argv = process.argv) {
|
|
|
245
245
|
export function stripInternalChildFlag(args) {
|
|
246
246
|
return args.filter((a) => a !== INTERNAL_DETACHED_CHILD_FLAG);
|
|
247
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Detect a Bun standalone virtual-filesystem path. Bun's compile mode
|
|
250
|
+
* on Windows injects the in-bundle script path as `process.argv[1]`
|
|
251
|
+
* (e.g., `B:/~BUN/root/runwork-windows-x64.exe`). When we self-spawn,
|
|
252
|
+
* the child Bun runtime re-injects an equivalent entry on its own --
|
|
253
|
+
* forwarding ours causes a duplicate that downstream parsers (commander
|
|
254
|
+
* here) misread as a stray positional command. macOS and Linux Bun
|
|
255
|
+
* standalone do NOT inject this entry, but the prefix is documented in
|
|
256
|
+
* Bun source as `/$bunfs/` if it ever appears, so we detect that too
|
|
257
|
+
* defensively.
|
|
258
|
+
*/
|
|
259
|
+
export function looksLikeBunStandaloneArtifact(p) {
|
|
260
|
+
// Windows: drive-letter paths under \~BUN\, e.g. "B:/~BUN/root/..." or "B:\~BUN\root\..."
|
|
261
|
+
if (/^[a-z]:[/\\]~BUN[/\\]/i.test(p))
|
|
262
|
+
return true;
|
|
263
|
+
// Linux/macOS bunfs prefix (documented; not currently emitted in user-visible argv).
|
|
264
|
+
if (p.startsWith('/$bunfs/'))
|
|
265
|
+
return true;
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Construct the args we should forward to the spawned child so it
|
|
270
|
+
* re-runs the same `runwork dev` invocation as the parent. Drops
|
|
271
|
+
* elements that the child runtime will re-inject on its own (notably
|
|
272
|
+
* the Bun-on-Windows virtual-FS path) and drops any pre-existing copy
|
|
273
|
+
* of the internal-child marker before we re-add exactly one.
|
|
274
|
+
*
|
|
275
|
+
* Pure function for testability -- accepts the parent's argv and returns
|
|
276
|
+
* what to hand to `spawn`. Real callers pass `process.argv`.
|
|
277
|
+
*/
|
|
278
|
+
export function buildChildArgs(parentArgv) {
|
|
279
|
+
// argv[0] is always the binary; the child gets it back via spawn's
|
|
280
|
+
// execPath argument. We start scanning from argv[1].
|
|
281
|
+
const rest = [];
|
|
282
|
+
for (let i = 1; i < parentArgv.length; i++) {
|
|
283
|
+
if (i === 1 && looksLikeBunStandaloneArtifact(parentArgv[i])) {
|
|
284
|
+
// Skip Bun-on-Windows's auto-injected virtual-FS path; the child
|
|
285
|
+
// runtime injects its own equivalent entry. Forwarding ours would
|
|
286
|
+
// duplicate it in the child's argv.
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
rest.push(parentArgv[i]);
|
|
290
|
+
}
|
|
291
|
+
return [...stripInternalChildFlag(rest), INTERNAL_DETACHED_CHILD_FLAG];
|
|
292
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.11.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.11.0";
|
|
@@ -60,7 +60,7 @@ describe('git/credentials', () => {
|
|
|
60
60
|
'--global',
|
|
61
61
|
'--unset',
|
|
62
62
|
'credential.https://runwork.ai.helper',
|
|
63
|
-
], { stdio: 'pipe' });
|
|
63
|
+
], { stdio: 'pipe', windowsHide: true });
|
|
64
64
|
});
|
|
65
65
|
it('ignores errors silently', async () => {
|
|
66
66
|
mockExecFileSync.mockImplementation(() => {
|
package/dist/git/auto-commit.js
CHANGED
package/dist/git/credentials.js
CHANGED
package/dist/git/identity.js
CHANGED
package/dist/git/preflight.js
CHANGED
package/dist/git/sync.js
CHANGED
package/dist/health/checks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
-
import { execFileSync } from '
|
|
2
|
+
import { execFileSync } from '../utils/subprocess.js';
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'fs';
|
|
4
4
|
import { join, relative } from 'path';
|
|
5
5
|
import { buildIgnoreSets } from '../utils/ignore-matcher.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -121,6 +121,25 @@ export interface McpServerConfig {
|
|
|
121
121
|
lastConnectedAt?: number;
|
|
122
122
|
toolCount?: number;
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Per-agent state tracking baked-in default permission rules that the CLI
|
|
126
|
+
* injects automatically during sync, with sticky opt-out semantics.
|
|
127
|
+
*
|
|
128
|
+
* lastInjected: what we wrote on the previous sync (used to detect when the
|
|
129
|
+
* user removed an entry from the agent's config file).
|
|
130
|
+
* userOptOuts: ever-accumulating set of entries the user has removed at least
|
|
131
|
+
* once. Never re-injected until the tool is removed entirely.
|
|
132
|
+
*/
|
|
133
|
+
export interface AgentDefaultsState {
|
|
134
|
+
lastInjected: {
|
|
135
|
+
allow?: string[];
|
|
136
|
+
deny?: string[];
|
|
137
|
+
};
|
|
138
|
+
userOptOuts: {
|
|
139
|
+
allow?: string[];
|
|
140
|
+
deny?: string[];
|
|
141
|
+
};
|
|
142
|
+
}
|
|
124
143
|
export interface SetupState {
|
|
125
144
|
workspaceId: string;
|
|
126
145
|
workspaceName: string;
|
|
@@ -147,6 +166,10 @@ export interface SetupState {
|
|
|
147
166
|
* on every cycle while still catching newly-installed agents within a day.
|
|
148
167
|
*/
|
|
149
168
|
lastDetectedAt?: string;
|
|
169
|
+
/** Schema version for agentDefaults; absence means bootstrap on next sync. */
|
|
170
|
+
agentDefaultsVersion?: number;
|
|
171
|
+
/** Per-agent slug => default rule state. Absence of an entry means bootstrap. */
|
|
172
|
+
agentDefaults?: Record<string, AgentDefaultsState>;
|
|
150
173
|
}
|
|
151
174
|
export interface WorkflowInfo {
|
|
152
175
|
name: string;
|
package/dist/ui/banner.js
CHANGED
|
@@ -10,9 +10,9 @@ function prettyPath(dir) {
|
|
|
10
10
|
return dir;
|
|
11
11
|
}
|
|
12
12
|
export const SUPPORTED_AGENTS = [
|
|
13
|
+
{ id: 'codex', name: 'OpenAI Codex', command: 'cd {dir} && codex' },
|
|
13
14
|
{ id: 'claude-code', name: 'Claude Code', command: 'cd {dir} && claude' },
|
|
14
15
|
{ id: 'cursor', name: 'Cursor', command: 'cursor {dir}' },
|
|
15
|
-
{ id: 'codex', name: 'OpenAI Codex', command: 'cd {dir} && codex' },
|
|
16
16
|
{ id: 'claude-desktop', name: 'Claude Desktop', command: 'Open Claude Desktop and add {dir} as a project folder' },
|
|
17
17
|
{ id: 'antigravity', name: 'Antigravity', command: 'cd {dir} && antigravity' },
|
|
18
18
|
];
|
|
@@ -24,6 +24,19 @@ export interface AgentResponse<T> {
|
|
|
24
24
|
}
|
|
25
25
|
/** Map of key template files and what they're for. */
|
|
26
26
|
export declare const APP_STRUCTURE: Record<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* The single most important workflow rule for AI agents working on a
|
|
29
|
+
* Runwork app. Repeated across multiple guides because it's the
|
|
30
|
+
* difference between a working dev cycle and a frustrating one.
|
|
31
|
+
*
|
|
32
|
+
* Rationale -- agents that don't follow this end up:
|
|
33
|
+
* 1. Editing files locally with no preview to verify against.
|
|
34
|
+
* 2. Hitting a single bulk-sync at dev startup that masks which
|
|
35
|
+
* change broke what (instead of incremental per-change syncs).
|
|
36
|
+
* 3. Risking sync conflicts when the local state has diverged from
|
|
37
|
+
* the remote in a way the auto-sync can't reconcile.
|
|
38
|
+
*/
|
|
39
|
+
export declare const DEV_FIRST_RULE = "CRITICAL: Always start `runwork dev --detach` BEFORE editing any code in this app. Dev establishes the live sync pipeline -- without it, changes do not flow to the preview, and the dev startup will batch-sync everything at once instead of giving you per-change feedback. To check whether dev is already running for this app, run `runwork info --json` and inspect `localDevSession.state` (alive / stale / none). If alive, just edit. If none or stale, run `runwork dev --detach --json` first, capture the preview URL from the `session_started` event, then proceed with edits.";
|
|
27
40
|
export declare function buildInitGuide(appName: string, dir: string): AgentGuide;
|
|
28
41
|
export declare function buildCloneGuide(appName: string, dir: string): AgentGuide;
|
|
29
42
|
export declare function buildDevSessionGuide(): AgentGuide;
|
|
@@ -18,7 +18,21 @@ export const APP_STRUCTURE = {
|
|
|
18
18
|
'blueprint.json': 'App feature registry. Update after adding entities, workflows, agents, etc.',
|
|
19
19
|
'CLAUDE.md': 'Complete framework documentation. Read this before editing anything.',
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* The single most important workflow rule for AI agents working on a
|
|
23
|
+
* Runwork app. Repeated across multiple guides because it's the
|
|
24
|
+
* difference between a working dev cycle and a frustrating one.
|
|
25
|
+
*
|
|
26
|
+
* Rationale -- agents that don't follow this end up:
|
|
27
|
+
* 1. Editing files locally with no preview to verify against.
|
|
28
|
+
* 2. Hitting a single bulk-sync at dev startup that masks which
|
|
29
|
+
* change broke what (instead of incremental per-change syncs).
|
|
30
|
+
* 3. Risking sync conflicts when the local state has diverged from
|
|
31
|
+
* the remote in a way the auto-sync can't reconcile.
|
|
32
|
+
*/
|
|
33
|
+
export const DEV_FIRST_RULE = 'CRITICAL: Always start `runwork dev --detach` BEFORE editing any code in this app. Dev establishes the live sync pipeline -- without it, changes do not flow to the preview, and the dev startup will batch-sync everything at once instead of giving you per-change feedback. To check whether dev is already running for this app, run `runwork info --json` and inspect `localDevSession.state` (alive / stale / none). If alive, just edit. If none or stale, run `runwork dev --detach --json` first, capture the preview URL from the `session_started` event, then proceed with edits.';
|
|
21
34
|
const COMMON_TIPS = [
|
|
35
|
+
DEV_FIRST_RULE,
|
|
22
36
|
'Read CLAUDE.md in the app directory first -- it has complete framework documentation with code examples.',
|
|
23
37
|
'You do NOT need to run git commands manually. runwork dev handles file syncing automatically. (git itself must be installed on the system -- see dependencies.)',
|
|
24
38
|
'Do NOT install external AI SDKs (openai, @anthropic-ai/sdk). Use @runworkai/framework/ai instead.',
|
|
@@ -39,8 +53,8 @@ export function buildInitGuide(appName, dir) {
|
|
|
39
53
|
structure: APP_STRUCTURE,
|
|
40
54
|
nextSteps: [
|
|
41
55
|
`cd ${dir}`,
|
|
42
|
-
'runwork dev # start
|
|
43
|
-
'
|
|
56
|
+
'runwork dev --detach --json # FIRST: start the dev sandbox in the background; capture the preview URL from the session_started event',
|
|
57
|
+
'THEN edit files for your needs (see structure above) -- changes auto-sync to the preview',
|
|
44
58
|
'runwork deploy # deploy to production when ready',
|
|
45
59
|
],
|
|
46
60
|
tips: COMMON_TIPS,
|
|
@@ -53,9 +67,9 @@ export function buildCloneGuide(appName, dir) {
|
|
|
53
67
|
structure: APP_STRUCTURE,
|
|
54
68
|
nextSteps: [
|
|
55
69
|
`cd ${dir}`,
|
|
56
|
-
'runwork dev # start
|
|
70
|
+
'runwork dev --detach --json # FIRST: start dev in the background, capture the preview URL',
|
|
57
71
|
'Review existing files to understand what is already built',
|
|
58
|
-
'
|
|
72
|
+
'THEN edit files for your needs -- changes auto-sync to the preview',
|
|
59
73
|
'runwork deploy # deploy to production when ready',
|
|
60
74
|
],
|
|
61
75
|
tips: [SYSTEM_DEPENDENCIES_NOTE, ...COMMON_TIPS],
|
|
@@ -89,17 +103,18 @@ export function buildDeployGuide() {
|
|
|
89
103
|
}
|
|
90
104
|
export function buildInfoGuide() {
|
|
91
105
|
return {
|
|
92
|
-
context: 'This shows the current state of the app: what is deployed, what integrations are connected, and what resources (entities, workflows, agents, etc.) are registered.',
|
|
106
|
+
context: 'This shows the current state of the app: what is deployed, what integrations are connected, and what resources (entities, workflows, agents, etc.) are registered. The `localDevSession` field tells you whether a dev session is already running on this machine -- check it BEFORE starting a new one.',
|
|
93
107
|
nextSteps: [
|
|
94
|
-
'
|
|
108
|
+
'If localDevSession.state == "alive": dev is already running. Use the URL in localDevSession.previewUrl and proceed with edits.',
|
|
109
|
+
'If localDevSession.state == "none" or "stale": run `runwork dev --detach --json` BEFORE editing any code. Capture the preview URL from the session_started event.',
|
|
95
110
|
'runwork deploy # deploy to production when ready',
|
|
96
111
|
],
|
|
97
112
|
tips: [
|
|
113
|
+
DEV_FIRST_RULE,
|
|
98
114
|
'Entities listed here are the data models available via Entity CRUD methods.',
|
|
99
115
|
'Workflows listed here can be triggered via their registered endpoints or schedules.',
|
|
100
116
|
'Agents listed here need frontend pages (conversational) or triggers (task) to be accessible.',
|
|
101
117
|
'Integrations with connected status are ready to use. Others need setup in workspace settings.',
|
|
102
|
-
'If preview is not active, run runwork dev to start a development session.',
|
|
103
118
|
],
|
|
104
119
|
};
|
|
105
120
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin wrappers around `child_process.execFileSync` and `spawn` that
|
|
3
|
+
* default `windowsHide: true`. On Windows, every console-subsystem
|
|
4
|
+
* subprocess we spawn (git, where.exe, registry queries) creates its
|
|
5
|
+
* own console window unless this flag is set. With our hot paths --
|
|
6
|
+
* auto-commit's git invocations, manifest's `git ls-files`, the sync
|
|
7
|
+
* loop's git rebase / fetch / push -- a missing flag visibly flashes a
|
|
8
|
+
* console window every few seconds, which Codex Desktop users see as
|
|
9
|
+
* "empty terminals keep popping up."
|
|
10
|
+
*
|
|
11
|
+
* Use these as drop-in replacements for the `child_process` exports.
|
|
12
|
+
* Existing options the caller passes still win (so you can opt back to
|
|
13
|
+
* `windowsHide: false` if you genuinely need the window, e.g. for
|
|
14
|
+
* interactive prompts -- though we don't have any of those in our
|
|
15
|
+
* subprocess paths today).
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync as cpExecFileSync, spawn as cpSpawn } from 'child_process';
|
|
18
|
+
export declare const execFileSync: typeof cpExecFileSync;
|
|
19
|
+
export declare const spawn: typeof cpSpawn;
|