runwork 0.10.3 → 0.10.4

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.
@@ -199,6 +199,7 @@ describe('ClaudeCodeAdapter.readVersion', () => {
199
199
  expect(version).toBe('claude-code 1.0.23');
200
200
  expect(execFileSync).toHaveBeenCalledWith('claude', ['--version'], {
201
201
  stdio: 'pipe',
202
+ windowsHide: true,
202
203
  });
203
204
  });
204
205
  it('returns null when claude is not installed', async () => {
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir, platform } from 'os';
4
- import { execFileSync } from 'child_process';
4
+ import { execFileSync } from '../utils/subprocess.js';
5
5
  import { whichBinary } from '../utils/which.js';
6
6
  import { buildSkillMd } from './types.js';
7
7
  import { mergeJsonMcpServers, readJsonConfig, writeJsonConfig, removeRunworkMcpServers } from './utils/json-config.js';
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir, platform } from 'os';
4
- import { execFileSync } from 'child_process';
4
+ import { execFileSync } from '../utils/subprocess.js';
5
5
  import { querySqlite, openWritableSqlite } from '../utils/sqlite.js';
6
6
  import { whichBinary } from '../utils/which.js';
7
7
  import { mergeJsonMcpServers, removeRunworkMcpServers, readJsonConfig, writeJsonConfig } from './utils/json-config.js';
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { execFileSync } from 'child_process';
2
+ import { execFileSync } from '../utils/subprocess.js';
3
3
  import { writeFileSync, mkdirSync, existsSync } from 'fs';
4
4
  import { join, resolve } from 'path';
5
5
  import { requireAuth } from '../auth/store.js';
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { execFileSync } from 'child_process';
2
+ import { execFileSync } from '../utils/subprocess.js';
3
3
  import { readFileSync, existsSync } from 'fs';
4
4
  import { requireAuth } from '../auth/store.js';
5
5
  import { ApiClient } from '../api/client.js';
@@ -1,5 +1,5 @@
1
1
  import { Command, Option } from 'commander';
