runwork 0.15.1 → 0.16.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.
Files changed (404) hide show
  1. package/bundled-types/task-delegation/task-delegation/types.d.ts +6 -0
  2. package/dist/index.js +20237 -116
  3. package/package.json +4 -4
  4. package/dist/__tests__/curl-parser.test.d.ts +0 -1
  5. package/dist/__tests__/curl-parser.test.js +0 -250
  6. package/dist/__tests__/install-scripts.test.d.ts +0 -1
  7. package/dist/__tests__/install-scripts.test.js +0 -280
  8. package/dist/__tests__/types-manager.test.d.ts +0 -1
  9. package/dist/__tests__/types-manager.test.js +0 -89
  10. package/dist/agents/__tests__/chatgpt-registry.test.d.ts +0 -1
  11. package/dist/agents/__tests__/chatgpt-registry.test.js +0 -61
  12. package/dist/agents/__tests__/claude-code-managed-block.test.d.ts +0 -1
  13. package/dist/agents/__tests__/claude-code-managed-block.test.js +0 -105
  14. package/dist/agents/__tests__/claude-code-plugin.test.d.ts +0 -1
  15. package/dist/agents/__tests__/claude-code-plugin.test.js +0 -47
  16. package/dist/agents/__tests__/claude-code-stats.test.d.ts +0 -1
  17. package/dist/agents/__tests__/claude-code-stats.test.js +0 -390
  18. package/dist/agents/__tests__/claude-desktop-memory.test.d.ts +0 -1
  19. package/dist/agents/__tests__/claude-desktop-memory.test.js +0 -154
  20. package/dist/agents/__tests__/claude-desktop-plugin-tree.test.d.ts +0 -1
  21. package/dist/agents/__tests__/claude-desktop-plugin-tree.test.js +0 -161
  22. package/dist/agents/__tests__/claude-desktop-plugin.test.d.ts +0 -1
  23. package/dist/agents/__tests__/claude-desktop-plugin.test.js +0 -117
  24. package/dist/agents/__tests__/claude-desktop-rpm.test.d.ts +0 -1
  25. package/dist/agents/__tests__/claude-desktop-rpm.test.js +0 -126
  26. package/dist/agents/__tests__/claude-desktop-stats.test.d.ts +0 -1
  27. package/dist/agents/__tests__/claude-desktop-stats.test.js +0 -280
  28. package/dist/agents/__tests__/codex-instructions.test.d.ts +0 -1
  29. package/dist/agents/__tests__/codex-instructions.test.js +0 -57
  30. package/dist/agents/__tests__/codex-minimum-permissions.test.d.ts +0 -1
  31. package/dist/agents/__tests__/codex-minimum-permissions.test.js +0 -82
  32. package/dist/agents/__tests__/codex-stats.test.d.ts +0 -1
  33. package/dist/agents/__tests__/codex-stats.test.js +0 -370
  34. package/dist/agents/__tests__/cursor-merge.test.d.ts +0 -1
  35. package/dist/agents/__tests__/cursor-merge.test.js +0 -66
  36. package/dist/agents/__tests__/cursor-stats.test.d.ts +0 -1
  37. package/dist/agents/__tests__/cursor-stats.test.js +0 -183
  38. package/dist/agents/__tests__/defaults-merge.test.d.ts +0 -1
  39. package/dist/agents/__tests__/defaults-merge.test.js +0 -268
  40. package/dist/agents/__tests__/detection.test.d.ts +0 -1
  41. package/dist/agents/__tests__/detection.test.js +0 -89
  42. package/dist/agents/__tests__/graceful-degradation.test.d.ts +0 -1
  43. package/dist/agents/__tests__/graceful-degradation.test.js +0 -129
  44. package/dist/agents/__tests__/instruction-hint.test.d.ts +0 -1
  45. package/dist/agents/__tests__/instruction-hint.test.js +0 -69
  46. package/dist/agents/__tests__/intro-skill.test.d.ts +0 -1
  47. package/dist/agents/__tests__/intro-skill.test.js +0 -193
  48. package/dist/agents/__tests__/schema-validation.test.d.ts +0 -1
  49. package/dist/agents/__tests__/schema-validation.test.js +0 -212
  50. package/dist/agents/__tests__/skill-slug.test.d.ts +0 -1
  51. package/dist/agents/__tests__/skill-slug.test.js +0 -77
  52. package/dist/agents/claude-code.d.ts +0 -59
  53. package/dist/agents/claude-code.js +0 -753
  54. package/dist/agents/claude-desktop-plugin-tree.d.ts +0 -59
  55. package/dist/agents/claude-desktop-plugin-tree.js +0 -143
  56. package/dist/agents/claude-desktop.d.ts +0 -47
  57. package/dist/agents/claude-desktop.js +0 -684
  58. package/dist/agents/cline.d.ts +0 -15
  59. package/dist/agents/cline.js +0 -130
  60. package/dist/agents/codex.d.ts +0 -44
  61. package/dist/agents/codex.js +0 -453
  62. package/dist/agents/conversation-skills.d.ts +0 -18
  63. package/dist/agents/conversation-skills.js +0 -229
  64. package/dist/agents/cursor.d.ts +0 -35
  65. package/dist/agents/cursor.js +0 -294
  66. package/dist/agents/default-config.d.ts +0 -30
  67. package/dist/agents/default-config.js +0 -67
  68. package/dist/agents/defaults-merge.d.ts +0 -87
  69. package/dist/agents/defaults-merge.js +0 -150
  70. package/dist/agents/detect.d.ts +0 -4
  71. package/dist/agents/detect.js +0 -67
  72. package/dist/agents/detection.d.ts +0 -18
  73. package/dist/agents/detection.js +0 -94
  74. package/dist/agents/gemini.d.ts +0 -15
  75. package/dist/agents/gemini.js +0 -122
  76. package/dist/agents/generic-adapter.d.ts +0 -25
  77. package/dist/agents/generic-adapter.js +0 -151
  78. package/dist/agents/intro-skill.d.ts +0 -45
  79. package/dist/agents/intro-skill.js +0 -447
  80. package/dist/agents/registry-data.d.ts +0 -192
  81. package/dist/agents/registry-data.js +0 -669
  82. package/dist/agents/registry.d.ts +0 -13
  83. package/dist/agents/registry.js +0 -36
  84. package/dist/agents/runtime-detection.d.ts +0 -82
  85. package/dist/agents/runtime-detection.js +0 -271
  86. package/dist/agents/session-start-hook.d.ts +0 -37
  87. package/dist/agents/session-start-hook.js +0 -159
  88. package/dist/agents/types.d.ts +0 -171
  89. package/dist/agents/types.js +0 -75
  90. package/dist/agents/utils/instruction-hint.d.ts +0 -21
  91. package/dist/agents/utils/instruction-hint.js +0 -108
  92. package/dist/agents/utils/json-config.d.ts +0 -19
  93. package/dist/agents/utils/json-config.js +0 -69
  94. package/dist/agents/utils/toml-config.d.ts +0 -8
  95. package/dist/agents/utils/toml-config.js +0 -27
  96. package/dist/agents/windsurf.d.ts +0 -14
  97. package/dist/agents/windsurf.js +0 -102
  98. package/dist/api/__tests__/client.test.d.ts +0 -1
  99. package/dist/api/__tests__/client.test.js +0 -272
  100. package/dist/api/client.d.ts +0 -326
  101. package/dist/api/client.js +0 -427
  102. package/dist/auth/__tests__/login-flow.test.d.ts +0 -1
  103. package/dist/auth/__tests__/login-flow.test.js +0 -101
  104. package/dist/auth/__tests__/store.test.d.ts +0 -1
  105. package/dist/auth/__tests__/store.test.js +0 -132
  106. package/dist/auth/login-flow.d.ts +0 -26
  107. package/dist/auth/login-flow.js +0 -137
  108. package/dist/auth/store.d.ts +0 -5
  109. package/dist/auth/store.js +0 -62
  110. package/dist/commands/__tests__/clone-args.test.d.ts +0 -1
  111. package/dist/commands/__tests__/clone-args.test.js +0 -51
  112. package/dist/commands/__tests__/clone-source-result.test.d.ts +0 -1
  113. package/dist/commands/__tests__/clone-source-result.test.js +0 -45
  114. package/dist/commands/__tests__/info-merge.test.d.ts +0 -1
  115. package/dist/commands/__tests__/info-merge.test.js +0 -55
  116. package/dist/commands/__tests__/mcp-entries.test.d.ts +0 -1
  117. package/dist/commands/__tests__/mcp-entries.test.js +0 -48
  118. package/dist/commands/__tests__/setup-persona.test.d.ts +0 -1
  119. package/dist/commands/__tests__/setup-persona.test.js +0 -31
  120. package/dist/commands/__tests__/setup.test.d.ts +0 -1
  121. package/dist/commands/__tests__/setup.test.js +0 -81
  122. package/dist/commands/__tests__/sync-redetect.test.d.ts +0 -1
  123. package/dist/commands/__tests__/sync-redetect.test.js +0 -96
  124. package/dist/commands/__tests__/sync-telemetry.test.d.ts +0 -1
  125. package/dist/commands/__tests__/sync-telemetry.test.js +0 -281
  126. package/dist/commands/__tests__/upgrade.test.d.ts +0 -1
  127. package/dist/commands/__tests__/upgrade.test.js +0 -157
  128. package/dist/commands/agents.d.ts +0 -2
  129. package/dist/commands/agents.js +0 -160
  130. package/dist/commands/api-keys.d.ts +0 -2
  131. package/dist/commands/api-keys.js +0 -95
  132. package/dist/commands/apps.d.ts +0 -2
  133. package/dist/commands/apps.js +0 -82
  134. package/dist/commands/build-plugin.d.ts +0 -2
  135. package/dist/commands/build-plugin.js +0 -135
  136. package/dist/commands/clone.d.ts +0 -60
  137. package/dist/commands/clone.js +0 -271
  138. package/dist/commands/components.d.ts +0 -2
  139. package/dist/commands/components.js +0 -43
  140. package/dist/commands/deploy.d.ts +0 -2
  141. package/dist/commands/deploy.js +0 -279
  142. package/dist/commands/dev.d.ts +0 -11
  143. package/dist/commands/dev.js +0 -1011
  144. package/dist/commands/doctor.d.ts +0 -2
  145. package/dist/commands/doctor.js +0 -230
  146. package/dist/commands/endpoints.d.ts +0 -2
  147. package/dist/commands/endpoints.js +0 -189
  148. package/dist/commands/entities.d.ts +0 -2
  149. package/dist/commands/entities.js +0 -198
  150. package/dist/commands/files.d.ts +0 -2
  151. package/dist/commands/files.js +0 -193
  152. package/dist/commands/inbox.d.ts +0 -11
  153. package/dist/commands/inbox.js +0 -60
  154. package/dist/commands/info.d.ts +0 -178
  155. package/dist/commands/info.js +0 -455
  156. package/dist/commands/init.d.ts +0 -21
  157. package/dist/commands/init.js +0 -185
  158. package/dist/commands/integrations.d.ts +0 -2
  159. package/dist/commands/integrations.js +0 -233
  160. package/dist/commands/login.d.ts +0 -2
  161. package/dist/commands/login.js +0 -44
  162. package/dist/commands/logout.d.ts +0 -2
  163. package/dist/commands/logout.js +0 -13
  164. package/dist/commands/logs.d.ts +0 -2
  165. package/dist/commands/logs.js +0 -232
  166. package/dist/commands/mcp-entries.d.ts +0 -13
  167. package/dist/commands/mcp-entries.js +0 -26
  168. package/dist/commands/mcp.d.ts +0 -2
  169. package/dist/commands/mcp.js +0 -245
  170. package/dist/commands/open.d.ts +0 -2
  171. package/dist/commands/open.js +0 -53
  172. package/dist/commands/resume.d.ts +0 -2
  173. package/dist/commands/resume.js +0 -289
  174. package/dist/commands/save-convo.d.ts +0 -8
  175. package/dist/commands/save-convo.js +0 -20
  176. package/dist/commands/schedules.d.ts +0 -2
  177. package/dist/commands/schedules.js +0 -221
  178. package/dist/commands/setup.d.ts +0 -27
  179. package/dist/commands/setup.js +0 -177
  180. package/dist/commands/share-convo.d.ts +0 -24
  181. package/dist/commands/share-convo.js +0 -167
  182. package/dist/commands/skills.d.ts +0 -2
  183. package/dist/commands/skills.js +0 -352
  184. package/dist/commands/sync-telemetry.d.ts +0 -33
  185. package/dist/commands/sync-telemetry.js +0 -186
  186. package/dist/commands/sync.d.ts +0 -37
  187. package/dist/commands/sync.js +0 -702
  188. package/dist/commands/uninstall.d.ts +0 -2
  189. package/dist/commands/uninstall.js +0 -145
  190. package/dist/commands/upgrade.d.ts +0 -14
  191. package/dist/commands/upgrade.js +0 -217
  192. package/dist/commands/validate.d.ts +0 -2
  193. package/dist/commands/validate.js +0 -89
  194. package/dist/commands/welcome.d.ts +0 -1
  195. package/dist/commands/welcome.js +0 -86
  196. package/dist/commands/workflows.d.ts +0 -2
  197. package/dist/commands/workflows.js +0 -187
  198. package/dist/deploy/__tests__/deploy-state.test.d.ts +0 -1
  199. package/dist/deploy/__tests__/deploy-state.test.js +0 -80
  200. package/dist/deploy/__tests__/deploy-status.test.d.ts +0 -1
  201. package/dist/deploy/__tests__/deploy-status.test.js +0 -41
  202. package/dist/deploy/__tests__/detach-args.test.d.ts +0 -1
  203. package/dist/deploy/__tests__/detach-args.test.js +0 -30
  204. package/dist/deploy/deploy-state.d.ts +0 -36
  205. package/dist/deploy/deploy-state.js +0 -63
  206. package/dist/deploy/deploy-status.d.ts +0 -21
  207. package/dist/deploy/deploy-status.js +0 -36
  208. package/dist/deploy/detach.d.ts +0 -32
  209. package/dist/deploy/detach.js +0 -71
  210. package/dist/dev/__tests__/attach.test.d.ts +0 -1
  211. package/dist/dev/__tests__/attach.test.js +0 -296
  212. package/dist/dev/__tests__/detach.test.d.ts +0 -1
  213. package/dist/dev/__tests__/detach.test.js +0 -404
  214. package/dist/dev/__tests__/preview-url-poller.test.d.ts +0 -1
  215. package/dist/dev/__tests__/preview-url-poller.test.js +0 -149
  216. package/dist/dev/__tests__/session.test.d.ts +0 -1
  217. package/dist/dev/__tests__/session.test.js +0 -446
  218. package/dist/dev/__tests__/startup-sync.test.d.ts +0 -1
  219. package/dist/dev/__tests__/startup-sync.test.js +0 -32
  220. package/dist/dev/__tests__/stop.test.d.ts +0 -1
  221. package/dist/dev/__tests__/stop.test.js +0 -172
  222. package/dist/dev/attach.d.ts +0 -120
  223. package/dist/dev/attach.js +0 -269
  224. package/dist/dev/detach.d.ts +0 -187
  225. package/dist/dev/detach.js +0 -292
  226. package/dist/dev/preview-url-poller.d.ts +0 -35
  227. package/dist/dev/preview-url-poller.js +0 -50
  228. package/dist/dev/session.d.ts +0 -175
  229. package/dist/dev/session.js +0 -258
  230. package/dist/dev/startup-sync.d.ts +0 -23
  231. package/dist/dev/startup-sync.js +0 -14
  232. package/dist/dev/stop.d.ts +0 -52
  233. package/dist/dev/stop.js +0 -101
  234. package/dist/devtools/registry-data.d.ts +0 -46
  235. package/dist/devtools/registry-data.js +0 -40
  236. package/dist/generated/bundled-types.d.ts +0 -1
  237. package/dist/generated/bundled-types.js +0 -46
  238. package/dist/generated/version.d.ts +0 -1
  239. package/dist/generated/version.js +0 -2
  240. package/dist/git/__tests__/auto-commit.test.d.ts +0 -1
  241. package/dist/git/__tests__/auto-commit.test.js +0 -431
  242. package/dist/git/__tests__/classify-sync-error.test.d.ts +0 -1
  243. package/dist/git/__tests__/classify-sync-error.test.js +0 -57
  244. package/dist/git/__tests__/credential-helper-e2e.test.d.ts +0 -21
  245. package/dist/git/__tests__/credential-helper-e2e.test.js +0 -195
  246. package/dist/git/__tests__/credentials.test.d.ts +0 -1
  247. package/dist/git/__tests__/credentials.test.js +0 -141
  248. package/dist/git/__tests__/deploy-guard.test.d.ts +0 -1
  249. package/dist/git/__tests__/deploy-guard.test.js +0 -61
  250. package/dist/git/__tests__/ensure-git-credential-helper.test.d.ts +0 -1
  251. package/dist/git/__tests__/ensure-git-credential-helper.test.js +0 -133
  252. package/dist/git/__tests__/ensure-runwork-remote.test.d.ts +0 -1
  253. package/dist/git/__tests__/ensure-runwork-remote.test.js +0 -109
  254. package/dist/git/__tests__/identity.test.d.ts +0 -1
  255. package/dist/git/__tests__/identity.test.js +0 -146
  256. package/dist/git/__tests__/manifest.test.d.ts +0 -1
  257. package/dist/git/__tests__/manifest.test.js +0 -446
  258. package/dist/git/__tests__/preflight-resolution.test.d.ts +0 -1
  259. package/dist/git/__tests__/preflight-resolution.test.js +0 -366
  260. package/dist/git/__tests__/preflight.test.d.ts +0 -1
  261. package/dist/git/__tests__/preflight.test.js +0 -36
  262. package/dist/git/__tests__/repo-config.test.d.ts +0 -1
  263. package/dist/git/__tests__/repo-config.test.js +0 -26
  264. package/dist/git/__tests__/sync.test.d.ts +0 -1
  265. package/dist/git/__tests__/sync.test.js +0 -405
  266. package/dist/git/auto-commit.d.ts +0 -35
  267. package/dist/git/auto-commit.js +0 -293
  268. package/dist/git/classify-sync-error.d.ts +0 -21
  269. package/dist/git/classify-sync-error.js +0 -111
  270. package/dist/git/credentials.d.ts +0 -75
  271. package/dist/git/credentials.js +0 -164
  272. package/dist/git/critical-files.d.ts +0 -20
  273. package/dist/git/critical-files.js +0 -68
  274. package/dist/git/deploy-guard.d.ts +0 -53
  275. package/dist/git/deploy-guard.js +0 -78
  276. package/dist/git/identity.d.ts +0 -44
  277. package/dist/git/identity.js +0 -133
  278. package/dist/git/preflight.d.ts +0 -57
  279. package/dist/git/preflight.js +0 -276
  280. package/dist/git/remote.d.ts +0 -37
  281. package/dist/git/remote.js +0 -75
  282. package/dist/git/repo-config.d.ts +0 -8
  283. package/dist/git/repo-config.js +0 -29
  284. package/dist/git/sync.d.ts +0 -15
  285. package/dist/git/sync.js +0 -159
  286. package/dist/health/__tests__/checks.test.d.ts +0 -1
  287. package/dist/health/__tests__/checks.test.js +0 -357
  288. package/dist/health/__tests__/cli-distribution-checks.test.d.ts +0 -1
  289. package/dist/health/__tests__/cli-distribution-checks.test.js +0 -196
  290. package/dist/health/__tests__/fix.test.d.ts +0 -1
  291. package/dist/health/__tests__/fix.test.js +0 -72
  292. package/dist/health/__tests__/runner-filter.test.d.ts +0 -1
  293. package/dist/health/__tests__/runner-filter.test.js +0 -61
  294. package/dist/health/checks.d.ts +0 -42
  295. package/dist/health/checks.js +0 -636
  296. package/dist/health/fix.d.ts +0 -22
  297. package/dist/health/fix.js +0 -62
  298. package/dist/health/runner.d.ts +0 -8
  299. package/dist/health/runner.js +0 -47
  300. package/dist/health/types.d.ts +0 -12
  301. package/dist/health/types.js +0 -1
  302. package/dist/index.d.ts +0 -1
  303. package/dist/logs/__tests__/tailer-format.test.d.ts +0 -1
  304. package/dist/logs/__tests__/tailer-format.test.js +0 -43
  305. package/dist/logs/tailer.d.ts +0 -17
  306. package/dist/logs/tailer.js +0 -217
  307. package/dist/sync/__tests__/change-detect.test.d.ts +0 -1
  308. package/dist/sync/__tests__/change-detect.test.js +0 -123
  309. package/dist/sync/__tests__/hash.test.d.ts +0 -1
  310. package/dist/sync/__tests__/hash.test.js +0 -18
  311. package/dist/sync/change-detect.d.ts +0 -19
  312. package/dist/sync/change-detect.js +0 -136
  313. package/dist/sync/conflict-ui.d.ts +0 -5
  314. package/dist/sync/conflict-ui.js +0 -78
  315. package/dist/sync/executor.d.ts +0 -23
  316. package/dist/sync/executor.js +0 -102
  317. package/dist/sync/hash.d.ts +0 -1
  318. package/dist/sync/hash.js +0 -4
  319. package/dist/sync/types.d.ts +0 -24
  320. package/dist/sync/types.js +0 -1
  321. package/dist/template/manifest.d.ts +0 -17
  322. package/dist/template/manifest.js +0 -145
  323. package/dist/tools/types.d.ts +0 -52
  324. package/dist/tools/types.js +0 -8
  325. package/dist/types-manager.d.ts +0 -1
  326. package/dist/types-manager.js +0 -35
  327. package/dist/types.d.ts +0 -349
  328. package/dist/types.js +0 -1
  329. package/dist/ui/__tests__/banner.test.d.ts +0 -1
  330. package/dist/ui/__tests__/banner.test.js +0 -98
  331. package/dist/ui/__tests__/colors.test.d.ts +0 -1
  332. package/dist/ui/__tests__/colors.test.js +0 -22
  333. package/dist/ui/__tests__/keyboard.test.d.ts +0 -1
  334. package/dist/ui/__tests__/keyboard.test.js +0 -34
  335. package/dist/ui/__tests__/status-line.test.d.ts +0 -1
  336. package/dist/ui/__tests__/status-line.test.js +0 -54
  337. package/dist/ui/banner.d.ts +0 -29
  338. package/dist/ui/banner.js +0 -153
  339. package/dist/ui/colors.d.ts +0 -4
  340. package/dist/ui/colors.js +0 -7
  341. package/dist/ui/keyboard.d.ts +0 -12
  342. package/dist/ui/keyboard.js +0 -61
  343. package/dist/ui/status-line.d.ts +0 -6
  344. package/dist/ui/status-line.js +0 -53
  345. package/dist/utils/__tests__/format-error.test.d.ts +0 -1
  346. package/dist/utils/__tests__/format-error.test.js +0 -43
  347. package/dist/utils/__tests__/help-json.test.d.ts +0 -1
  348. package/dist/utils/__tests__/help-json.test.js +0 -39
  349. package/dist/utils/__tests__/http.test.d.ts +0 -1
  350. package/dist/utils/__tests__/http.test.js +0 -381
  351. package/dist/utils/__tests__/ignore-matcher.test.d.ts +0 -1
  352. package/dist/utils/__tests__/ignore-matcher.test.js +0 -205
  353. package/dist/utils/__tests__/output.test.d.ts +0 -1
  354. package/dist/utils/__tests__/output.test.js +0 -38
  355. package/dist/utils/__tests__/prompt.test.d.ts +0 -1
  356. package/dist/utils/__tests__/prompt.test.js +0 -27
  357. package/dist/utils/__tests__/resolve.test.d.ts +0 -1
  358. package/dist/utils/__tests__/resolve.test.js +0 -95
  359. package/dist/utils/__tests__/sqlite.test.d.ts +0 -1
  360. package/dist/utils/__tests__/sqlite.test.js +0 -89
  361. package/dist/utils/agent-guidance.d.ts +0 -47
  362. package/dist/utils/agent-guidance.js +0 -128
  363. package/dist/utils/app-info.d.ts +0 -70
  364. package/dist/utils/app-info.js +0 -167
  365. package/dist/utils/curl-parser.d.ts +0 -12
  366. package/dist/utils/curl-parser.js +0 -162
  367. package/dist/utils/data-input.d.ts +0 -1
  368. package/dist/utils/data-input.js +0 -38
  369. package/dist/utils/format-error.d.ts +0 -10
  370. package/dist/utils/format-error.js +0 -38
  371. package/dist/utils/fs.d.ts +0 -5
  372. package/dist/utils/fs.js +0 -23
  373. package/dist/utils/help-json.d.ts +0 -70
  374. package/dist/utils/help-json.js +0 -88
  375. package/dist/utils/http.d.ts +0 -46
  376. package/dist/utils/http.js +0 -421
  377. package/dist/utils/ignore-matcher.d.ts +0 -47
  378. package/dist/utils/ignore-matcher.js +0 -132
  379. package/dist/utils/output.d.ts +0 -17
  380. package/dist/utils/output.js +0 -27
  381. package/dist/utils/prompt.d.ts +0 -5
  382. package/dist/utils/prompt.js +0 -42
  383. package/dist/utils/resolve.d.ts +0 -12
  384. package/dist/utils/resolve.js +0 -52
  385. package/dist/utils/setup-state.d.ts +0 -3
  386. package/dist/utils/setup-state.js +0 -19
  387. package/dist/utils/sqlite-adapter.d.ts +0 -48
  388. package/dist/utils/sqlite-adapter.js +0 -147
  389. package/dist/utils/sqlite.d.ts +0 -14
  390. package/dist/utils/sqlite.js +0 -14
  391. package/dist/utils/subprocess.d.ts +0 -19
  392. package/dist/utils/subprocess.js +0 -27
  393. package/dist/utils/which.d.ts +0 -14
  394. package/dist/utils/which.js +0 -47
  395. package/dist/utils/zip.d.ts +0 -7
  396. package/dist/utils/zip.js +0 -42
  397. package/dist/validate/__tests__/validate.test.d.ts +0 -1
  398. package/dist/validate/__tests__/validate.test.js +0 -42
  399. package/dist/validate/freshness.d.ts +0 -28
  400. package/dist/validate/freshness.js +0 -48
  401. package/dist/validate/preview.d.ts +0 -21
  402. package/dist/validate/preview.js +0 -39
  403. package/dist/workspace/resolve.d.ts +0 -19
  404. package/dist/workspace/resolve.js +0 -100