2
- import { execFileSync } from 'child_process';
2
+ import { execFileSync } from '../utils/subprocess.js';
3
3
  import { readFileSync, writeFileSync, existsSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { requireAuth } from '../auth/store.js';
@@ -11,7 +11,7 @@ import { requireGit } from '../git/preflight.js';
11
11
  import { startLogTailer } from '../logs/tailer.js';
12
12
  import { startPreviewUrlPoller } from '../dev/preview-url-poller.js';
13
13
  import { buildSessionFile, getSessionState, readSessionFile, removeSessionFile, removeSessionFileIfOwned, writeSessionFile, } from '../dev/session.js';
14
- import { INTERNAL_DETACHED_CHILD_FLAG, isInternalDetachedChild, runAsDetachedParent, stripInternalChildFlag, } from '../dev/detach.js';
14
+ import { INTERNAL_DETACHED_CHILD_FLAG, buildChildArgs, isInternalDetachedChild, runAsDetachedParent, } from '../dev/detach.js';
15
15
  import { stopSession } from '../dev/stop.js';
16
16
  import { formatStartedAgo, getAttachLogPaths, getCurrentPreviewUrl, renderLogLine, resolveAttachTarget, startLogTail, startSessionFileWatch, } from '../dev/attach.js';
17
17
  import { populateTypes } from '../types-manager.js';
@@ -164,19 +164,31 @@ export async function execDev(options) {
164
164
  // fresh machine where `git config --global user.email/.name` has never
165
165
  // been set (extremely common on Windows after a clean install).
166
166
  ensureGitIdentity(cwd, creds);
167
- // Detect user edits made outside of `runwork dev`
167
+ // Detect user edits made outside of `runwork dev`. When this fires
168
+ // for an AI agent, it almost always means the agent edited code
169
+ // before starting dev -- the wrong order. We surface this loudly so
170
+ // the agent's tool-result-handling can correct course on the next
171
+ // task. The recommendation is canonical: start `runwork dev --detach`
172
+ // first, then edit. See DEV_FIRST_RULE.
168
173
  const oldManifest = await loadManifest(cwd);
169
174
  if (oldManifest) {
170
175
  const userEdits = await detectUserEdits(cwd, oldManifest);
171
176
  if (userEdits.length > 0) {
172
177
  if (useJson) {
173
- jsonLine({ event: 'startup', phase: 'user_edits_detected', files: userEdits, timestamp: ts() });
178
+ jsonLine({
179
+ event: 'startup',
180
+ phase: 'user_edits_detected',
181
+ files: userEdits,
182
+ timestamp: ts(),
183
+ warning: `Detected ${userEdits.length} file(s) edited before \`runwork dev\` was running. These will be batch-synced now. Next time, start \`runwork dev --detach\` BEFORE editing so changes flow incrementally and you can verify each edit against the preview.`,
184
+ });
174
185
  }
175
186
  else {
176
187
  console.log(`Detected ${userEdits.length} file(s) edited outside dev session:`);
177
188
  for (const file of userEdits) {
178
189
  console.log(` ${file}`);
179
190
  }
191
+ console.log(yellow(` Next time, start \`runwork dev --detach\` BEFORE editing -- changes will sync incrementally and the preview will validate each one.`));
180
192
  }
181
193
  try {
182
194
  execFileSync('git', ['add', '--', ...userEdits], { stdio: 'pipe' });
@@ -552,20 +564,12 @@ async function runDevDetachParent(opts) {
552
564
  else {
553
565
  console.log(dim('Starting dev session in background...'));
554
566
  }
555
- // Construct the child's args from our own process.argv. We strip the
556
- // marker (so the parent can't accidentally fall into the child path
557
- // itself if invoked through a wrapper) and then add it back exactly
558
- // once.
559
- //
560
- // process.argv[0] is the executable, which we replace with
561
- // `process.execPath` at spawn time. We forward everything from argv[1]
562
- // onwards. This works for both Node (`node script.js dev --detach` ->
563
- // ["script.js", "dev", "--detach"]) and Bun-standalone binaries
564
- // (`runwork dev --detach` -> ["dev", "--detach"]) because in both cases
565
- // re-spawning with execPath + argv.slice(1) reproduces the same
566
- // invocation.
567
- const userArgs = stripInternalChildFlag(process.argv.slice(1));
568
- const childArgs = [...userArgs, INTERNAL_DETACHED_CHILD_FLAG];
567
+ // `buildChildArgs` reproduces our invocation as args for the child
568
+ // (which is spawned with the same `process.execPath`), with two
569
+ // platform-aware tweaks: it skips Bun's auto-injected virtual-FS path
570
+ // on Windows (which the child runtime re-injects on its own), and it
571
+ // ensures exactly one copy of the internal-child marker.
572
+ const childArgs = buildChildArgs(process.argv);
569
573
  const outcome = await runAsDetachedParent({
570
574
  appDir: cwd,
571
575
  expectedAppId: config.appId,
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { execFileSync } from 'child_process';
2
+ import { execFileSync } from '../utils/subprocess.js';
3
3
  import { writeFileSync, existsSync, mkdirSync } from 'fs';
4
4
  import { join, resolve } from 'path';
5
5
  import { homedir } from 'os';
@@ -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(() => {
@@ -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[];
@@ -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.10.3";
1
+ export declare const VERSION = "0.10.4";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/embed-types.ts -- do not edit
2
- export const VERSION = "0.10.3";
2
+ export const VERSION = "0.10.4";
@@ -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(() => {
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  import { readFileSync } from 'fs';
3
3
  import { watch } from 'chokidar';
4
4
  import { join, relative } from 'path';
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  import { getCredentials } from '../auth/store.js';
3
3
  /**
4
4
  * Build the value we hand to `git config credential.<origin>.helper`.
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  /**
3
3
  * Default identity used when we cannot derive anything better from the
4
4
  * authenticated credentials. Local-scoped, never written to global config,
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  import { existsSync } from 'fs';
3
3
  import { win32 as winPath } from 'path';
4
4
  import { homedir } from 'os';
package/dist/git/sync.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  import { unlinkSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  export function hasCommits(cwd) {
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  import { existsSync, readFileSync } from 'fs';
3
3
  import { join, sep } from 'path';
4
4
  import { homedir, platform as osPlatform, arch as osArch } from 'os';
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'crypto';
2
- import { execFileSync } from 'child_process';
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';
@@ -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 development server -- watches for file changes and syncs automatically',
43
- 'Edit files for your needs (see structure above)',
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 development server',
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
- 'Edit files for your needs',
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
- 'runwork dev # start development server if not already running',
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;
@@ -0,0 +1,27 @@
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
+ // We re-export with the *exact* `typeof` signature of the originals so
19
+ // caller-side overload narrowing (encoding -> string vs Buffer return
20
+ // type) keeps working. The implementation forwards through the original
21
+ // after merging in `windowsHide: true` as a default.
22
+ export const execFileSync = ((file, args, options) => {
23
+ return cpExecFileSync(file, args, { windowsHide: true, ...(options ?? {}) });
24
+ });
25
+ export const spawn = ((command, args, options) => {
26
+ return cpSpawn(command, args ?? [], { windowsHide: true, ...(options ?? {}) });
27
+ });
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from './subprocess.js';
2
2
  import { platform } from 'os';
3
3
  /**
4
4
  * Cross-platform binary detection.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Runwork <info@runwork.ai> (https://www.runwork.ai)",