@@ -1,1011 +0,0 @@
1
- import { Command, Option } from 'commander';
2
- import { execFileSync } from '../utils/subprocess.js';
3
- import { readFileSync, writeFileSync, existsSync } from 'fs';
4
- import { join } from 'path';
5
- import { requireAuth } from '../auth/store.js';
6
- import { ApiClient } from '../api/client.js';
7
- import { watchAndAutoCommit, stopAutoCommit } from '../git/auto-commit.js';
8
- import { syncWithRemote } from '../git/sync.js';
9
- import { snapshotCriticalFiles, restoreMissingCriticalFiles, commitAndPushRestoredFiles } from '../git/critical-files.js';
10
- import { ensureGitIdentity } from '../git/identity.js';
11
- import { ensureGitCredentialHelper } from '../git/credentials.js';
12
- import { ensureRunworkRemote } from '../git/remote.js';
13
- import { diagnoseSyncError } from '../git/classify-sync-error.js';
14
- import { buildStartupSyncSummary } from '../dev/startup-sync.js';
15
- import { requireGit } from '../git/preflight.js';
16
- import { startLogTailer } from '../logs/tailer.js';
17
- import { startPreviewUrlPoller } from '../dev/preview-url-poller.js';
18
- import { buildSessionFile, getSessionState, readSessionFile, removeSessionFile, removeSessionFileIfOwned, writeSessionFile, } from '../dev/session.js';
19
- import { INTERNAL_DETACHED_CHILD_FLAG, buildChildArgs, isInternalDetachedChild, runAsDetachedParent, } from '../dev/detach.js';
20
- import { stopSession } from '../dev/stop.js';
21
- import { formatStartedAgo, getAttachLogPaths, getCurrentPreviewUrl, renderLogLine, resolveAttachTarget, startLogTail, startSessionFileWatch, } from '../dev/attach.js';
22
- import { populateTypes } from '../types-manager.js';
23
- import { loadManifest, generateManifest, saveManifest, detectUserEdits } from '../template/manifest.js';
24
- import { extractZip } from '../utils/zip.js';
25
- import { removeNestedGitDirs } from '../utils/fs.js';
26
- import { getDevBanner, getKeyboardHints, getInfoPanel } from '../ui/banner.js';
27
- import { createStatusLine } from '../ui/status-line.js';
28
- import { createKeyboardListener } from '../ui/keyboard.js';
29
- import { bold, dim, green, yellow, cyan, red } from '../ui/colors.js';
30
- import { shouldOutputJson, jsonLine } from '../utils/output.js';
31
- import { buildDevSessionGuide } from '../utils/agent-guidance.js';
32
- import { VERSION } from '../generated/version.js';
33
- async function populateSkill(projectDir, client, appId) {
34
- try {
35
- const { skill } = await client.getAppSkill(appId);
36
- if (skill) {
37
- writeFileSync(join(projectDir, 'SKILL.md'), skill, 'utf-8');
38
- }
39
- }
40
- catch {
41
- // Best-effort: app may be new with no registries yet
42
- }
43
- }
44
- function readConfig() {
45
- if (!existsSync('.runwork.json')) {
46
- console.error('No .runwork.json found. Run `runwork init` first.');
47
- process.exit(1);
48
- }
49
- return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
50
- }
51
- export async function execDev(options) {
52
- const useJson = options?.json ?? false;
53
- const mode = options?.mode ?? 'foreground';
54
- // No-sync mode: hold the preview open but never auto-touch the user's
55
- // git or working tree. No file watcher, no auto-commit, and no
56
- // working-tree-mutating startup (template auto-update / syncWithRemote).
57
- // The user drives git manually; `git push` updates the preview.
58
- const noSync = options?.noSync ?? false;
59
- requireGit('dev');
60
- const config = readConfig();
61
- const creds = requireAuth();
62
- const client = new ApiClient(creds);
63
- const cwd = process.cwd();
64
- const ts = () => new Date().toISOString();
65
- // --restart: stop any pre-existing session before starting a new one.
66
- // We do this BEFORE the "already running" check so the user's intent
67
- // is respected even when a healthy session is already in place.
68
- if (options?.restart) {
69
- const stopOutcome = await stopSession(cwd, config.appId);
70
- if (!useJson && stopOutcome.result === 'stopped') {
71
- console.log(dim(`Stopped previous dev session (PID ${stopOutcome.pid}).`));
72
- }
73
- if (useJson && (stopOutcome.result === 'stopped' || stopOutcome.result === 'stale-cleaned')) {
74
- jsonLine({
75
- event: 'session_stopped',
76
- pid: 'pid' in stopOutcome ? stopOutcome.pid : undefined,
77
- timestamp: ts(),
78
- });
79
- }
80
- }
81
- // Idempotency check: read the session file before doing any work. If a
82
- // live session is already running for this app on this machine, exit
83
- // success with a structured event so the caller (human or agent) knows
84
- // the URL and can move on. Stale files are cleaned up silently.
85
- const existing = getSessionState(cwd, config.appId);
86
- if (existing.state === 'alive') {
87
- const f = existing.file;
88
- if (useJson) {
89
- jsonLine({
90
- event: 'already_running',
91
- previewUrl: f.previewUrl,
92
- pid: f.pid,
93
- sessionId: f.sessionId,
94
- mode: f.mode,
95
- timestamp: ts(),
96
- });
97
- }
98
- else {
99
- console.log(`Dev session already running (PID ${f.pid}, mode: ${f.mode}).`);
100
- console.log(` Preview: ${green(f.previewUrl || '(starting up)')}`);
101
- console.log(dim(` Follow logs: runwork dev attach`));
102
- console.log(dim(` Stop: runwork dev stop`));
103
- console.log(dim(` Start fresh: runwork dev --restart`));
104
- }
105
- return;
106
- }
107
- if (existing.state === 'stale') {
108
- removeSessionFile(cwd);
109
- if (!useJson) {
110
- console.log(dim(`Cleaned up dead session from a previous run (reason: ${existing.reason}).`));
111
- }
112
- }
113
- // Seed a local git identity so the very first commit cannot fail on a
114
- // fresh machine where `git config --global user.email/.name` has never
115
- // been set (extremely common on Windows after a clean install).
116
- ensureGitIdentity(cwd, creds);
117
- // Ensure git can authenticate to the runwork remote before the sync loop
118
- // fetches/pushes. Headless auth (get_cli_setup / hand-written credentials)
119
- // registers API creds but not the git helper. See
120
- // git/credentials.ts:ensureGitCredentialHelper.
121
- if (creds?.baseUrl) {
122
- await ensureGitCredentialHelper(creds.baseUrl);
123
- }
124
- // Ensure the `runwork` remote exists and points at the canonical URL. A
125
- // manual `git clone <url>` names the remote `origin`, which otherwise makes
126
- // the sync loop fail with "'runwork' does not appear to be a git repository".
127
- ensureRunworkRemote(cwd, client.getGitRemoteUrl(config.workspaceId, config.appId));
128
- // Detect user edits made outside of `runwork dev`. When this fires
129
- // for an AI agent, it almost always means the agent edited code
130
- // before starting dev -- the wrong order. We surface this loudly so
131
- // the agent's tool-result-handling can correct course on the next
132
- // task. The recommendation is canonical: start `runwork dev --detach`
133
- // first, then edit. See DEV_FIRST_RULE.
134
- const oldManifest = await loadManifest(cwd);
135
- if (oldManifest) {
136
- const userEdits = await detectUserEdits(cwd, oldManifest);
137
- if (userEdits.length > 0) {
138
- if (useJson) {
139
- jsonLine({
140
- event: 'startup',
141
- phase: 'user_edits_detected',
142
- files: userEdits,
143
- timestamp: ts(),
144
- 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.`,
145
- });
146
- }
147
- else {
148
- console.log(`Detected ${userEdits.length} file(s) edited outside dev session:`);
149
- for (const file of userEdits) {
150
- console.log(` ${file}`);
151
- }
152
- console.log(yellow(` Next time, start \`runwork dev --detach\` BEFORE editing -- changes will sync incrementally and the preview will validate each one.`));
153
- }
154
- try {
155
- execFileSync('git', ['add', '--', ...userEdits], { stdio: 'pipe' });
156
- execFileSync('git', ['commit', '-m', 'sync: user edits'], { stdio: 'pipe' });
157
- }
158
- catch {
159
- // Nothing to commit or git not initialized yet
160
- }
161
- }
162
- }
163
- // Download fresh template to ensure latest version. Skipped entirely in
164
- // no-sync mode: this block mutates the working tree (`git checkout -- .`),
165
- // which would fight the user's manual git workflow.
166
- if (!noSync) {
167
- if (useJson) {
168
- jsonLine({ event: 'startup', phase: 'template_update', timestamp: ts() });
169
- }
170
- else {
171
- console.log(dim('Updating template...'));
172
- }
173
- try {
174
- const zipData = await client.downloadSkeleton();
175
- extractZip(zipData, cwd);
176
- removeNestedGitDirs(cwd);
177
- const newManifest = await generateManifest(cwd);
178
- await saveManifest(cwd, newManifest);
179
- try {
180
- execFileSync('git', ['checkout', '--', '.'], { cwd, stdio: 'pipe' });
181
- }
182
- catch {
183
- // May fail if no commits yet
184
- }
185
- }
186
- catch {
187
- if (!useJson)
188
- console.warn(yellow('Template update failed. Continuing with current files.'));
189
- }
190
- }
191
- // Populate type definitions
192
- await populateTypes(cwd);
193
- if (useJson)
194
- jsonLine({ event: 'startup', phase: 'types_populated', timestamp: ts() });
195
- // Start dev session
196
- if (useJson) {
197
- jsonLine({ event: 'startup', phase: 'dev_session', timestamp: ts() });
198
- }
199
- else {
200
- console.log(dim('Starting dev session...'));
201
- }
202
- const session = await client.startDevSession(config.appId);
203
- // Fetch SKILL.md (best-effort)
204
- await populateSkill(cwd, client, config.appId);
205
- if (useJson)
206
- jsonLine({ event: 'startup', phase: 'skill_fetched', timestamp: ts() });
207
- // Track the startup sync outcome so `session_started` can tell agents
208
- // whether the preview reflects their latest edits or is serving stale code
209
- // (friction log #2: a happy session_started masked a failed initial sync).
210
- let startupSync = buildStartupSyncSummary({ noSync });
211
- // Sync. In no-sync mode we never mutate the working tree: instead of
212
- // syncWithRemote (rebase/merge/commit/push) we do a best-effort,
213
- // read-only `git fetch` so remote-tracking refs stay current and
214
- // `runwork info` can still report ahead/behind accurately.
215
- if (noSync) {
216
- try {
217
- execFileSync('git', ['fetch', 'runwork'], { cwd, stdio: 'pipe' });
218
- }
219
- catch {
220
- // Offline, or the `runwork` remote does not exist yet. Non-fatal.
221
- }
222
- if (useJson) {
223
- jsonLine({ event: 'startup', phase: 'fetch', timestamp: ts() });
224
- }
225
- else {
226
- console.log(dim('Fetched remote refs (no-sync mode).'));
227
- }
228
- }
229
- else {
230
- if (!useJson)
231
- console.log(dim('Syncing...'));
232
- const criticalSnapshot = snapshotCriticalFiles(cwd);
233
- const syncResult = syncWithRemote(cwd);
234
- startupSync = buildStartupSyncSummary({ noSync: false, status: syncResult.status });
235
- const restoredCriticalFiles = restoreMissingCriticalFiles(cwd, criticalSnapshot);
236
- if (restoredCriticalFiles.length > 0) {
237
- const pushed = commitAndPushRestoredFiles(cwd, restoredCriticalFiles);
238
- if (useJson) {
239
- jsonLine({
240
- event: 'sync_restored_critical_files',
241
- files: restoredCriticalFiles,
242
- pushed,
243
- timestamp: ts(),
244
- });
245
- }
246
- else {
247
- console.warn(yellow(` Sync removed critical file(s); restored: ${restoredCriticalFiles.join(', ')}`));
248
- if (!pushed) {
249
- console.warn(dim(' Restoration committed locally but not pushed; will retry on next auto-sync.'));
250
- }
251
- }
252
- }
253
- if (useJson) {
254
- jsonLine({ event: 'startup', phase: 'sync', status: syncResult.status, pushed: syncResult.pushed, timestamp: ts() });
255
- if (syncResult.status === 'sync-failed') {
256
- const diag = diagnoseSyncError(syncResult.error);
257
- jsonLine({
258
- event: 'error',
259
- phase: 'sync',
260
- timestamp: ts(),
261
- error: {
262
- reason: diag.reason,
263
- message: diag.message,
264
- diagnosis: diag.diagnosis,
265
- suggestions: diag.suggestions,
266
- },
267
- });
268
- // A stash-pop conflict leaves the working tree in a state the user
269
- // must resolve, so we stop. Other sync failures are non-fatal -- the
270
- // preview keeps serving the last good state -- so we continue.
271
- if (syncResult.error === 'stash-conflict') {
272
- process.exit(1);
273
- }
274
- }
275
- }
276
- else {
277
- switch (syncResult.status) {
278
- case 'skipped':
279
- console.log(dim(' No commits yet. Skipping sync.'));
280
- break;
281
- case 'synced':
282
- console.log(green(' Synced with Runwork.'));
283
- break;
284
- case 'merged':
285
- console.log(yellow(' Merged with Runwork (histories diverged).'));
286
- break;
287
- case 'sync-failed': {
288
- const diag = diagnoseSyncError(syncResult.error);
289
- if (syncResult.error === 'stash-conflict') {
290
- console.error(`${diag.message}. ${diag.diagnosis}`);
291
- for (const s of diag.suggestions)
292
- console.error(dim(` - ${s}`));
293
- process.exit(1);
294
- }
295
- console.warn(yellow(`Sync failed: ${diag.message}. Continuing...`));
296
- console.warn(dim(` ${diag.diagnosis}`));
297
- for (const s of diag.suggestions)
298
- console.warn(dim(` - ${s}`));
299
- break;
300
- }
301
- }
302
- if (!syncResult.pushed && syncResult.status !== 'skipped' && syncResult.status !== 'sync-failed') {
303
- console.warn(yellow('Push failed. Continuing with current state...'));
304
- }
305
- }
306
- }
307
- // The session POST returns a snapshot of the preview URL at boot time.
308
- // Sandboxes can rotate that URL mid-session (instance replacement,
309
- // tunnel restart), so we treat this as the *initial* value and refresh
310
- // it via `getDevStatus` -- the same source `runwork info` reads from.
311
- // That keeps `dev` and `info` in agreement.
312
- let currentPreviewUrl = session.previewUrl;
313
- // Write the session file as soon as we have the URL. This is the
314
- // rendezvous moment for `runwork dev --detach` -- the parent is polling
315
- // `.runwork/dev-session.json` and exits 0 when this file appears with a
316
- // non-empty previewUrl matching the child's PID.
317
- writeSessionFile(cwd, buildSessionFile({
318
- pid: process.pid,
319
- sessionId: session.sessionId,
320
- appId: config.appId,
321
- previewUrl: currentPreviewUrl,
322
- cliVersion: VERSION,
323
- mode,
324
- noSync,
325
- // Persist the startup sync outcome so the detached parent can relay
326
- // stale-code warnings -- not just the child's own log (friction log #2).
327
- startupSync,
328
- }));
329
- // Emit session_started (JSON) or show banner (human). We always emit
330
- // session_started -- it's the rendezvous signal `runwork dev --detach`
331
- // polls for -- but carry the startup sync outcome so agents don't mistake a
332
- // stale preview for one reflecting their latest edits. `staleCode: true`
333
- // means the initial sync failed and the preview is serving the last good
334
- // state, not the current working tree.
335
- if (useJson) {
336
- jsonLine({
337
- event: 'session_started',
338
- previewUrl: currentPreviewUrl,
339
- appName: config.appName,
340
- syncStatus: startupSync.syncStatus,
341
- syncFailed: startupSync.syncFailed,
342
- staleCode: startupSync.staleCode,
343
- timestamp: ts(),
344
- guide: buildDevSessionGuide(),
345
- });
346
- }
347
- else {
348
- console.log(getDevBanner({ appName: config.appName, previewUrl: currentPreviewUrl, workspaceName: config.workspaceName }));
349
- if (startupSync.syncFailed) {
350
- console.log(yellow(' Warning: initial sync failed. The preview shows the last deployed code, NOT your latest changes.'));
351
- }
352
- console.log(getKeyboardHints());
353
- console.log('');
354
- console.log(dim(' ─────────────────────────────────────────────'));
355
- console.log('');
356
- }
357
- // Status line and keyboard only in human mode
358
- const statusLine = useJson ? null : createStatusLine(process.stdout);
359
- let syncedCount = 0;
360
- let logFilter = 'all';
361
- function updateStatus() {
362
- if (!statusLine)
363
- return;
364
- const filterLabel = logFilter === 'all' ? 'All' : logFilter === 'events' ? 'Events' : 'Runtime';
365
- const sep = '\x1b[90m\u2502\x1b[39m';
366
- statusLine.update(`${bold(config.appName)} ${sep} ${green(currentPreviewUrl)} ${sep} ${cyan(`${syncedCount} synced`)} ${sep} ${filterLabel} ${sep} \x1b[90mo\x1b[39m:open \x1b[90mi\x1b[39m:info \x1b[90ma/e/r\x1b[39m:filter \x1b[90mq\x1b[39m:quit`);
367
- }
368
- if (statusLine)
369
- updateStatus();
370
- const previewUrlPoller = startPreviewUrlPoller({
371
- client,
372
- appId: config.appId,
373
- initialUrl: currentPreviewUrl,
374
- onChange: (next, prev) => {
375
- currentPreviewUrl = next;
376
- // Persist the rotated URL into the session file so `runwork info`,
377
- // detached parent processes, and other observers see the same
378
- // preview URL we're showing in the status line.
379
- const current = readSessionFile(cwd);
380
- if (current && current.pid === process.pid) {
381
- writeSessionFile(cwd, { ...current, previewUrl: next });
382
- }
383
- if (useJson) {
384
- jsonLine({
385
- event: 'preview_url_changed',
386
- previewUrl: next,
387
- previousUrl: prev,
388
- appName: config.appName,
389
- timestamp: ts(),
390
- });
391
- }
392
- else {
393
- console.log(dim(`Preview URL changed: ${next}`));
394
- updateStatus();
395
- }
396
- },
397
- });
398
- let logTailer;
399
- const keyboard = useJson ? null : createKeyboardListener();
400
- const cleanup = async () => {
401
- // Remove the session file FIRST. If a Windows console-close-event
402
- // truncates our cleanup window, or if we crash later in this handler,
403
- // at least the file is gone -- the next `runwork dev` will see a
404
- // clean slate. Stale-detection on next-startup is the primary recovery
405
- // path; this is just polish for the graceful-exit case.
406
- //
407
- // Use the PID-guarded variant: if a startup race ended with another
408
- // process owning the session file, we must NOT delete it -- that
409
- // would leave the winner's session orphaned.
410
- try {
411
- removeSessionFileIfOwned(cwd, process.pid);
412
- }
413
- catch { /* best-effort */ }
414
- if (!useJson)
415
- console.log('\nStopping...');
416
- logTailer?.stop();
417
- previewUrlPoller.stop();
418
- await stopAutoCommit();
419
- statusLine?.destroy();
420
- keyboard?.stop();
421
- process.exit(0);
422
- };
423
- process.on('SIGINT', cleanup);
424
- process.on('SIGTERM', cleanup);
425
- if (keyboard) {
426
- keyboard.start((action) => {
427
- switch (action) {
428
- case 'o':
429
- case 'p':
430
- import('open').then(m => m.default(currentPreviewUrl));
431
- break;
432
- case 'a':
433
- logFilter = 'all';
434
- console.log(dim(' Showing all logs.'));
435
- updateStatus();
436
- break;
437
- case 'e':
438
- logFilter = 'events';
439
- console.log(dim(' Showing events only.'));
440
- updateStatus();
441
- break;
442
- case 'r':
443
- logFilter = 'runtime';
444
- console.log(dim(' Showing runtime logs only.'));
445
- updateStatus();
446
- break;
447
- case 'i':
448
- console.log(getInfoPanel({
449
- appName: config.appName,
450
- previewUrl: currentPreviewUrl,
451
- workspaceName: config.workspaceName,
452
- directory: cwd,
453
- }));
454
- break;
455
- case 'quit':
456
- cleanup();
457
- break;
458
- }
459
- });
460
- }
461
- // Start file watcher. In no-sync mode we never watch the working tree:
462
- // no fast-sync to the sandbox, no git auto-commit. The user drives git
463
- // manually and `git push` is what updates the preview. We print a loud
464
- // notice so it is unmistakable that edits are not flowing automatically.
465
- if (noSync) {
466
- if (useJson) {
467
- jsonLine({ event: 'startup', phase: 'no_sync_mode', timestamp: ts() });
468
- }
469
- else {
470
- console.log('');
471
- console.log(yellow(' Auto-sync is OFF (--no-sync).'));
472
- console.log(dim(' The preview reflects pushed commits. Run `git push` to update it;'));
473
- console.log(dim(' uncommitted or unpushed changes are not shown.'));
474
- console.log('');
475
- }
476
- }
477
- else {
478
- await watchAndAutoCommit(process.cwd(), client, config.appId, {
479
- onFastSync: useJson
480
- ? (count) => { jsonLine({ event: 'files_synced', count, target: 'preview', timestamp: ts() }); }
481
- : (count) => { syncedCount += count; updateStatus(); },
482
- onGitPush: useJson
483
- ? (count) => { jsonLine({ event: 'files_pushed', count, target: 'git', timestamp: ts() }); }
484
- : (count) => { syncedCount += count; updateStatus(); },
485
- });
486
- }
487
- // Start log tailing (unless --no-logs)
488
- if (options?.logs !== false) {
489
- logTailer = startLogTailer({
490
- appId: config.appId,
491
- client,
492
- projectDir: cwd,
493
- toTerminal: !options?.logsOnlyFile,
494
- toFile: true,
495
- getFilter: () => logFilter,
496
- useJson,
497
- });
498
- }
499
- // Keep alive
500
- await new Promise(() => { });
501
- }
502
- export const devCommand = new Command('dev')
503
- .description('Start local development with live sync, preview sandbox, and file watching')
504
- .option('--no-logs', 'Disable automatic log tailing')
505
- .option('--logs-only-file', 'Write logs to file only, not terminal')
506
- .option('--detach', 'Start the dev session in the background and exit. Logs go to .runwork/dev-{stdout,stderr}.log')
507
- .option('--restart', 'Stop any existing dev session first, then start a fresh one')
508
- .option('--no-sync', 'Hold the preview open without auto-syncing files or git; you push manually to update the preview')
509
- // Hidden internal flag passed by `runwork dev --detach` when it spawns
510
- // its detached child. Not for end users -- registered here only so
511
- // commander does not throw "unknown option" when the child receives it.
512
- .addOption(new Option(INTERNAL_DETACHED_CHILD_FLAG).hideHelp())
513
- .action(async (options, command) => {
514
- const globalJson = shouldOutputJson(command.optsWithGlobals().json);
515
- const noSync = options.sync === false;
516
- // The internal child marker is parsed manually because we want it
517
- // hidden from --help and from commander's option list. Its presence
518
- // means: "this process is the detached child of a `--detach` parent;
519
- // skip the parent-spawn branch and run the actual dev work."
520
- const isChild = isInternalDetachedChild(process.argv);
521
- if (options.detach && !isChild) {
522
- await runDevDetachParent({
523
- json: globalJson,
524
- restart: options.restart,
525
- });
526
- return;
527
- }
528
- await execDev({
529
- ...options,
530
- json: globalJson,
531
- mode: isChild ? 'detached' : 'foreground',
532
- restart: options.restart,
533
- noSync,
534
- });
535
- });
536
- /**
537
- * Parent half of `runwork dev --detach`. Reads the app config to know
538
- * which appId we're spawning a child for, dispatches to the detach
539
- * orchestrator in `src/dev/detach.ts`, and renders the outcome.
540
- *
541
- * The parent does not do any of the dev work itself -- it is a thin
542
- * observer that exists only long enough to confirm the child has a
543
- * preview URL. See `docs/plans/2026-05-06-runwork-dev-lifecycle-design.md`
544
- * for the full handshake contract.
545
- */
546
- async function runDevDetachParent(opts) {
547
- const config = readConfig();
548
- const cwd = process.cwd();
549
- const ts = () => new Date().toISOString();
550
- // --restart: stop the existing session before spawning a child.
551
- if (opts.restart) {
552
- const stopOutcome = await stopSession(cwd, config.appId);
553
- if (!opts.json && stopOutcome.result === 'stopped') {
554
- console.log(dim(`Stopped previous dev session (PID ${stopOutcome.pid}).`));
555
- }
556
- }
557
- // Idempotency: if a live session is already running on this machine,
558
- // don't spawn a second child -- just report it.
559
- const existing = getSessionState(cwd, config.appId);
560
- if (existing.state === 'alive') {
561
- const f = existing.file;
562
- if (opts.json) {
563
- jsonLine({
564
- event: 'already_running',
565
- previewUrl: f.previewUrl,
566
- pid: f.pid,
567
- sessionId: f.sessionId,
568
- mode: f.mode,
569
- timestamp: ts(),
570
- });
571
- }
572
- else {
573
- console.log(`Dev session already running (PID ${f.pid}, mode: ${f.mode}).`);
574
- console.log(` Preview: ${green(f.previewUrl || '(starting up)')}`);
575
- console.log(dim(` Follow logs: runwork dev attach`));
576
- }
577
- return;
578
- }
579
- if (existing.state === 'stale') {
580
- removeSessionFile(cwd);
581
- }
582
- if (opts.json) {
583
- jsonLine({ event: 'starting', mode: 'detached', timestamp: ts() });
584
- }
585
- else {
586
- console.log(dim('Starting dev session in background...'));
587
- }
588
- // `buildChildArgs` reproduces our invocation as args for the child
589
- // (which is spawned with the same `process.execPath`), with two
590
- // platform-aware tweaks: it skips Bun's auto-injected virtual-FS path
591
- // on Windows (which the child runtime re-injects on its own), and it
592
- // ensures exactly one copy of the internal-child marker.
593
- const childArgs = buildChildArgs(process.argv);
594
- const outcome = await runAsDetachedParent({
595
- appDir: cwd,
596
- expectedAppId: config.appId,
597
- childArgs,
598
- });
599
- switch (outcome.result) {
600
- case 'started': {
601
- const f = outcome.file;
602
- // Relay the child's startup sync outcome so a detached caller learns the
603
- // preview may be serving stale code (the child only logs it otherwise).
604
- const syncFailed = f.startupSync?.syncFailed ?? false;
605
- if (opts.json) {
606
- jsonLine({
607
- event: 'session_started',
608
- previewUrl: f.previewUrl,
609
- pid: f.pid,
610
- sessionId: f.sessionId,
611
- mode: f.mode,
612
- syncStatus: f.startupSync?.syncStatus,
613
- syncFailed,
614
- staleCode: f.startupSync?.staleCode ?? false,
615
- timestamp: ts(),
616
- guide: buildDevSessionGuide(),
617
- });
618
- }
619
- else {
620
- console.log('');
621
- console.log(`Dev session running in background (PID ${f.pid}).`);
622
- console.log(` Preview: ${green(f.previewUrl)}`);
623
- if (syncFailed) {
624
- console.log(yellow(' Warning: initial sync failed. The preview shows the last deployed code, NOT your latest changes.'));
625
- }
626
- console.log(dim(` Stop with: runwork dev stop`));
627
- console.log(dim(` Logs: tail -f .runwork/dev-stdout.log`));
628
- console.log('');
629
- }
630
- return;
631
- }
632
- case 'wrong-pid': {
633
- // Race: another process won the session-file write. Treat as
634
- // "already running" -- the user's intent is satisfied. Our spawned
635
- // child will detect the same file at its own startup probe and
636
- // exit cleanly without doing duplicate work.
637
- const f = outcome.file;
638
- if (opts.json) {
639
- jsonLine({
640
- event: 'already_running',
641
- previewUrl: f.previewUrl,
642
- pid: f.pid,
643
- sessionId: f.sessionId,
644
- mode: f.mode,
645
- timestamp: ts(),
646
- });
647
- }
648
- else {
649
- console.log(`Another dev session already running (PID ${f.pid}).`);
650
- console.log(` Preview: ${green(f.previewUrl)}`);
651
- }
652
- return;
653
- }
654
- case 'child-exited': {
655
- const message = `Detached dev session exited before becoming ready.`;
656
- if (opts.json) {
657
- jsonLine({
658
- event: 'error',
659
- phase: 'detach-child-exited',
660
- timestamp: ts(),
661
- error: {
662
- message,
663
- diagnosis: 'The detached child process exited (typically due to missing auth, missing git, network failure, or a sync conflict) before it could publish a preview URL.',
664
- suggestions: [
665
- 'Inspect the child stderr at .runwork/dev-stderr.log',
666
- 'Run `runwork doctor` to verify auth and connectivity',
667
- 'Try `runwork dev` (foreground) to see the failure inline',
668
- ],
669
- childLogTail: outcome.childLogTail,
670
- },
671
- });
672
- }
673
- else {
674
- console.error(yellow(message));
675
- console.error(dim(' Inspect .runwork/dev-stderr.log or run `runwork dev` to see what failed.'));
676
- if (outcome.childLogTail) {
677
- console.error(dim(' Tail of child stderr:'));
678
- for (const line of outcome.childLogTail.split('\n').slice(-10)) {
679
- if (line)
680
- console.error(dim(` ${line}`));
681
- }
682
- }
683
- }
684
- process.exit(1);
685
- return;
686
- }
687
- case 'timeout': {
688
- const message = `Detached dev session did not become ready within 90s.`;
689
- if (opts.json) {
690
- jsonLine({
691
- event: 'error',
692
- phase: 'detach',
693
- timestamp: ts(),
694
- error: {
695
- message,
696
- diagnosis: 'The detached child process was spawned but never wrote a session file with a preview URL. The sandbox boot may have failed.',
697
- suggestions: [
698
- 'Inspect the child stderr at .runwork/dev-stderr.log',
699
- 'Run `runwork doctor` to verify auth and connectivity',
700
- 'Try `runwork dev` (foreground) to see the failure inline',
701
- ],
702
- childLogTail: outcome.childLogTail,
703
- },
704
- });
705
- }
706
- else {
707
- console.error(yellow(message));
708
- console.error(dim(' Inspect .runwork/dev-stderr.log or run `runwork dev` to see what failed.'));
709
- if (outcome.childLogTail) {
710
- console.error(dim(' Tail of child stderr:'));
711
- for (const line of outcome.childLogTail.split('\n').slice(-10)) {
712
- if (line)
713
- console.error(dim(` ${line}`));
714
- }
715
- }
716
- }
717
- process.exit(1);
718
- return;
719
- }
720
- case 'spawn-failed': {
721
- const message = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
722
- if (opts.json) {
723
- jsonLine({
724
- event: 'error',
725
- phase: 'detach-spawn',
726
- timestamp: ts(),
727
- error: {
728
- message: `Failed to spawn detached child: ${message}`,
729
- diagnosis: 'The CLI could not fork itself into a background process.',
730
- suggestions: ['Run `runwork dev` (foreground) instead'],
731
- },
732
- });
733
- }
734
- else {
735
- console.error(yellow(`Failed to start detached dev session: ${message}`));
736
- console.error(dim(' Run `runwork dev` (foreground) instead.'));
737
- }
738
- process.exit(1);
739
- return;
740
- }
741
- }
742
- }
743
- /**
744
- * `runwork dev stop` -- subcommand of `dev`. Tears down a running
745
- * session by reading the local session file. Idempotent.
746
- */
747
- const devStopCommand = new Command('stop')
748
- .description('Stop the dev session running for this app')
749
- .action(async (_opts, command) => {
750
- const useJson = shouldOutputJson(command.optsWithGlobals().json);
751
- const config = readConfig();
752
- const cwd = process.cwd();
753
- const ts = () => new Date().toISOString();
754
- const outcome = await stopSession(cwd, config.appId);
755
- switch (outcome.result) {
756
- case 'no-session':
757
- if (useJson) {
758
- jsonLine({ event: 'session_stopped', result: 'no-session', timestamp: ts() });
759
- }
760
- else {
761
- console.log('No dev session running here.');
762
- }
763
- return;
764
- case 'stale-cleaned':
765
- if (useJson) {
766
- jsonLine({
767
- event: 'session_stopped',
768
- result: 'stale-cleaned',
769
- reason: outcome.reason,
770
- pid: outcome.pid,
771
- timestamp: ts(),
772
- });
773
- }
774
- else {
775
- console.log(`Cleaned up stale session file (reason: ${outcome.reason}).`);
776
- }
777
- return;
778
- case 'stopped':
779
- if (useJson) {
780
- jsonLine({
781
- event: 'session_stopped',
782
- result: 'stopped',
783
- pid: outcome.pid,
784
- gracefully: outcome.gracefully,
785
- timestamp: ts(),
786
- });
787
- }
788
- else {
789
- const how = outcome.gracefully ? 'gracefully' : 'forcefully';
790
- console.log(`Stopped dev session ${how} (PID ${outcome.pid}).`);
791
- }
792
- return;
793
- case 'kill-failed': {
794
- const msg = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
795
- if (useJson) {
796
- jsonLine({
797
- event: 'error',
798
- phase: 'dev-stop',
799
- timestamp: ts(),
800
- error: {
801
- message: `Failed to stop PID ${outcome.pid}: ${msg}`,
802
- diagnosis: 'The session file was removed but the process could not be killed by this CLI invocation.',
803
- suggestions: [
804
- `Manually kill the process: kill ${outcome.pid}`,
805
- 'Verify with: ps -p ' + outcome.pid,
806
- ],
807
- },
808
- });
809
- }
810
- else {
811
- console.error(yellow(`Could not kill PID ${outcome.pid}: ${msg}`));
812
- console.error(dim(' The session file has been removed; the process may need to be killed manually.'));
813
- }
814
- process.exit(1);
815
- return;
816
- }
817
- }
818
- });
819
- devCommand.addCommand(devStopCommand);
820
- /**
821
- * `runwork dev attach` -- read-only join on a running dev session.
822
- *
823
- * The keyboard glue here is intentionally a near-copy of the foreground
824
- * dev path's switch (we considered factoring it out and decided the
825
- * inverted cleanup semantics would force a config-heavy abstraction --
826
- * see the conversation in `docs/plans/2026-05-06-runwork-dev-lifecycle-design.md`).
827
- *
828
- * The hard rule: nothing this command does, EXCEPT the explicit `s`
829
- * keypress, ever stops the attached session. Ctrl+C and `q` exit the
830
- * attach UI but leave the dev session running.
831
- */
832
- const devAttachCommand = new Command('attach')
833
- .description('Attach to a running dev session for this app: tail logs, see the preview URL, control via keys')
834
- .option('--initial-lines <n>', 'How many trailing log lines to replay on connect (default 50)', (v) => parseInt(v, 10), 50)
835
- .action(async (options, command) => {
836
- const useJson = shouldOutputJson(command.optsWithGlobals().json);
837
- const config = readConfig();
838
- const cwd = process.cwd();
839
- const ts = () => new Date().toISOString();
840
- const target = resolveAttachTarget(cwd, config.appId);
841
- if (target.result === 'no-session') {
842
- if (useJson) {
843
- jsonLine({ event: 'attach_no_session', timestamp: ts() });
844
- }
845
- else {
846
- console.log('No dev session running here.');
847
- console.log(dim(' Start one with `runwork dev` or `runwork dev --detach`.'));
848
- }
849
- return;
850
- }
851
- if (target.result === 'stale-cleaned') {
852
- if (useJson) {
853
- jsonLine({ event: 'attach_stale_cleaned', reason: target.reason, timestamp: ts() });
854
- }
855
- else {
856
- console.log(`Found a stale session file (reason: ${target.reason}); cleaned up.`);
857
- console.log(dim(' Nothing to attach to. Start a new session with `runwork dev`.'));
858
- }
859
- return;
860
- }
861
- const file = target.file;
862
- if (useJson) {
863
- jsonLine({
864
- event: 'attach_started',
865
- pid: file.pid,
866
- sessionId: file.sessionId,
867
- previewUrl: file.previewUrl,
868
- mode: file.mode,
869
- startedAt: file.startedAt,
870
- timestamp: ts(),
871
- });
872
- }
873
- else {
874
- console.log('');
875
- console.log(`Attached to dev session ${dim(`(PID ${file.pid}, mode: ${file.mode}, ${formatStartedAgo(file.startedAt)})`)}`);
876
- console.log(` Preview: ${green(file.previewUrl || '(starting up)')}`);
877
- console.log(dim(` Logs: .runwork/dev-stdout.log, dev-stderr.log`));
878
- console.log('');
879
- console.log(dim(' o:open i:info s:stop q:detach (Ctrl+C also detaches; session keeps running)'));
880
- console.log('');
881
- }
882
- const colors = { dim, green, yellow, red, cyan };
883
- const paths = getAttachLogPaths(cwd);
884
- let stopping = false;
885
- // Status line: only in human + TTY mode. The same caveats apply as
886
- // foreground dev: createStatusLine returns a no-op when stdout
887
- // is not a TTY.
888
- const statusLine = useJson ? null : createStatusLine(process.stdout);
889
- let displayedUrl = file.previewUrl;
890
- function updateStatus() {
891
- if (!statusLine)
892
- return;
893
- const sep = '\x1b[90m│\x1b[39m';
894
- statusLine.update(`${bold(config.appName)} ${sep} ${green(displayedUrl || '(starting up)')} ${sep} ${cyan('attached')} ${sep} ${dim(`PID ${file.pid}`)} ${sep} \x1b[90mo\x1b[39m:open \x1b[90mi\x1b[39m:info \x1b[90ms\x1b[39m:stop \x1b[90mq\x1b[39m:detach`);
895
- }
896
- if (statusLine)
897
- updateStatus();
898
- const tail = startLogTail(paths.stdout, paths.stderr, {
899
- initialLines: options.initialLines ?? 50,
900
- onLine: (line, source) => {
901
- if (useJson) {
902
- // Pass-through: agents consuming attach in JSON mode see the
903
- // raw NDJSON log lines plus a wrapper noting the source.
904
- jsonLine({ event: 'log', source, line, timestamp: ts() });
905
- return;
906
- }
907
- const rendered = renderLogLine(line, source, colors);
908
- console.log(rendered.text);
909
- },
910
- onTruncated: () => {
911
- // The detached child reopens log files in 'w' mode at session
912
- // start, but a restart of the SAME PID shouldn't happen --
913
- // restart kills the old PID, spawns a new one. So a truncation
914
- // we observe is most likely "another process restarted the
915
- // session." We surface it and let the session-file watch decide
916
- // whether to fully exit (which it will when it sees the PID
917
- // change).
918
- if (!useJson)
919
- console.log(yellow(' (log file was truncated -- session may have restarted)'));
920
- },
921
- });
922
- const sessionWatch = startSessionFileWatch(cwd, file, {
923
- expectedAppId: config.appId,
924
- onUrlChanged: (next, prev) => {
925
- displayedUrl = next;
926
- if (useJson) {
927
- jsonLine({ event: 'preview_url_changed', previewUrl: next, previousUrl: prev, timestamp: ts() });
928
- }
929
- else {
930
- console.log(dim(` Preview URL changed: ${next}`));
931
- updateStatus();
932
- }
933
- },
934
- onSessionGone: () => {
935
- if (stopping)
936
- return;
937
- if (useJson) {
938
- jsonLine({ event: 'attach_session_ended', pid: file.pid, timestamp: ts() });
939
- }
940
- else {
941
- console.log('');
942
- console.log(yellow('Session ended. Detaching.'));
943
- }
944
- cleanupAttach('session-ended');
945
- },
946
- });
947
- const keyboard = useJson ? null : createKeyboardListener();
948
- function cleanupAttach(reason) {
949
- if (stopping)
950
- return;
951
- stopping = true;
952
- tail.stop();
953
- sessionWatch.stop();
954
- statusLine?.destroy();
955
- keyboard?.stop();
956
- // We never call removeSessionFile here -- attach does not own the
957
- // session. The only path that ends a session is `s` -> stopSession,
958
- // which removes the file itself.
959
- void reason;
960
- process.exit(0);
961
- }
962
- process.on('SIGINT', () => cleanupAttach('user'));
963
- process.on('SIGTERM', () => cleanupAttach('user'));
964
- if (keyboard) {
965
- keyboard.start(async (action) => {
966
- switch (action) {
967
- case 'o':
968
- case 'p': {
969
- const url = getCurrentPreviewUrl(cwd, displayedUrl);
970
- if (url)
971
- import('open').then((m) => m.default(url));
972
- break;
973
- }
974
- case 'i':
975
- console.log(getInfoPanel({
976
- appName: config.appName,
977
- previewUrl: getCurrentPreviewUrl(cwd, displayedUrl),
978
- workspaceName: config.workspaceName,
979
- directory: cwd,
980
- }));
981
- break;
982
- case 's': {
983
- console.log(dim('Stopping dev session...'));
984
- const outcome = await stopSession(cwd, config.appId);
985
- if (outcome.result === 'stopped') {
986
- console.log(`Stopped dev session (PID ${outcome.pid}).`);
987
- }
988
- else if (outcome.result === 'kill-failed') {
989
- console.error(yellow(`Could not kill PID ${outcome.pid}.`));
990
- }
991
- cleanupAttach('stop');
992
- break;
993
- }
994
- case 'quit':
995
- cleanupAttach('user');
996
- break;
997
- // 'a' / 'e' / 'r' are no-ops in attach -- log filtering is a
998
- // foreground-dev-only feature. The keys are still parsed
999
- // centrally so they don't surface as null actions.
1000
- case 'a':
1001
- case 'e':
1002
- case 'r':
1003
- case null:
1004
- break;
1005
- }
1006
- });
1007
- }
1008
- // Block until cleanup runs.
1009
- await new Promise(() => { });
1010
- });
1011
- devCommand.addCommand(devAttachCommand);