@miosa/cli 1.3.4 → 2.1.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 (489) hide show
  1. package/README.md +20 -835
  2. package/bin/miosa.js +75 -0
  3. package/lib/archive.js +88 -0
  4. package/lib/checksums.js +29 -0
  5. package/lib/platforms.js +31 -0
  6. package/lib/resolve.js +47 -0
  7. package/package.json +26 -41
  8. package/scripts/postinstall.js +68 -0
  9. package/CHANGELOG.md +0 -110
  10. package/dist/action-authority.d.ts +0 -106
  11. package/dist/action-authority.d.ts.map +0 -1
  12. package/dist/action-authority.js +0 -116
  13. package/dist/action-authority.js.map +0 -1
  14. package/dist/app-advisor.d.ts +0 -80
  15. package/dist/app-advisor.d.ts.map +0 -1
  16. package/dist/app-advisor.js +0 -486
  17. package/dist/app-advisor.js.map +0 -1
  18. package/dist/app-contract.d.ts +0 -92
  19. package/dist/app-contract.d.ts.map +0 -1
  20. package/dist/app-contract.js +0 -218
  21. package/dist/app-contract.js.map +0 -1
  22. package/dist/app-document.d.ts +0 -23
  23. package/dist/app-document.d.ts.map +0 -1
  24. package/dist/app-document.js +0 -180
  25. package/dist/app-document.js.map +0 -1
  26. package/dist/app-manifest.d.ts +0 -140
  27. package/dist/app-manifest.d.ts.map +0 -1
  28. package/dist/app-manifest.js +0 -377
  29. package/dist/app-manifest.js.map +0 -1
  30. package/dist/app-operation.d.ts +0 -23
  31. package/dist/app-operation.d.ts.map +0 -1
  32. package/dist/app-operation.js +0 -49
  33. package/dist/app-operation.js.map +0 -1
  34. package/dist/app-release.d.ts +0 -132
  35. package/dist/app-release.d.ts.map +0 -1
  36. package/dist/app-release.js +0 -294
  37. package/dist/app-release.js.map +0 -1
  38. package/dist/bin/miosa.d.ts +0 -3
  39. package/dist/bin/miosa.d.ts.map +0 -1
  40. package/dist/bin/miosa.js +0 -288
  41. package/dist/bin/miosa.js.map +0 -1
  42. package/dist/cli-env.d.ts +0 -12
  43. package/dist/cli-env.d.ts.map +0 -1
  44. package/dist/cli-env.js +0 -34
  45. package/dist/cli-env.js.map +0 -1
  46. package/dist/client.d.ts +0 -155
  47. package/dist/client.d.ts.map +0 -1
  48. package/dist/client.js +0 -865
  49. package/dist/client.js.map +0 -1
  50. package/dist/commands/actions.d.ts +0 -3
  51. package/dist/commands/actions.d.ts.map +0 -1
  52. package/dist/commands/actions.js +0 -222
  53. package/dist/commands/actions.js.map +0 -1
  54. package/dist/commands/agent-runtime-profiles.d.ts +0 -3
  55. package/dist/commands/agent-runtime-profiles.d.ts.map +0 -1
  56. package/dist/commands/agent-runtime-profiles.js +0 -198
  57. package/dist/commands/agent-runtime-profiles.js.map +0 -1
  58. package/dist/commands/agent.d.ts +0 -18
  59. package/dist/commands/agent.d.ts.map +0 -1
  60. package/dist/commands/agent.js +0 -737
  61. package/dist/commands/agent.js.map +0 -1
  62. package/dist/commands/alerts.d.ts +0 -3
  63. package/dist/commands/alerts.d.ts.map +0 -1
  64. package/dist/commands/alerts.js +0 -41
  65. package/dist/commands/alerts.js.map +0 -1
  66. package/dist/commands/api-keys.d.ts +0 -3
  67. package/dist/commands/api-keys.d.ts.map +0 -1
  68. package/dist/commands/api-keys.js +0 -160
  69. package/dist/commands/api-keys.js.map +0 -1
  70. package/dist/commands/api-resource.d.ts +0 -20
  71. package/dist/commands/api-resource.d.ts.map +0 -1
  72. package/dist/commands/api-resource.js +0 -120
  73. package/dist/commands/api-resource.js.map +0 -1
  74. package/dist/commands/app.d.ts +0 -38
  75. package/dist/commands/app.d.ts.map +0 -1
  76. package/dist/commands/app.js +0 -1182
  77. package/dist/commands/app.js.map +0 -1
  78. package/dist/commands/apps.d.ts +0 -3
  79. package/dist/commands/apps.d.ts.map +0 -1
  80. package/dist/commands/apps.js +0 -218
  81. package/dist/commands/apps.js.map +0 -1
  82. package/dist/commands/audit.d.ts +0 -3
  83. package/dist/commands/audit.d.ts.map +0 -1
  84. package/dist/commands/audit.js +0 -25
  85. package/dist/commands/audit.js.map +0 -1
  86. package/dist/commands/auth.d.ts +0 -3
  87. package/dist/commands/auth.d.ts.map +0 -1
  88. package/dist/commands/auth.js +0 -282
  89. package/dist/commands/auth.js.map +0 -1
  90. package/dist/commands/backups.d.ts +0 -3
  91. package/dist/commands/backups.d.ts.map +0 -1
  92. package/dist/commands/backups.js +0 -23
  93. package/dist/commands/backups.js.map +0 -1
  94. package/dist/commands/billing.d.ts +0 -3
  95. package/dist/commands/billing.d.ts.map +0 -1
  96. package/dist/commands/billing.js +0 -208
  97. package/dist/commands/billing.js.map +0 -1
  98. package/dist/commands/builds.d.ts +0 -3
  99. package/dist/commands/builds.d.ts.map +0 -1
  100. package/dist/commands/builds.js +0 -198
  101. package/dist/commands/builds.js.map +0 -1
  102. package/dist/commands/capabilities.d.ts +0 -3
  103. package/dist/commands/capabilities.d.ts.map +0 -1
  104. package/dist/commands/capabilities.js +0 -1002
  105. package/dist/commands/capabilities.js.map +0 -1
  106. package/dist/commands/checkpoints.d.ts +0 -3
  107. package/dist/commands/checkpoints.d.ts.map +0 -1
  108. package/dist/commands/checkpoints.js +0 -193
  109. package/dist/commands/checkpoints.js.map +0 -1
  110. package/dist/commands/cleanup.d.ts +0 -3
  111. package/dist/commands/cleanup.d.ts.map +0 -1
  112. package/dist/commands/cleanup.js +0 -65
  113. package/dist/commands/cleanup.js.map +0 -1
  114. package/dist/commands/cloud.d.ts +0 -3
  115. package/dist/commands/cloud.d.ts.map +0 -1
  116. package/dist/commands/cloud.js +0 -192
  117. package/dist/commands/cloud.js.map +0 -1
  118. package/dist/commands/command-overview.d.ts +0 -13
  119. package/dist/commands/command-overview.d.ts.map +0 -1
  120. package/dist/commands/command-overview.js +0 -50
  121. package/dist/commands/command-overview.js.map +0 -1
  122. package/dist/commands/completion.d.ts +0 -3
  123. package/dist/commands/completion.d.ts.map +0 -1
  124. package/dist/commands/completion.js +0 -183
  125. package/dist/commands/completion.js.map +0 -1
  126. package/dist/commands/computers.d.ts +0 -3
  127. package/dist/commands/computers.d.ts.map +0 -1
  128. package/dist/commands/computers.js +0 -706
  129. package/dist/commands/computers.js.map +0 -1
  130. package/dist/commands/config.d.ts +0 -3
  131. package/dist/commands/config.d.ts.map +0 -1
  132. package/dist/commands/config.js +0 -114
  133. package/dist/commands/config.js.map +0 -1
  134. package/dist/commands/connect.d.ts +0 -6
  135. package/dist/commands/connect.d.ts.map +0 -1
  136. package/dist/commands/connect.js +0 -127
  137. package/dist/commands/connect.js.map +0 -1
  138. package/dist/commands/connectors.d.ts +0 -3
  139. package/dist/commands/connectors.d.ts.map +0 -1
  140. package/dist/commands/connectors.js +0 -172
  141. package/dist/commands/connectors.js.map +0 -1
  142. package/dist/commands/containers.d.ts +0 -3
  143. package/dist/commands/containers.d.ts.map +0 -1
  144. package/dist/commands/containers.js +0 -20
  145. package/dist/commands/containers.js.map +0 -1
  146. package/dist/commands/context.d.ts +0 -3
  147. package/dist/commands/context.d.ts.map +0 -1
  148. package/dist/commands/context.js +0 -233
  149. package/dist/commands/context.js.map +0 -1
  150. package/dist/commands/cp.d.ts +0 -3
  151. package/dist/commands/cp.d.ts.map +0 -1
  152. package/dist/commands/cp.js +0 -319
  153. package/dist/commands/cp.js.map +0 -1
  154. package/dist/commands/cron.d.ts +0 -3
  155. package/dist/commands/cron.d.ts.map +0 -1
  156. package/dist/commands/cron.js +0 -65
  157. package/dist/commands/cron.js.map +0 -1
  158. package/dist/commands/dashboard.d.ts +0 -13
  159. package/dist/commands/dashboard.d.ts.map +0 -1
  160. package/dist/commands/dashboard.js +0 -54
  161. package/dist/commands/dashboard.js.map +0 -1
  162. package/dist/commands/databases.d.ts +0 -3
  163. package/dist/commands/databases.d.ts.map +0 -1
  164. package/dist/commands/databases.js +0 -714
  165. package/dist/commands/databases.js.map +0 -1
  166. package/dist/commands/db.d.ts +0 -3
  167. package/dist/commands/db.d.ts.map +0 -1
  168. package/dist/commands/db.js +0 -385
  169. package/dist/commands/db.js.map +0 -1
  170. package/dist/commands/deploy.d.ts +0 -3
  171. package/dist/commands/deploy.d.ts.map +0 -1
  172. package/dist/commands/deploy.js +0 -1234
  173. package/dist/commands/deploy.js.map +0 -1
  174. package/dist/commands/desktop.d.ts +0 -3
  175. package/dist/commands/desktop.d.ts.map +0 -1
  176. package/dist/commands/desktop.js +0 -277
  177. package/dist/commands/desktop.js.map +0 -1
  178. package/dist/commands/dev.d.ts +0 -3
  179. package/dist/commands/dev.d.ts.map +0 -1
  180. package/dist/commands/dev.js +0 -245
  181. package/dist/commands/dev.js.map +0 -1
  182. package/dist/commands/devices.d.ts +0 -3
  183. package/dist/commands/devices.d.ts.map +0 -1
  184. package/dist/commands/devices.js +0 -279
  185. package/dist/commands/devices.js.map +0 -1
  186. package/dist/commands/docker-deploy.d.ts +0 -3
  187. package/dist/commands/docker-deploy.d.ts.map +0 -1
  188. package/dist/commands/docker-deploy.js +0 -659
  189. package/dist/commands/docker-deploy.js.map +0 -1
  190. package/dist/commands/doctor.d.ts +0 -69
  191. package/dist/commands/doctor.d.ts.map +0 -1
  192. package/dist/commands/doctor.js +0 -596
  193. package/dist/commands/doctor.js.map +0 -1
  194. package/dist/commands/domains.d.ts +0 -3
  195. package/dist/commands/domains.d.ts.map +0 -1
  196. package/dist/commands/domains.js +0 -307
  197. package/dist/commands/domains.js.map +0 -1
  198. package/dist/commands/enterprise-util.d.ts +0 -39
  199. package/dist/commands/enterprise-util.d.ts.map +0 -1
  200. package/dist/commands/enterprise-util.js +0 -217
  201. package/dist/commands/enterprise-util.js.map +0 -1
  202. package/dist/commands/env-input.d.ts +0 -26
  203. package/dist/commands/env-input.d.ts.map +0 -1
  204. package/dist/commands/env-input.js +0 -68
  205. package/dist/commands/env-input.js.map +0 -1
  206. package/dist/commands/env.d.ts +0 -3
  207. package/dist/commands/env.d.ts.map +0 -1
  208. package/dist/commands/env.js +0 -189
  209. package/dist/commands/env.js.map +0 -1
  210. package/dist/commands/exec.d.ts +0 -3
  211. package/dist/commands/exec.d.ts.map +0 -1
  212. package/dist/commands/exec.js +0 -110
  213. package/dist/commands/exec.js.map +0 -1
  214. package/dist/commands/forge.d.ts +0 -3
  215. package/dist/commands/forge.d.ts.map +0 -1
  216. package/dist/commands/forge.js +0 -254
  217. package/dist/commands/forge.js.map +0 -1
  218. package/dist/commands/functions.d.ts +0 -3
  219. package/dist/commands/functions.d.ts.map +0 -1
  220. package/dist/commands/functions.js +0 -47
  221. package/dist/commands/functions.js.map +0 -1
  222. package/dist/commands/gha-runners.d.ts +0 -3
  223. package/dist/commands/gha-runners.d.ts.map +0 -1
  224. package/dist/commands/gha-runners.js +0 -33
  225. package/dist/commands/gha-runners.js.map +0 -1
  226. package/dist/commands/groups.d.ts +0 -3
  227. package/dist/commands/groups.d.ts.map +0 -1
  228. package/dist/commands/groups.js +0 -38
  229. package/dist/commands/groups.js.map +0 -1
  230. package/dist/commands/host.d.ts +0 -3
  231. package/dist/commands/host.d.ts.map +0 -1
  232. package/dist/commands/host.js +0 -74
  233. package/dist/commands/host.js.map +0 -1
  234. package/dist/commands/hosts.d.ts +0 -7
  235. package/dist/commands/hosts.d.ts.map +0 -1
  236. package/dist/commands/hosts.js +0 -91
  237. package/dist/commands/hosts.js.map +0 -1
  238. package/dist/commands/link.d.ts +0 -8
  239. package/dist/commands/link.d.ts.map +0 -1
  240. package/dist/commands/link.js +0 -124
  241. package/dist/commands/link.js.map +0 -1
  242. package/dist/commands/login.d.ts +0 -10
  243. package/dist/commands/login.d.ts.map +0 -1
  244. package/dist/commands/login.js +0 -263
  245. package/dist/commands/login.js.map +0 -1
  246. package/dist/commands/logout.d.ts +0 -3
  247. package/dist/commands/logout.d.ts.map +0 -1
  248. package/dist/commands/logout.js +0 -43
  249. package/dist/commands/logout.js.map +0 -1
  250. package/dist/commands/logs.d.ts +0 -3
  251. package/dist/commands/logs.d.ts.map +0 -1
  252. package/dist/commands/logs.js +0 -401
  253. package/dist/commands/logs.js.map +0 -1
  254. package/dist/commands/ls.d.ts +0 -3
  255. package/dist/commands/ls.d.ts.map +0 -1
  256. package/dist/commands/ls.js +0 -121
  257. package/dist/commands/ls.js.map +0 -1
  258. package/dist/commands/mcp.d.ts +0 -46
  259. package/dist/commands/mcp.d.ts.map +0 -1
  260. package/dist/commands/mcp.js +0 -3413
  261. package/dist/commands/mcp.js.map +0 -1
  262. package/dist/commands/menu.d.ts +0 -29
  263. package/dist/commands/menu.d.ts.map +0 -1
  264. package/dist/commands/menu.js +0 -147
  265. package/dist/commands/menu.js.map +0 -1
  266. package/dist/commands/meshes.d.ts +0 -3
  267. package/dist/commands/meshes.d.ts.map +0 -1
  268. package/dist/commands/meshes.js +0 -27
  269. package/dist/commands/meshes.js.map +0 -1
  270. package/dist/commands/network-policy.d.ts +0 -3
  271. package/dist/commands/network-policy.d.ts.map +0 -1
  272. package/dist/commands/network-policy.js +0 -155
  273. package/dist/commands/network-policy.js.map +0 -1
  274. package/dist/commands/new.d.ts +0 -3
  275. package/dist/commands/new.d.ts.map +0 -1
  276. package/dist/commands/new.js +0 -296
  277. package/dist/commands/new.js.map +0 -1
  278. package/dist/commands/opencomputers.d.ts +0 -10
  279. package/dist/commands/opencomputers.d.ts.map +0 -1
  280. package/dist/commands/opencomputers.js +0 -25
  281. package/dist/commands/opencomputers.js.map +0 -1
  282. package/dist/commands/operating-contract.d.ts +0 -3
  283. package/dist/commands/operating-contract.d.ts.map +0 -1
  284. package/dist/commands/operating-contract.js +0 -651
  285. package/dist/commands/operating-contract.js.map +0 -1
  286. package/dist/commands/project.d.ts +0 -4
  287. package/dist/commands/project.d.ts.map +0 -1
  288. package/dist/commands/project.js +0 -25
  289. package/dist/commands/project.js.map +0 -1
  290. package/dist/commands/pull.d.ts +0 -14
  291. package/dist/commands/pull.d.ts.map +0 -1
  292. package/dist/commands/pull.js +0 -156
  293. package/dist/commands/pull.js.map +0 -1
  294. package/dist/commands/regions.d.ts +0 -3
  295. package/dist/commands/regions.d.ts.map +0 -1
  296. package/dist/commands/regions.js +0 -103
  297. package/dist/commands/regions.js.map +0 -1
  298. package/dist/commands/releases.d.ts +0 -3
  299. package/dist/commands/releases.d.ts.map +0 -1
  300. package/dist/commands/releases.js +0 -365
  301. package/dist/commands/releases.js.map +0 -1
  302. package/dist/commands/rm.d.ts +0 -3
  303. package/dist/commands/rm.d.ts.map +0 -1
  304. package/dist/commands/rm.js +0 -42
  305. package/dist/commands/rm.js.map +0 -1
  306. package/dist/commands/rollback.d.ts +0 -3
  307. package/dist/commands/rollback.d.ts.map +0 -1
  308. package/dist/commands/rollback.js +0 -62
  309. package/dist/commands/rollback.js.map +0 -1
  310. package/dist/commands/run-groups.d.ts +0 -3
  311. package/dist/commands/run-groups.d.ts.map +0 -1
  312. package/dist/commands/run-groups.js +0 -364
  313. package/dist/commands/run-groups.js.map +0 -1
  314. package/dist/commands/run.d.ts +0 -3
  315. package/dist/commands/run.d.ts.map +0 -1
  316. package/dist/commands/run.js +0 -130
  317. package/dist/commands/run.js.map +0 -1
  318. package/dist/commands/runs.d.ts +0 -3
  319. package/dist/commands/runs.d.ts.map +0 -1
  320. package/dist/commands/runs.js +0 -365
  321. package/dist/commands/runs.js.map +0 -1
  322. package/dist/commands/runtime-env.d.ts +0 -3
  323. package/dist/commands/runtime-env.d.ts.map +0 -1
  324. package/dist/commands/runtime-env.js +0 -114
  325. package/dist/commands/runtime-env.js.map +0 -1
  326. package/dist/commands/sandbox-delete-guard.d.ts +0 -4
  327. package/dist/commands/sandbox-delete-guard.d.ts.map +0 -1
  328. package/dist/commands/sandbox-delete-guard.js +0 -32
  329. package/dist/commands/sandbox-delete-guard.js.map +0 -1
  330. package/dist/commands/sandbox-dev.d.ts +0 -20
  331. package/dist/commands/sandbox-dev.d.ts.map +0 -1
  332. package/dist/commands/sandbox-dev.js +0 -583
  333. package/dist/commands/sandbox-dev.js.map +0 -1
  334. package/dist/commands/sandbox.d.ts +0 -36
  335. package/dist/commands/sandbox.d.ts.map +0 -1
  336. package/dist/commands/sandbox.js +0 -3796
  337. package/dist/commands/sandbox.js.map +0 -1
  338. package/dist/commands/scale.d.ts +0 -3
  339. package/dist/commands/scale.d.ts.map +0 -1
  340. package/dist/commands/scale.js +0 -61
  341. package/dist/commands/scale.js.map +0 -1
  342. package/dist/commands/schedules.d.ts +0 -3
  343. package/dist/commands/schedules.d.ts.map +0 -1
  344. package/dist/commands/schedules.js +0 -37
  345. package/dist/commands/schedules.js.map +0 -1
  346. package/dist/commands/secrets.d.ts +0 -3
  347. package/dist/commands/secrets.d.ts.map +0 -1
  348. package/dist/commands/secrets.js +0 -200
  349. package/dist/commands/secrets.js.map +0 -1
  350. package/dist/commands/services.d.ts +0 -3
  351. package/dist/commands/services.d.ts.map +0 -1
  352. package/dist/commands/services.js +0 -70
  353. package/dist/commands/services.js.map +0 -1
  354. package/dist/commands/shell.d.ts +0 -16
  355. package/dist/commands/shell.d.ts.map +0 -1
  356. package/dist/commands/shell.js +0 -555
  357. package/dist/commands/shell.js.map +0 -1
  358. package/dist/commands/snapshot.d.ts +0 -10
  359. package/dist/commands/snapshot.d.ts.map +0 -1
  360. package/dist/commands/snapshot.js +0 -181
  361. package/dist/commands/snapshot.js.map +0 -1
  362. package/dist/commands/ssh.d.ts +0 -3
  363. package/dist/commands/ssh.d.ts.map +0 -1
  364. package/dist/commands/ssh.js +0 -93
  365. package/dist/commands/ssh.js.map +0 -1
  366. package/dist/commands/status.d.ts +0 -3
  367. package/dist/commands/status.d.ts.map +0 -1
  368. package/dist/commands/status.js +0 -302
  369. package/dist/commands/status.js.map +0 -1
  370. package/dist/commands/storage.d.ts +0 -3
  371. package/dist/commands/storage.d.ts.map +0 -1
  372. package/dist/commands/storage.js +0 -456
  373. package/dist/commands/storage.js.map +0 -1
  374. package/dist/commands/teams.d.ts +0 -3
  375. package/dist/commands/teams.d.ts.map +0 -1
  376. package/dist/commands/teams.js +0 -169
  377. package/dist/commands/teams.js.map +0 -1
  378. package/dist/commands/templates.d.ts +0 -3
  379. package/dist/commands/templates.d.ts.map +0 -1
  380. package/dist/commands/templates.js +0 -690
  381. package/dist/commands/templates.js.map +0 -1
  382. package/dist/commands/tenant.d.ts +0 -3
  383. package/dist/commands/tenant.d.ts.map +0 -1
  384. package/dist/commands/tenant.js +0 -95
  385. package/dist/commands/tenant.js.map +0 -1
  386. package/dist/commands/tunnel.d.ts +0 -3
  387. package/dist/commands/tunnel.d.ts.map +0 -1
  388. package/dist/commands/tunnel.js +0 -418
  389. package/dist/commands/tunnel.js.map +0 -1
  390. package/dist/commands/up.d.ts +0 -14
  391. package/dist/commands/up.d.ts.map +0 -1
  392. package/dist/commands/up.js +0 -720
  393. package/dist/commands/up.js.map +0 -1
  394. package/dist/commands/update.d.ts +0 -3
  395. package/dist/commands/update.d.ts.map +0 -1
  396. package/dist/commands/update.js +0 -166
  397. package/dist/commands/update.js.map +0 -1
  398. package/dist/commands/util.d.ts +0 -25
  399. package/dist/commands/util.d.ts.map +0 -1
  400. package/dist/commands/util.js +0 -199
  401. package/dist/commands/util.js.map +0 -1
  402. package/dist/commands/volumes.d.ts +0 -3
  403. package/dist/commands/volumes.d.ts.map +0 -1
  404. package/dist/commands/volumes.js +0 -196
  405. package/dist/commands/volumes.js.map +0 -1
  406. package/dist/commands/watch.d.ts +0 -6
  407. package/dist/commands/watch.d.ts.map +0 -1
  408. package/dist/commands/watch.js +0 -414
  409. package/dist/commands/watch.js.map +0 -1
  410. package/dist/commands/webhooks.d.ts +0 -3
  411. package/dist/commands/webhooks.d.ts.map +0 -1
  412. package/dist/commands/webhooks.js +0 -23
  413. package/dist/commands/webhooks.js.map +0 -1
  414. package/dist/commands/whoami.d.ts +0 -3
  415. package/dist/commands/whoami.d.ts.map +0 -1
  416. package/dist/commands/whoami.js +0 -156
  417. package/dist/commands/whoami.js.map +0 -1
  418. package/dist/commands/workspaces.d.ts +0 -3
  419. package/dist/commands/workspaces.d.ts.map +0 -1
  420. package/dist/commands/workspaces.js +0 -147
  421. package/dist/commands/workspaces.js.map +0 -1
  422. package/dist/config.d.ts +0 -54
  423. package/dist/config.d.ts.map +0 -1
  424. package/dist/config.js +0 -275
  425. package/dist/config.js.map +0 -1
  426. package/dist/errors.d.ts +0 -30
  427. package/dist/errors.d.ts.map +0 -1
  428. package/dist/errors.js +0 -92
  429. package/dist/errors.js.map +0 -1
  430. package/dist/framework-detector.d.ts +0 -22
  431. package/dist/framework-detector.d.ts.map +0 -1
  432. package/dist/framework-detector.js +0 -373
  433. package/dist/framework-detector.js.map +0 -1
  434. package/dist/generated/action-capabilities.d.ts +0 -826
  435. package/dist/generated/action-capabilities.d.ts.map +0 -1
  436. package/dist/generated/action-capabilities.js +0 -1035
  437. package/dist/generated/action-capabilities.js.map +0 -1
  438. package/dist/net-diagnosis.d.ts +0 -87
  439. package/dist/net-diagnosis.d.ts.map +0 -1
  440. package/dist/net-diagnosis.js +0 -408
  441. package/dist/net-diagnosis.js.map +0 -1
  442. package/dist/pty/raw-mode.d.ts +0 -7
  443. package/dist/pty/raw-mode.d.ts.map +0 -1
  444. package/dist/pty/raw-mode.js +0 -22
  445. package/dist/pty/raw-mode.js.map +0 -1
  446. package/dist/pty/ws-pty-client.d.ts +0 -13
  447. package/dist/pty/ws-pty-client.d.ts.map +0 -1
  448. package/dist/pty/ws-pty-client.js +0 -73
  449. package/dist/pty/ws-pty-client.js.map +0 -1
  450. package/dist/tui/dashboard.d.ts +0 -35
  451. package/dist/tui/dashboard.d.ts.map +0 -1
  452. package/dist/tui/dashboard.js +0 -191
  453. package/dist/tui/dashboard.js.map +0 -1
  454. package/dist/tui/logs.d.ts +0 -26
  455. package/dist/tui/logs.d.ts.map +0 -1
  456. package/dist/tui/logs.js +0 -276
  457. package/dist/tui/logs.js.map +0 -1
  458. package/dist/types.d.ts +0 -362
  459. package/dist/types.d.ts.map +0 -1
  460. package/dist/types.js +0 -16
  461. package/dist/types.js.map +0 -1
  462. package/dist/ui/picker.d.ts +0 -41
  463. package/dist/ui/picker.d.ts.map +0 -1
  464. package/dist/ui/picker.js +0 -77
  465. package/dist/ui/picker.js.map +0 -1
  466. package/dist/ui/progress.d.ts +0 -10
  467. package/dist/ui/progress.d.ts.map +0 -1
  468. package/dist/ui/progress.js +0 -36
  469. package/dist/ui/progress.js.map +0 -1
  470. package/dist/ui/render.d.ts +0 -91
  471. package/dist/ui/render.d.ts.map +0 -1
  472. package/dist/ui/render.js +0 -138
  473. package/dist/ui/render.js.map +0 -1
  474. package/dist/ui/spinner.d.ts +0 -4
  475. package/dist/ui/spinner.d.ts.map +0 -1
  476. package/dist/ui/spinner.js +0 -7
  477. package/dist/ui/spinner.js.map +0 -1
  478. package/dist/ui/table.d.ts +0 -8
  479. package/dist/ui/table.d.ts.map +0 -1
  480. package/dist/ui/table.js +0 -46
  481. package/dist/ui/table.js.map +0 -1
  482. package/dist/update-check.d.ts +0 -13
  483. package/dist/update-check.d.ts.map +0 -1
  484. package/dist/update-check.js +0 -94
  485. package/dist/update-check.js.map +0 -1
  486. package/dist/version.d.ts +0 -3
  487. package/dist/version.d.ts.map +0 -1
  488. package/dist/version.js +0 -5
  489. package/dist/version.js.map +0 -1
@@ -1,3796 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import os from "node:os";
4
- import { spawn, spawnSync } from "node:child_process";
5
- import { createServer } from "node:net";
6
- import { randomUUID } from "node:crypto";
7
- import * as http from "node:http";
8
- import * as https from "node:https";
9
- import chalk from "chalk";
10
- import WebSocket from "ws";
11
- import { loadAppManifest, manifestPort, manifestProbePath, manifestStartCommand, parseAppManifest, } from "../app-manifest.js";
12
- import { detectFramework } from "../framework-detector.js";
13
- import { addDataOption, client, apiPath, deleteAndPrint, enc, getAndPrint, parseData, postAndPrint, printValue, runAction, unwrap, } from "./enterprise-util.js";
14
- import { loadConfig } from "../config.js";
15
- import { assertDeletableRemoteDir } from "./sandbox-delete-guard.js";
16
- import { ENV_FILE_OPTION_HELP, ENV_INLINE_SHELL_WARNING, ENV_STDIN_OPTION_HELP, resolveEnvInputs, } from "./env-input.js";
17
- import { handleError, isJsonMode, printJson } from "./util.js";
18
- import { renderTable } from "../ui/table.js";
19
- import { formatDuration, hintBlock, icon, kvPanel, printBanner, printElapsed, } from "../ui/render.js";
20
- import { formatBytes } from "../ui/progress.js";
21
- import { ApiResponseError, MiosaError, NetworkError, ServerError, UserError, mapHttpError, } from "../errors.js";
22
- import { EXIT_USER_ERROR } from "../types.js";
23
- import { registerSandboxDevCommands, runFullSandboxDoctor, } from "./sandbox-dev.js";
24
- const DEFAULT_INTERACTIVE_SANDBOX_TIMEOUT_SEC = 3_600;
25
- const DEFAULT_CREATE_WAIT_TIMEOUT_SEC = 120;
26
- const EXPIRING_SANDBOX_THRESHOLD_SEC = 5 * 60;
27
- // How long, after a sandbox reaches `running`, to keep polling for
28
- // command-readiness before `--wait` gives up.
29
- const COMMAND_READY_WAIT_TIMEOUT_SEC = 60;
30
- const COMMAND_READY_POLL_INTERVAL_MS = 500;
31
- // Platform readiness components that gate command execution. Mirrors the
32
- // server's `Engine.Sandbox.Readiness` platform set: `exec` returns
33
- // `409 SANDBOX_NOT_COMMAND_READY` until all of these report `ready`. Excludes
34
- // the template `application_probe` (the workload), which is started BY exec.
35
- const PLATFORM_READINESS_COMPONENTS = [
36
- "command_agent",
37
- "process_control",
38
- "resource_contract",
39
- "clock",
40
- "identity",
41
- "durability",
42
- "filesystem",
43
- "internal_probe",
44
- ];
45
- /**
46
- * True when a sandbox is command-ready — `exec` will succeed rather than return
47
- * `409 SANDBOX_NOT_COMMAND_READY`. Accepts a readiness payload
48
- * (`GET /sandboxes/:id/readiness`) or a sandbox record. Mirrors the server's
49
- * `command_path_ready?/1`: the aggregate `ready` flag short-circuits, otherwise
50
- * every platform component must report `ready`.
51
- */
52
- function isCommandReady(payload) {
53
- if (!payload || typeof payload !== "object")
54
- return false;
55
- const p = payload;
56
- if (p["ready"] === true)
57
- return true;
58
- const readiness = p["readiness"];
59
- const components = (readiness?.["components"] ??
60
- p["components"] ??
61
- p["readiness_components"]);
62
- if (!components || typeof components !== "object")
63
- return false;
64
- return PLATFORM_READINESS_COMPONENTS.every((name) => {
65
- const comp = components[name];
66
- const status = typeof comp === "string" ? comp : comp?.status;
67
- return status === "ready";
68
- });
69
- }
70
- /**
71
- * Generate a create Idempotency-Key. Generated once per logical `create` and
72
- * re-sent unchanged on every retry (see `MiosaClient.apiPostWithRetry`), so a
73
- * retried create dedups to the same sandbox instead of billing a duplicate VM.
74
- */
75
- function generateIdempotencyKey() {
76
- return randomUUID();
77
- }
78
- const SANDBOX_SIZE_CONTRACTS = {
79
- xs: { cpu: 1, memory: 2_048, disk: 10_240 },
80
- small: { cpu: 2, memory: 4_096, disk: 10_240 },
81
- medium: { cpu: 4, memory: 8_192, disk: 20_480 },
82
- large: { cpu: 8, memory: 16_384, disk: 40_960 },
83
- xl: { cpu: 16, memory: 32_768, disk: 81_920 },
84
- };
85
- export function register(program) {
86
- // -------------------------------------------------------------------------
87
- // sandbox / sandboxes command group — built manually to avoid subcommand
88
- // conflicts that arise when using the resourceCommands helper alongside
89
- // custom create/exec/file subcommands.
90
- // -------------------------------------------------------------------------
91
- const sandbox = program
92
- .command("sandbox")
93
- .alias("sandboxes")
94
- .description("Manage Sandboxes — lightweight code-only Computers (Firecracker microVMs without a desktop)");
95
- registerSandboxDevCommands(sandbox);
96
- // migrate — point a sandbox at a different template version.
97
- //
98
- // PREVIEW BY DEFAULT. Migrating replaces the sandbox's root filesystem, so
99
- // the destructive path is behind an explicit --confirm rather than being what
100
- // happens when you forget a flag. Ross/HackerAI asked for exactly this shape:
101
- // a preview of what is preserved, explicit confirmation, the original kept
102
- // until the replacement is ready, and rollback.
103
- sandbox
104
- .command("migrate <id>")
105
- .description("Preview (default) or perform moving a Sandbox to another template version")
106
- .requiredOption("--template <ref>", "Target version: v3, <template>@v3, a build id, or 'latest' (latest USABLE version)")
107
- .option("--confirm", "Actually perform the migration. Without this, nothing is changed.")
108
- .option("--json", "Output raw JSON")
109
- .action(async (id, opts) => {
110
- try {
111
- const json = isJsonMode(opts);
112
- const body = {
113
- template_version: opts.template,
114
- confirm: Boolean(opts.confirm),
115
- };
116
- const res = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/migrate`), body));
117
- if (json) {
118
- printJson(res);
119
- return;
120
- }
121
- const plan = (res["plan"] ?? res);
122
- const from = (plan["from"] ?? {});
123
- const to = (plan["to"] ?? {});
124
- console.log();
125
- console.log(` ${chalk.bold("Migration")} ${chalk.dim(str(from["image_id"]))} ${chalk.dim("->")} ${chalk.bold(`v${str(to["version"])}`)} ${chalk.dim(str(to["image_id"]))}`);
126
- const preserved = (plan["preserved"] ?? []);
127
- const lost = (plan["lost"] ?? []);
128
- if (preserved.length > 0) {
129
- console.log();
130
- console.log(` ${chalk.green("Preserved")}`);
131
- for (const p of preserved) {
132
- console.log(` ${icon.ok} ${str(p["item"])} ${chalk.dim(str(p["detail"]))}`);
133
- }
134
- }
135
- // Printed with the same weight as the preserved list, deliberately.
136
- // A preview that only shows survivors invites you to assume the rest
137
- // survives too.
138
- if (lost.length > 0) {
139
- console.log();
140
- console.log(` ${chalk.red("NOT preserved")}`);
141
- for (const l of lost) {
142
- console.log(` ${icon.fail} ${str(l["item"])} ${chalk.dim(str(l["detail"]))}`);
143
- }
144
- }
145
- const warnings = (plan["warnings"] ?? []);
146
- for (const w of warnings) {
147
- console.log();
148
- console.log(` ${chalk.yellow("Warning:")} ${w}`);
149
- }
150
- console.log();
151
- if (!opts.confirm) {
152
- console.log(` ${chalk.dim("Nothing has changed. Re-run with --confirm to perform this migration.")}`);
153
- }
154
- else {
155
- console.log(` ${chalk.dim(str(res["note"]) || "the original Sandbox is preserved until the replacement is ready")}`);
156
- }
157
- }
158
- catch (err) {
159
- handleError(err);
160
- }
161
- });
162
- // list
163
- sandbox
164
- .command("list")
165
- .description("List all Sandboxes")
166
- .option("--state <state>", "Filter by state (running, paused, …)")
167
- .option("--json", "Output as JSON")
168
- .action((opts) => runAction(async () => {
169
- const qs = opts.state ? `?state=${enc(opts.state)}` : "";
170
- if (isJsonMode(opts)) {
171
- await getAndPrint(`/sandboxes${qs}`, opts);
172
- return;
173
- }
174
- const raw = unwrap(await client().apiGet(apiPath(`/sandboxes${qs}`)));
175
- const items = Array.isArray(raw)
176
- ? raw
177
- : [];
178
- console.log();
179
- console.log(` ${icon.info} ${chalk.bold(String(items.length))} ${chalk.dim("sandbox(es)")}`);
180
- console.log();
181
- if (items.length === 0) {
182
- console.log(kvPanel([
183
- { label: "Sandboxes", value: chalk.dim("0 — none created yet") },
184
- ]));
185
- console.log();
186
- console.log(hintBlock("Try", ["miosa sandbox create"]));
187
- console.log();
188
- return;
189
- }
190
- renderTable(items, [
191
- { header: "ID", key: "id" },
192
- { header: "NAME", key: "name" },
193
- {
194
- header: "STATUS",
195
- key: "state",
196
- color: (val) => statusColor(val.trim()),
197
- },
198
- {
199
- header: "TEMPLATE",
200
- key: "template_id",
201
- },
202
- {
203
- header: "CREATED",
204
- key: "created_at",
205
- },
206
- ]);
207
- console.log();
208
- console.log(hintBlock("Try", [
209
- "miosa sandbox show <id>",
210
- "miosa sandbox exec <id> --command ...",
211
- "miosa sandbox create",
212
- ]));
213
- console.log();
214
- }));
215
- // ls — muscle-memory alias for list
216
- sandbox
217
- .command("ls")
218
- .description("Alias for list")
219
- .option("--json", "Output as JSON")
220
- .action((opts) => runAction(() => getAndPrint("/sandboxes", opts)));
221
- // show — canonical resourceCommands-style name
222
- sandbox
223
- .command("show <sandbox-id>")
224
- .description("Show a Sandbox by ID")
225
- .option("--port <port>", "Include live preview readiness for this port", parseIntegerOption)
226
- .option("--probe-path <path>", "HTTP path to probe when --port is set", "/")
227
- .option("--json", "Output as JSON")
228
- .action((id, opts) => runAction(async () => {
229
- if (isJsonMode(opts)) {
230
- const data = opts.port != null
231
- ? await showSandboxWithPreview(id, opts.port, opts.probePath)
232
- : unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}`)));
233
- console.log(JSON.stringify(data, null, 2));
234
- return;
235
- }
236
- const raw = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}`)));
237
- const sb = (raw ?? {});
238
- printBanner({ subtitle: "Sandbox" });
239
- const rows = [
240
- { label: "ID", value: chalk.bold(str(sb["id"])), icon: icon.info },
241
- { label: "Name", value: str(sb["name"]) },
242
- {
243
- label: "Status",
244
- value: statusColor(str(sb["state"])),
245
- },
246
- ];
247
- if (sb["template_id"]) {
248
- rows.push({ label: "Template", value: str(sb["template_id"]) });
249
- }
250
- if (sb["cpu_count"] != null) {
251
- rows.push({ label: "CPU", value: str(sb["cpu_count"]) });
252
- }
253
- if (sb["memory_mb"] != null) {
254
- rows.push({
255
- label: "Memory",
256
- value: `${str(sb["memory_mb"])} MB`,
257
- });
258
- }
259
- if (sb["timeout_sec"] != null) {
260
- rows.push({
261
- label: "Timeout",
262
- value: `${str(sb["timeout_sec"])} sec`,
263
- });
264
- }
265
- if (sb["ip_address"]) {
266
- rows.push({ label: "IP", value: str(sb["ip_address"]) });
267
- }
268
- if (sb["public_url"]) {
269
- rows.push({
270
- label: "URL",
271
- value: chalk.cyan(str(sb["public_url"])),
272
- });
273
- }
274
- if (sb["created_at"]) {
275
- rows.push({
276
- label: "Created",
277
- value: chalk.dim(str(sb["created_at"])),
278
- });
279
- }
280
- console.log(kvPanel(rows));
281
- console.log();
282
- warnIfExpiringSoon(sb, str(sb["id"]));
283
- console.log(hintBlock("Try", [
284
- `miosa sandbox exec ${str(sb["id"])} --command ...`,
285
- `miosa sandbox preview ${str(sb["id"])} --port 5173 --wait`,
286
- `miosa sandbox destroy ${str(sb["id"])}`,
287
- ]));
288
- console.log();
289
- }));
290
- // get — matches documented form: miosa sandboxes get <id>
291
- sandbox
292
- .command("get <sandbox-id>")
293
- .description("Show a Sandbox by ID (alias for show)")
294
- .option("--json", "Output as JSON")
295
- .action((id, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}`, opts)));
296
- // create — typed flags for common options; --data JSON overrides all flags
297
- addDataOption(sandbox
298
- .command("create")
299
- .description("Create a new Sandbox")
300
- .option("--template <template>", "Template / image ID (default: miosa-sandbox)")
301
- .option("--name <name>", "Human-readable name for the Sandbox")
302
- .option("--size <size>", "Named size: xs, small, medium, large, or xl (default: small)", parseSandboxSize)
303
- .option("--cpu <n>", "vCPU count", parseIntegerOption)
304
- .option("--memory <size>", "Memory size, e.g. 4096mb or 4gb", parseSizeMb)
305
- .option("--disk <size>", "Disk size, e.g. 10240mb or 10gb", parseSizeMb)
306
- .option("--timeout <duration>", "Wall-clock timeout, e.g. 300s, 1h", parseDurationSec)
307
- .option("--idle-timeout <duration>", "Idle timeout before pause; 0 disables it (default: 0)", parseDurationSec)
308
- .option("--idempotency-key <key>", "Retry-safe create key retained by the service for 24 hours")
309
- .option("--publish-port <port>", "Expose this port after create", parseIntegerOption)
310
- .option("--wait", "Wait for sandbox running and published port readiness")
311
- .option("--probe-path <path>", "HTTP path to probe when --publish-port is set", "/")
312
- .option("--source <source>", "Source: git:https://..., tarball:https://..., or snapshot:<id>")
313
- .option("--revision <revision>", "Git revision/branch when --source git: is used")
314
- .option("--depth <n>", "Git clone depth when --source git: is used", parseIntegerOption)
315
- .option("--snapshot <id>", "Create from a sandbox snapshot")
316
- .option("--workspace <id-or-slug>", "Workspace ID/slug")
317
- .option("--external-workspace <id>", "White-label workspace/customer ID for billing attribution")
318
- .option("--external-user <id>", "White-label user ID for billing attribution")
319
- .option("--external-project <id>", "White-label project ID for billing attribution")
320
- .option("--agent-profile <id>", "Agent runtime profile ID to mount into the sandbox")
321
- .option("--skip-agent-profile", "Do not apply the tenant/workspace default agent runtime profile")
322
- .option("--network-policy <policy>", "Network policy: allow-all or deny-all")
323
- .option("--allowed-domain <domain>", "Allowed egress domain. Repeatable.", collectOption, [])
324
- .option("--allowed-cidr <cidr>", "Allowed egress CIDR. Repeatable.", collectOption, [])
325
- .option("--denied-cidr <cidr>", "Denied egress CIDR. Repeatable.", collectOption, [])
326
- .option("--always-on", "Keep the sandbox running until explicitly stopped or destroyed")
327
- .option("--non-persistent", "Discard filesystem state on timeout instead of pausing for resume")
328
- .option("--auto-start", "Seed and start the template app after the sandbox reaches running"))
329
- .option("--json", "Output as JSON")
330
- .addHelpText("after", `
331
- Note:
332
- White-label preview domains require external attribution at creation.
333
- Pass --external-workspace / --external-user / --external-project when the
334
- sandbox will be exposed on a white-label preview domain.
335
- `)
336
- .action((opts) => runAction(async () => {
337
- const t0 = Date.now();
338
- const json = !!isJsonMode(opts) || process.env["MIOSA_JSON"] === "1";
339
- if (opts.data) {
340
- // Raw --data/--input/--file body — still idempotent and retry-safe.
341
- const idempotencyKey = opts.idempotencyKey ?? generateIdempotencyKey();
342
- const dataBody = parseData(opts.data, opts.input, opts.file) ?? {};
343
- const value = unwrap(await client().apiPostWithRetry(apiPath("/sandboxes"), dataBody, { "Idempotency-Key": idempotencyKey }));
344
- if (json) {
345
- printValue(value, opts);
346
- return;
347
- }
348
- renderCreateSuccess(value, Date.now() - t0);
349
- return;
350
- }
351
- const body = {
352
- template_id: opts.template ?? "miosa-sandbox",
353
- };
354
- if (opts.name)
355
- body["name"] = opts.name;
356
- const resources = resolveSandboxResources(opts);
357
- if (resources.size)
358
- body["size"] = resources.size;
359
- if (resources.legacy) {
360
- body["cpu_count"] = resources.legacy.cpu;
361
- body["memory_mb"] = resources.legacy.memory;
362
- body["disk_size_mb"] = resources.legacy.disk;
363
- }
364
- const timeoutSec = opts.timeout ?? DEFAULT_INTERACTIVE_SANDBOX_TIMEOUT_SEC;
365
- body["timeout_sec"] = timeoutSec;
366
- body["idle_timeout_sec"] = opts.idleTimeout ?? 0;
367
- if (opts.source)
368
- body["source"] = opts.source;
369
- if (opts.revision)
370
- body["revision"] = opts.revision;
371
- if (opts.depth != null)
372
- body["depth"] = opts.depth;
373
- if (opts.snapshot)
374
- body["snapshot_id"] = opts.snapshot;
375
- if (opts.agentProfile) {
376
- body["agent_runtime_profile_id"] = opts.agentProfile;
377
- }
378
- if (opts.skipAgentProfile) {
379
- body["skip_agent_runtime_profile"] = true;
380
- }
381
- const workspace = opts.workspace ??
382
- program.opts().workspace ??
383
- process.env["MIOSA_WORKSPACE"];
384
- if (workspace)
385
- body["workspace_id"] = workspace;
386
- if (opts.externalWorkspace) {
387
- body["external_workspace_id"] = opts.externalWorkspace;
388
- }
389
- if (opts.externalUser) {
390
- body["external_user_id"] = opts.externalUser;
391
- }
392
- if (opts.externalProject) {
393
- body["external_project_id"] = opts.externalProject;
394
- }
395
- const networkPolicy = buildNetworkPolicy(opts);
396
- if (networkPolicy) {
397
- body["metadata"] = {
398
- ...(body["metadata"] ??
399
- {}),
400
- network_policy: networkPolicy,
401
- };
402
- }
403
- if (opts.alwaysOn)
404
- body["always_on"] = true;
405
- body["persistent"] = opts.nonPersistent ? false : true;
406
- if (opts.autoStart)
407
- body["auto_start"] = true;
408
- // Auto-generate an Idempotency-Key when none is supplied, and retry
409
- // on 429/5xx with the SAME key so a retried create dedups to one
410
- // sandbox instead of billing a duplicate.
411
- const idempotencyKey = opts.idempotencyKey ?? generateIdempotencyKey();
412
- const raw = unwrap(await client().apiPostWithRetry(apiPath("/sandboxes"), body, {
413
- "Idempotency-Key": idempotencyKey,
414
- }));
415
- const sb = (raw ?? {});
416
- const id = String(sb["id"] ?? "");
417
- if (opts.publishPort != null && id) {
418
- if (opts.wait) {
419
- const preview = await waitSandboxReady(id, opts.publishPort, opts.probePath ?? "/", Math.max(Math.min(opts.timeout ?? DEFAULT_CREATE_WAIT_TIMEOUT_SEC, DEFAULT_CREATE_WAIT_TIMEOUT_SEC), 30));
420
- const latest = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}`)));
421
- Object.assign(sb, latest, {
422
- preview,
423
- preview_url: preview.url,
424
- ready: preview.ready,
425
- });
426
- }
427
- else {
428
- const preview = await previewSandbox(id, opts.publishPort, {
429
- wait: false,
430
- timeout: 1,
431
- probePath: opts.probePath ?? "/",
432
- });
433
- sb["preview"] = preview;
434
- sb["preview_url"] = preview.url;
435
- }
436
- }
437
- else if (opts.wait && id) {
438
- const latest = await waitForSandboxRunning(client(), id, Math.max(Math.min(timeoutSec, DEFAULT_CREATE_WAIT_TIMEOUT_SEC), 30),
439
- // create --wait must guarantee exec works before returning.
440
- true);
441
- Object.assign(sb, latest, { ready: true });
442
- }
443
- if (json) {
444
- console.log(JSON.stringify(sb, null, 2));
445
- return;
446
- }
447
- renderCreateSuccess(sb, Date.now() - t0);
448
- }));
449
- // delete
450
- sandbox
451
- .command("delete <sandbox-id>")
452
- .description("Permanently delete a Sandbox (legacy API extension)")
453
- .requiredOption("--force", "Confirm permanent filesystem deletion")
454
- .option("--json", "Output as JSON")
455
- .action((id, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}`, opts)));
456
- // destroy / rm — aliases for delete
457
- sandbox
458
- .command("destroy <sandbox-id>")
459
- .alias("rm")
460
- .description("Permanently destroy a Sandbox (legacy API extension)")
461
- .requiredOption("--force", "Confirm permanent filesystem deletion")
462
- .option("--json", "Output as JSON")
463
- .action((id, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}`, opts)));
464
- // action — lifecycle operations: pause, resume
465
- addDataOption(sandbox
466
- .command("action <sandbox-id> <action>")
467
- .description("Run a lifecycle action: pause, resume"))
468
- .option("--json", "Output as JSON")
469
- .action((id, action, opts) => runAction(async () => {
470
- const allowed = ["pause", "resume"];
471
- if (!allowed.includes(action)) {
472
- throw new Error(`Unsupported action "${action}". Use: ${allowed.join(", ")}`);
473
- }
474
- await postAndPrint(`/sandboxes/${enc(id)}/${enc(action)}`, opts, {});
475
- }));
476
- sandbox
477
- .command("pause <sandbox-id>")
478
- .description("Pause a running persistent Sandbox and preserve its workspace")
479
- .option("--json", "Output as JSON")
480
- .action((id, opts) => runAction(() => postAndPrint(`/sandboxes/${enc(id)}/pause`, opts, {})));
481
- // stop — snapshot + pause (mirrors `box stop`)
482
- sandbox
483
- .command("stop <sandbox-id>")
484
- .description("Stop a persistent Sandbox session and preserve its filesystem")
485
- .option("--no-snapshot", "Deprecated compatibility flag; stop still preserves state server-side")
486
- .option("--json", "Output as JSON")
487
- .action((id, opts) => runAction(async () => {
488
- const stopped = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/stop`), {}));
489
- if (isJsonMode(opts)) {
490
- console.log(JSON.stringify(stopped, null, 2));
491
- return;
492
- }
493
- const row = (stopped ?? {});
494
- console.log(chalk.green("Sandbox stopped"));
495
- console.log(kvPanel([
496
- { label: "ID", value: str(row["id"] ?? id) },
497
- {
498
- label: "State",
499
- value: statusColor(str(row["state"] ?? "paused")),
500
- },
501
- { label: "Resume", value: `miosa sandbox resume ${id}` },
502
- { label: "Destroy", value: `miosa sandbox destroy ${id}` },
503
- ]));
504
- }));
505
- // resume — direct shortcut (mirrors `box resume`)
506
- sandbox
507
- .command("resume <sandbox-id>")
508
- .description("Resume a paused Sandbox")
509
- .option("--idempotency-key <key>", "Retry-safe lifecycle key sent as Idempotency-Key")
510
- .option("--json", "Output as JSON")
511
- .action((id, opts) => runAction(() => resumeSandboxAndPrint(id, opts)));
512
- // fork — clone from snapshot in one call (mirrors `box fork`)
513
- sandbox
514
- .command("fork <sandbox-id>")
515
- .description("Snapshot and fork a running Sandbox into a new Sandbox")
516
- .option("--template <template>", "Template or immutable image override")
517
- .option("--timeout <duration>", "Fork timeout", parseDurationSec)
518
- .option("--idempotency-key <key>", "Retry-safe fork key sent as Idempotency-Key")
519
- .option("--json", "Output as JSON")
520
- .action((id, opts) => runAction(async () => {
521
- const body = {};
522
- if (opts.template)
523
- body["template_id"] = opts.template;
524
- if (opts.timeout != null)
525
- body["timeout_sec"] = opts.timeout;
526
- const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/fork`), body, opts.idempotencyKey
527
- ? { "Idempotency-Key": opts.idempotencyKey }
528
- : undefined));
529
- printValue(result, opts);
530
- }));
531
- // desktop — open the Sandbox's web desktop URL (mirrors `box desktop`).
532
- // Sandboxes are headless by default; only templates that expose a desktop
533
- // port (e.g. `miosa-sandbox` w/ Kasm) emit a `preview_url`. We surface that.
534
- sandbox
535
- .command("desktop <sandbox-id>")
536
- .description("Print the Sandbox's web desktop URL (when the template exposes one)")
537
- .option("--json", "Output as JSON")
538
- .action((id, opts) => runAction(async () => {
539
- if (isJsonMode(opts)) {
540
- await getAndPrint(`/sandboxes/${enc(id)}`, opts);
541
- return;
542
- }
543
- const sb = (await getAndPrint(`/sandboxes/${enc(id)}`, {
544
- json: true,
545
- }));
546
- if (sb?.preview_url) {
547
- console.log(sb.preview_url);
548
- }
549
- else {
550
- console.error(chalk.yellow("No desktop URL on this Sandbox. Use `miosa desktop open <computer-id>` for Computers, or expose a port via `miosa sandbox` for headless previews."));
551
- process.exitCode = 1;
552
- }
553
- }));
554
- registerSandboxConnectorCommands(sandbox);
555
- // run-agent - invoke an in-Sandbox AI runner CLI.
556
- // Implemented through Runs so callers get a stable run response while
557
- // execution still happens inside the remote filesystem.
558
- sandbox
559
- .command("run-agent <sandbox-id> <instruction...>")
560
- .description("Run an in-Sandbox AI agent runtime with the given instruction")
561
- .option("--runner <name>", "Runner: claude (default), claude-code, codex, pi, hermes, osa, custom")
562
- .option("--runtime-command <command>", "Executable command for --runner custom, e.g. 'hermes-agent run'")
563
- .option("--model <name>", "Provider-specific model name")
564
- .option("--connector <uid>", "MIOSA Connect connector UID to preflight before running the agent")
565
- .option("--preflight", "Verify the Sandbox has the requested provider connector before exec")
566
- .option("--cwd <path>", "Working directory inside the Sandbox")
567
- .option("--env <KEY=VALUE>", `Environment variable for the Agent Run. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
568
- .option("--env-file <path>", ENV_FILE_OPTION_HELP)
569
- .option("--env-stdin", ENV_STDIN_OPTION_HELP)
570
- .option("--agent-profile <id>", "Agent runtime profile ID")
571
- .option("--skip-agent-profile", "Do not apply the default agent runtime profile")
572
- .option("--external-workspace <id>", "White-label workspace/customer ID for billing attribution")
573
- .option("--external-user <id>", "White-label user ID for billing attribution")
574
- .option("--external-project <id>", "White-label project ID for billing attribution")
575
- .option("--timeout <sec>", "Exec timeout in seconds", parseIntegerOption)
576
- .option("--json", "Output as JSON")
577
- .action((id, words, opts) => runAction(async () => {
578
- const runner = opts.runner ?? "claude";
579
- if (!isSupportedRunAgentRunner(runner)) {
580
- const allowedRunners = supportedRunAgentRunners();
581
- throw new Error(`Unsupported runner "${runner}". Use: ${allowedRunners.join(", ")}`);
582
- }
583
- if (opts.runtimeCommand && runner !== "custom") {
584
- throw new Error("--runtime-command can only be used with --runner custom");
585
- }
586
- if (opts.connector || opts.preflight) {
587
- await preflightSandboxConnector(id, {
588
- provider: runner,
589
- connector: opts.connector,
590
- model: opts.model,
591
- cwd: opts.cwd,
592
- });
593
- }
594
- const instruction = words.join(" ");
595
- const body = {
596
- target_kind: "sandbox",
597
- target_id: id,
598
- runner,
599
- instruction,
600
- };
601
- if (opts.runtimeCommand)
602
- body["command"] = opts.runtimeCommand;
603
- if (opts.model)
604
- body["model"] = opts.model;
605
- if (opts.cwd)
606
- body["cwd"] = opts.cwd;
607
- const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
608
- if (Object.keys(env).length > 0) {
609
- body["env"] = env;
610
- }
611
- if (opts.agentProfile) {
612
- body["agent_runtime_profile_id"] = opts.agentProfile;
613
- }
614
- if (opts.skipAgentProfile) {
615
- body["skip_agent_runtime_profile"] = true;
616
- }
617
- if (opts.externalWorkspace) {
618
- body["external_workspace_id"] = opts.externalWorkspace;
619
- }
620
- if (opts.externalUser) {
621
- body["external_user_id"] = opts.externalUser;
622
- }
623
- if (opts.externalProject) {
624
- body["external_project_id"] = opts.externalProject;
625
- }
626
- if (opts.timeout != null)
627
- body["timeout"] = opts.timeout;
628
- const run = unwrap(await client().apiPost(apiPath("/runs"), body));
629
- if (isJsonMode(opts)) {
630
- console.log(JSON.stringify(run, null, 2));
631
- return;
632
- }
633
- console.log();
634
- console.log(kvPanel([
635
- { label: "Run", value: chalk.bold(str(run["id"])) },
636
- { label: "Target", value: `${str(run["target_kind"])} ${id}` },
637
- { label: "Runner", value: str(run["runner"]) },
638
- { label: "Status", value: statusColor(str(run["status"])) },
639
- { label: "Exit", value: str(run["exit_code"]) },
640
- ]));
641
- const output = str(run["output"]).trim();
642
- const stderr = str(run["stderr"]).trim();
643
- if (output) {
644
- console.log();
645
- console.log(output);
646
- }
647
- if (stderr) {
648
- console.error();
649
- console.error(chalk.red(stderr));
650
- }
651
- console.log();
652
- }));
653
- // exec — positional command arg; --data body overrides when supplied
654
- addDataOption(sandbox
655
- .command("exec <sandbox-id> [command...]")
656
- .description('Run a command inside a Sandbox (positional args joined as shell command). Use `--` before the command to pass flags, e.g. `sandbox exec <id> -- bash -c "cd x && y"`.')
657
- .allowUnknownOption()
658
- .allowExcessArguments()
659
- .option("--cwd <path>", "Working directory inside the Sandbox")
660
- .option("--workdir <path>", "Alias for --cwd")
661
- .option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
662
- .option("--command <command>", "Alias for --cmd")
663
- .option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
664
- .option("--env <pair>", `Environment variable KEY=VALUE. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
665
- .option("--env-file <path>", ENV_FILE_OPTION_HELP)
666
- .option("--env-stdin", ENV_STDIN_OPTION_HELP)
667
- .option("--background", "Start the command in the background and return immediately")
668
- .option("--detached", "Create a durable backend command and return command_id immediately")
669
- .option("--follow", "Stream command output until it exits (alias --stream)")
670
- .option("--stream", "Alias for --follow")
671
- .option("--user <user>", "Run command as user")
672
- .option("--sudo", "Run command through sudo")
673
- .option("--tty", "Request TTY metadata for command resource")
674
- .option("--interactive", "Request interactive metadata for command resource")
675
- .option("--timeout <sec>", "Exec timeout in seconds", parseIntegerOption))
676
- .option("--json", "Output as JSON")
677
- .action((id, words, opts) => runAction(async () => {
678
- opts.follow = opts.follow || opts.stream;
679
- if (opts.data) {
680
- await postSandboxExecAndPrint(id, opts, {});
681
- return;
682
- }
683
- const cmd = resolveSandboxCommand(words, opts);
684
- const effectiveCommand = opts.background
685
- ? backgroundCommand(cmd)
686
- : cmd;
687
- const cwd = opts.cwd ?? opts.workdir;
688
- const body = cmd
689
- ? { command: commandInCwd(effectiveCommand, cwd) }
690
- : {};
691
- if (cwd) {
692
- body["cwd"] = cwd;
693
- body["dir"] = cwd;
694
- }
695
- const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
696
- if (opts.detached) {
697
- const result = await createSandboxCommand(id, cmd, {
698
- cwd,
699
- env,
700
- user: opts.user,
701
- sudo: !!opts.sudo,
702
- tty: !!opts.tty,
703
- interactive: !!opts.interactive,
704
- timeout: opts.timeout,
705
- });
706
- if (isJsonMode(opts)) {
707
- console.log(JSON.stringify(result, null, 2));
708
- return;
709
- }
710
- console.log(String(result["id"] ?? result["command_id"] ?? ""));
711
- return;
712
- }
713
- if (opts.follow) {
714
- await runFollowExec(id, cmd, cwd, env, opts.timeout, opts);
715
- return;
716
- }
717
- if (Object.keys(env).length > 0)
718
- body["env"] = env;
719
- if (opts.timeout != null)
720
- body["timeout"] = opts.timeout;
721
- await postSandboxExecAndPrint(id, opts, body);
722
- }));
723
- // run — alias for exec with identical semantics
724
- addDataOption(sandbox
725
- .command("run <sandbox-id> [command...]")
726
- .description("Run a command inside a Sandbox (alias for exec). Use `--` before the command to pass flags.")
727
- .allowUnknownOption()
728
- .allowExcessArguments()
729
- .option("--cwd <path>", "Working directory inside the Sandbox")
730
- .option("--workdir <path>", "Alias for --cwd")
731
- .option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
732
- .option("--command <command>", "Alias for --cmd")
733
- .option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
734
- .option("--env <pair>", `Environment variable KEY=VALUE. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
735
- .option("--env-file <path>", ENV_FILE_OPTION_HELP)
736
- .option("--env-stdin", ENV_STDIN_OPTION_HELP)
737
- .option("--background", "Start the command in the background and return immediately")
738
- .option("--detached", "Create a durable backend command and return command_id immediately")
739
- .option("--follow", "Stream command output until it exits (alias --stream)")
740
- .option("--stream", "Alias for --follow")
741
- .option("--user <user>", "Run command as user")
742
- .option("--sudo", "Run command through sudo")
743
- .option("--tty", "Request TTY metadata for command resource")
744
- .option("--interactive", "Request interactive metadata for command resource")
745
- .option("--timeout <sec>", "Exec timeout in seconds", parseIntegerOption))
746
- .option("--json", "Output as JSON")
747
- .action((id, words, opts) => runAction(async () => {
748
- opts.follow = opts.follow || opts.stream;
749
- if (opts.data) {
750
- await postSandboxExecAndPrint(id, opts, {});
751
- return;
752
- }
753
- const cmd = resolveSandboxCommand(words, opts);
754
- const effectiveCommand = opts.background
755
- ? backgroundCommand(cmd)
756
- : cmd;
757
- const cwd = opts.cwd ?? opts.workdir;
758
- const body = cmd
759
- ? { command: commandInCwd(effectiveCommand, cwd) }
760
- : {};
761
- if (cwd) {
762
- body["cwd"] = cwd;
763
- body["dir"] = cwd;
764
- }
765
- const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
766
- if (opts.detached) {
767
- const result = await createSandboxCommand(id, cmd, {
768
- cwd,
769
- env,
770
- user: opts.user,
771
- sudo: !!opts.sudo,
772
- tty: !!opts.tty,
773
- interactive: !!opts.interactive,
774
- timeout: opts.timeout,
775
- });
776
- if (isJsonMode(opts)) {
777
- console.log(JSON.stringify(result, null, 2));
778
- return;
779
- }
780
- console.log(String(result["id"] ?? result["command_id"] ?? ""));
781
- return;
782
- }
783
- if (opts.follow) {
784
- await runFollowExec(id, cmd, cwd, env, opts.timeout, opts);
785
- return;
786
- }
787
- if (Object.keys(env).length > 0)
788
- body["env"] = env;
789
- if (opts.timeout != null)
790
- body["timeout"] = opts.timeout;
791
- await postSandboxExecAndPrint(id, opts, body);
792
- }));
793
- sandbox
794
- .command("ports <sandbox-id>")
795
- .description("List listening TCP ports inside a Sandbox")
796
- .option("--json", "Output as JSON")
797
- .action((id, opts) => runAction(async () => {
798
- const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/ports`));
799
- const ports = sandboxPortsFromResponse(result);
800
- if (isJsonMode(opts)) {
801
- console.log(JSON.stringify(result, null, 2));
802
- return;
803
- }
804
- if (ports.length === 0) {
805
- console.log(chalk.dim("(no listening ports)"));
806
- return;
807
- }
808
- renderTable(ports, [
809
- {
810
- header: "PROTO",
811
- key: (p) => p.protocol ?? "tcp",
812
- width: 8,
813
- },
814
- {
815
- header: "PORT",
816
- key: "port",
817
- width: 8,
818
- },
819
- {
820
- header: "ADDRESS",
821
- key: "address",
822
- width: 24,
823
- },
824
- {
825
- header: "STATE",
826
- key: (p) => p.state ?? "listen",
827
- width: 10,
828
- },
829
- {
830
- header: "PROCESS",
831
- key: (p) => p.process?.name
832
- ? `${p.process.name}${p.process.pid ? `:${p.process.pid}` : ""}`
833
- : chalk.dim("-"),
834
- width: 28,
835
- },
836
- ]);
837
- }));
838
- sandbox
839
- .command("metrics <sandbox-id>")
840
- .description("Show Sandbox resource, uptime, timeout, and readiness metrics")
841
- .option("--window <window>", "Metrics window: 1h, 24h, or 7d", "1h")
842
- .option("--json", "Output as JSON")
843
- .action((id, opts) => runAction(async () => {
844
- const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/metrics?window=${enc(opts.window)}`));
845
- if (isJsonMode(opts)) {
846
- console.log(JSON.stringify(result, null, 2));
847
- return;
848
- }
849
- renderResourceMetrics("Sandbox metrics", result);
850
- }));
851
- sandbox
852
- .command("deploy [local-dir]")
853
- .description("Upload an app directory, start it in a sandbox, expose a preview URL, and wait for readiness")
854
- .option("--sandbox <id>", "Existing sandbox ID. Creates one when omitted")
855
- .option("--template <template>", "Template for new sandbox", "miosa-sandbox")
856
- .option("--name <name>", "Name for a new sandbox")
857
- .option("--port <port>", "Preview port", parseIntegerOption)
858
- .option("--publish-port <port>", "Alias for --port", parseIntegerOption)
859
- .option("--start <command>", "Start command to run inside /workspace")
860
- .option("--install-command <command>", "Install command to run before start")
861
- .option("--no-install", "Skip automatic dependency install")
862
- .option("--source <source>", "Source: git:https://... or tarball:https://... for repo-backed preview deploy")
863
- .option("--revision <revision>", "Git revision/branch for --source git:...")
864
- .option("--depth <n>", "Git clone depth for --source git:...", parseIntegerOption)
865
- .option("--wait", "Wait until the public preview returns a good HTTP status")
866
- .option("--timeout <duration>", "Wait timeout, e.g. 180s or 3m", parseDurationSec, 180)
867
- .option("--probe-path <path>", "HTTP path to probe")
868
- .option("--json", "Output as JSON")
869
- .action(async (localDir = ".", opts) => {
870
- try {
871
- const result = await deploySandbox(localDir, opts);
872
- if (isJsonMode(opts)) {
873
- console.log(JSON.stringify(result, null, 2));
874
- return;
875
- }
876
- console.log();
877
- console.log(` ${chalk.bold("Sandbox")} ${result.sandbox_id}`);
878
- console.log(` ${chalk.bold("Port")} ${result.port}`);
879
- console.log(` ${chalk.bold("Preview")} ${chalk.cyan(result.preview_url)}`);
880
- console.log(` ${chalk.bold("Ready")} ${result.preview_ready
881
- ? chalk.green("yes")
882
- : chalk.yellow("not verified")}`);
883
- console.log();
884
- }
885
- catch (err) {
886
- handleSandboxDeployError(err, opts);
887
- }
888
- });
889
- sandbox
890
- .command("preview <sandbox-id>")
891
- .description("Expose a sandbox port and optionally wait for the public preview to answer")
892
- .requiredOption("--port <port>", "Port inside the sandbox to expose", parseIntegerOption)
893
- .option("--wait", "Wait until the public URL returns a good HTTP status")
894
- .option("--timeout <sec>", "Wait timeout in seconds", parseDurationSec, 120)
895
- .option("--probe-path <path>", "HTTP path to probe", "/")
896
- .option("--json", "Output as JSON")
897
- .action((id, opts) => runAction(async () => {
898
- const result = await previewSandbox(id, opts.port, {
899
- wait: !!opts.wait,
900
- timeout: opts.timeout,
901
- probePath: opts.probePath,
902
- });
903
- if (isJsonMode(opts)) {
904
- console.log(JSON.stringify(result, null, 2));
905
- return;
906
- }
907
- console.log(result.url);
908
- if (!result.ready) {
909
- console.error(chalk.yellow(`Preview route created but not verified yet (${result.error ?? result.status ?? "pending"}).`));
910
- }
911
- }));
912
- sandbox
913
- .command("wait <sandbox-id>")
914
- .description("Wait for sandbox VM readiness; pass --port to also verify app preview readiness")
915
- .option("--port <port>", "Port inside the sandbox to check", parseIntegerOption)
916
- .option("--url", "Print only the ready public preview URL")
917
- .option("--timeout <sec>", "Wait timeout in seconds", parseDurationSec, 120)
918
- .option("--probe-path <path>", "HTTP path to probe", "/")
919
- .option("--json", "Output as JSON")
920
- .action((id, opts) => runAction(async () => {
921
- const result = opts.port == null
922
- ? await waitSandboxVmReady(id, opts.timeout)
923
- : await waitSandboxReady(id, opts.port, opts.probePath, opts.timeout);
924
- if (opts.url && result.url) {
925
- console.log(result.url);
926
- return;
927
- }
928
- if (isJsonMode(opts)) {
929
- console.log(JSON.stringify(result, null, 2));
930
- return;
931
- }
932
- console.log(chalk.green("Ready"));
933
- if (result.url)
934
- console.log(chalk.cyan(result.url));
935
- }));
936
- sandbox
937
- .command("domain <sandbox-id> <port>")
938
- .description("Return the public preview domain/URL for a sandbox port")
939
- .option("--wait", "Wait until the public URL is externally ready")
940
- .option("--timeout <duration>", "Wait timeout", parseDurationSec, 120)
941
- .option("--probe-path <path>", "HTTP path to probe", "/")
942
- .option("--json", "Output as JSON")
943
- .action((id, portText, opts) => runAction(async () => {
944
- const port = Number(portText);
945
- if (!Number.isInteger(port))
946
- throw new UserError(`Invalid port: ${portText}`);
947
- const result = await previewSandbox(id, port, {
948
- wait: !!opts.wait,
949
- timeout: opts.timeout,
950
- probePath: opts.probePath,
951
- });
952
- const data = { port, ...result };
953
- if (isJsonMode(opts)) {
954
- console.log(JSON.stringify(data, null, 2));
955
- return;
956
- }
957
- console.log(result.url);
958
- }));
959
- sandbox
960
- .command("publish <sandbox-id>")
961
- .description("Publish a sandbox workspace to durable MIOSA Deploy hosting")
962
- .option("--path <path>", "Path inside the sandbox to publish", "/workspace")
963
- .option("--app <id>", "Existing app/deployment ID to publish a new release to")
964
- .option("--name <name>", "Name for a new durable app")
965
- .option("--slug <slug>", "Production slug for a new durable app")
966
- .option("--environment <name>", "Target environment label", "production")
967
- .option("--build-command <cmd>", "Build command to run before publishing")
968
- .option("--run-command <cmd>", "Run command for dynamic/server deployments")
969
- .option("--docker-deploy", "Publish onto the workspace App Engine runtime instead of standard app hosting")
970
- .option("--domain <domain>", "Custom domain to attach")
971
- .option("--deployment-type <type>", "Deployment runtime type: miosa_deploy, docker_deploy, dynamic, static")
972
- .option("--database <mode>", "none, create:postgres, postgres, or existing:<db-id>")
973
- .option("--port <port>", "Runtime port for dynamic deployments", parseIntegerOption)
974
- .option("--wait", "Wait for the production URL to answer")
975
- .option("--no-wait", "Return immediately without waiting (this is the default)")
976
- .option("--timeout <duration>", "Wait timeout", parseDurationSec, 600)
977
- .option("--json", "Output as JSON")
978
- .action((id, opts) => runAction(async () => {
979
- const result = await publishSandbox(id, opts);
980
- if (isJsonMode(opts)) {
981
- console.log(JSON.stringify(result, null, 2));
982
- return;
983
- }
984
- console.log();
985
- console.log(` ${chalk.bold("Type")} deployment`);
986
- console.log(` ${chalk.bold("App")} ${result.deployment_id ?? result.app_id ?? ""}`);
987
- if (result.release_id)
988
- console.log(` ${chalk.bold("Release")} ${result.release_id}`);
989
- if (result.deployment_product)
990
- console.log(` ${chalk.bold("Product")} ${String(result.deployment_product)}`);
991
- if (result.docker_deploy_host_id)
992
- console.log(` ${chalk.bold("Docker")} ${String(result.docker_deploy_host_id)}`);
993
- if (result.url)
994
- console.log(` ${chalk.bold("URL")} ${chalk.cyan(String(result.url))}`);
995
- console.log(` ${chalk.bold("Ready")} ${result.ready ? chalk.green("yes") : chalk.yellow("pending")}`);
996
- console.log();
997
- }));
998
- const command = sandbox
999
- .command("command")
1000
- .description("Inspect and control detached sandbox commands");
1001
- command
1002
- .command("get <sandbox-id> <command-id>")
1003
- .option("--json", "Output as JSON")
1004
- .action((id, commandId, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}/commands/${enc(commandId)}`, opts)));
1005
- command
1006
- .command("logs <sandbox-id> <command-id>")
1007
- .option("--follow", "Follow logs (currently returns tailed logs when backend SSE is unavailable)")
1008
- .option("--tail <n>", "Number of lines", parseIntegerOption, 200)
1009
- .option("--json", "Output as JSON")
1010
- .action((id, commandId, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}/commands/${enc(commandId)}/logs?tail=${opts.tail}`, opts)));
1011
- command
1012
- .command("kill <sandbox-id> <command-id>")
1013
- .option("--json", "Output as JSON")
1014
- .action((id, commandId, opts) => runAction(() => postAndPrint(`/sandboxes/${enc(id)}/commands/${enc(commandId)}/kill`, opts, {})));
1015
- sandbox
1016
- .command("extend <sandbox-id>")
1017
- .description("Extend or replace a sandbox timeout")
1018
- .requiredOption("--timeout <duration>", "New timeout, e.g. 2h", parseDurationSec)
1019
- .option("--json", "Output as JSON")
1020
- .action((id, opts) => runAction(() => postAndPrint(`/sandboxes/${enc(id)}/extend`, opts, {
1021
- timeout_sec: opts.timeout,
1022
- })));
1023
- sandbox
1024
- .command("usage <sandbox-id>")
1025
- .description("Show sandbox runtime, rough resource usage, and estimated cost")
1026
- .option("--json", "Output as JSON")
1027
- .action((id, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}/usage`, opts)));
1028
- const config = sandbox
1029
- .command("config")
1030
- .description("Configure sandbox settings");
1031
- config
1032
- .command("network-policy <sandbox-id>")
1033
- .description("Store egress network policy metadata for a sandbox")
1034
- .option("--network-policy <policy>", "Network policy: allow-all or deny-all")
1035
- .option("--allowed-domain <domain>", "Allowed egress domain. Repeatable.", collectOption, [])
1036
- .option("--allowed-cidr <cidr>", "Allowed egress CIDR. Repeatable.", collectOption, [])
1037
- .option("--denied-cidr <cidr>", "Denied egress CIDR. Repeatable.", collectOption, [])
1038
- .option("--json", "Output as JSON")
1039
- .action((id, opts) => runAction(async () => {
1040
- const policy = buildNetworkPolicy(opts);
1041
- if (!policy)
1042
- throw new UserError("No network policy options provided.");
1043
- const result = unwrap(await client().apiPatch(apiPath(`/sandboxes/${enc(id)}`), {
1044
- metadata: { network_policy: policy },
1045
- }));
1046
- if (isJsonMode(opts)) {
1047
- console.log(JSON.stringify(result, null, 2));
1048
- return;
1049
- }
1050
- console.log(chalk.green("Network policy updated."));
1051
- }));
1052
- const service = sandbox
1053
- .command("service")
1054
- .description("Manage named long-running processes inside a sandbox");
1055
- service
1056
- .command("start <sandbox-id> <name>")
1057
- .alias("up")
1058
- .requiredOption("--cmd <command>", "Command to start")
1059
- .option("--cwd <path>", "Working directory inside the sandbox", "/workspace")
1060
- .option("--port <port>", "Port served by this service", parseIntegerOption)
1061
- .option("--json", "Output as JSON")
1062
- .action((id, name, opts) => runAction(async () => {
1063
- const result = await startSandboxService(id, name, opts);
1064
- if (isJsonMode(opts)) {
1065
- console.log(JSON.stringify(result, null, 2));
1066
- return;
1067
- }
1068
- console.log(chalk.green(`Started ${name}`));
1069
- if (result.preview_url)
1070
- console.log(chalk.cyan(String(result.preview_url)));
1071
- }));
1072
- service
1073
- .command("restart <sandbox-id> <name>")
1074
- .option("--cmd <command>", "Ignored when backend service metadata already exists")
1075
- .option("--cwd <path>", "Working directory inside the sandbox", "/workspace")
1076
- .option("--port <port>", "Port served by this service", parseIntegerOption)
1077
- .option("--json", "Output as JSON")
1078
- .action((id, name, opts) => runAction(async () => {
1079
- const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/services/${enc(name)}/restart`), {}));
1080
- if (isJsonMode(opts)) {
1081
- console.log(JSON.stringify(result, null, 2));
1082
- return;
1083
- }
1084
- console.log(chalk.green(`Restarted ${name}`));
1085
- if (result.preview_url)
1086
- console.log(chalk.cyan(String(result.preview_url)));
1087
- }));
1088
- service
1089
- .command("status <sandbox-id> <name>")
1090
- .option("--json", "Output as JSON")
1091
- .action((id, name, opts) => runAction(async () => {
1092
- const result = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}/services/${enc(name)}`)));
1093
- const status = String(result["status"] ?? "unknown");
1094
- if (isJsonMode(opts)) {
1095
- console.log(JSON.stringify(result, null, 2));
1096
- return;
1097
- }
1098
- console.log(status === "running" ? chalk.green(status) : chalk.yellow(status));
1099
- }));
1100
- service
1101
- .command("logs <sandbox-id> <name>")
1102
- .option("--lines <n>", "Number of log lines", parseIntegerOption, 100)
1103
- .option("--json", "Output as JSON")
1104
- .action((id, name, opts) => runAction(async () => {
1105
- const result = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}/services/${enc(name)}/logs?tail=${Number(opts.lines) || 100}`)));
1106
- if (isJsonMode(opts)) {
1107
- console.log(JSON.stringify(result, null, 2));
1108
- return;
1109
- }
1110
- const lines = Array.isArray(result["lines"]) ? result["lines"] : [];
1111
- process.stdout.write(lines.join("\n") + (lines.length > 0 ? "\n" : ""));
1112
- }));
1113
- const env = sandbox
1114
- .command("env")
1115
- .description("Manage encrypted environment variables for a sandbox");
1116
- env
1117
- .command("list <sandbox-id>")
1118
- .description("List sandbox env var names and masked previews")
1119
- .option("--json", "Output as JSON")
1120
- .action((id, opts) => runAction(async () => {
1121
- const result = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}/env`)));
1122
- if (isJsonMode(opts)) {
1123
- console.log(JSON.stringify(result, null, 2));
1124
- return;
1125
- }
1126
- const rows = Array.isArray(result)
1127
- ? result
1128
- : [];
1129
- if (rows.length === 0) {
1130
- console.log(chalk.dim("No sandbox env vars."));
1131
- return;
1132
- }
1133
- renderTable(rows, [
1134
- {
1135
- header: "NAME",
1136
- key: "name",
1137
- width: 32,
1138
- },
1139
- {
1140
- header: "VALUE",
1141
- key: "preview",
1142
- width: 24,
1143
- },
1144
- {
1145
- header: "UPDATED",
1146
- key: "updated_at",
1147
- width: 28,
1148
- },
1149
- ]);
1150
- }));
1151
- env
1152
- .command("set <sandbox-id> [pairs...]")
1153
- .description(`Set encrypted sandbox env vars as KEY=VALUE. ${ENV_INLINE_SHELL_WARNING}`)
1154
- .option("--env-file <path>", ENV_FILE_OPTION_HELP)
1155
- .option("--env-stdin", ENV_STDIN_OPTION_HELP)
1156
- .option("--json", "Output as JSON")
1157
- .action((id, pairs, opts) => runAction(async () => {
1158
- const values = await resolveEnvInputs(parseEnvPairs(pairs ?? []), opts);
1159
- if (Object.keys(values).length === 0) {
1160
- throw new UserError("No env vars provided.", "Pass KEY=VALUE pairs, --env-file <path>, or --env-stdin.");
1161
- }
1162
- const vars = Object.entries(values).map(([key, value]) => ({
1163
- key,
1164
- value,
1165
- }));
1166
- const result = unwrap(await client().apiPut(apiPath(`/sandboxes/${enc(id)}/env`), {
1167
- vars,
1168
- }));
1169
- if (isJsonMode(opts)) {
1170
- console.log(JSON.stringify(result, null, 2));
1171
- return;
1172
- }
1173
- console.log(chalk.green(`Set ${vars.length} sandbox env var(s).`));
1174
- }));
1175
- env
1176
- .command("delete <sandbox-id> <key>")
1177
- .alias("unset")
1178
- .description("Delete an encrypted sandbox env var")
1179
- .option("--json", "Output as JSON")
1180
- .action((id, key, opts) => runAction(async () => {
1181
- const result = unwrap(await client().apiDelete(apiPath(`/sandboxes/${enc(id)}/env/${enc(key)}`)));
1182
- if (isJsonMode(opts)) {
1183
- console.log(JSON.stringify(result, null, 2));
1184
- return;
1185
- }
1186
- console.log(chalk.green(`Deleted ${key}.`));
1187
- }));
1188
- env
1189
- .command("sync <sandbox-id>")
1190
- .description("Sync encrypted sandbox env vars into the running VM")
1191
- .option("--json", "Output as JSON")
1192
- .action((id, opts) => runAction(async () => {
1193
- const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/env/sync`), {}));
1194
- if (isJsonMode(opts)) {
1195
- console.log(JSON.stringify(result, null, 2));
1196
- return;
1197
- }
1198
- const status = result && typeof result === "object" && "status" in result
1199
- ? String(result["status"])
1200
- : "ok";
1201
- console.log(chalk.green(`Sandbox env sync ${status}.`));
1202
- }));
1203
- const sandboxDb = sandbox
1204
- .command("db")
1205
- .description("Attach managed databases to sandboxes");
1206
- sandboxDb
1207
- .command("attach <sandbox-id> <database-id>")
1208
- .description("Attach a managed database and persist DATABASE_URL env vars")
1209
- .option("--json", "Output as JSON")
1210
- .action((id, databaseId, opts) => runAction(async () => {
1211
- const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/database`), { database_id: databaseId }));
1212
- if (isJsonMode(opts)) {
1213
- console.log(JSON.stringify(result, null, 2));
1214
- return;
1215
- }
1216
- console.log(chalk.green(`Attached database ${databaseId} to sandbox ${id}.`));
1217
- }));
1218
- sandbox
1219
- .command("doctor [sandbox-id]")
1220
- .description("Diagnose sandbox app readiness across sandbox state, internal HTTP, public route, and TLS/edge reachability")
1221
- .option("--port <port>", "Port inside the sandbox to check", parseIntegerOption)
1222
- .option("--probe-path <path>", "HTTP path to probe", "/")
1223
- .option("--full", "Inspect the complete canonical developer contract")
1224
- .option("--dir <path>", "Project directory for --full", ".")
1225
- .option("--json", "Output as JSON")
1226
- .action((id, opts) => runAction(async () => {
1227
- if (opts.full) {
1228
- const report = await runFullSandboxDoctor(opts.dir, id);
1229
- if (!report.ok)
1230
- process.exitCode = 1;
1231
- if (isJsonMode(opts)) {
1232
- console.log(JSON.stringify(report, null, 2));
1233
- return;
1234
- }
1235
- console.log();
1236
- console.log(chalk.bold("Sandbox Doctor Full"));
1237
- console.log();
1238
- for (const check of report.checks) {
1239
- console.log(` ${check.ok ? chalk.green("ok") : chalk.red("fail")} ${check.id}: ${check.message}`);
1240
- }
1241
- console.log();
1242
- return;
1243
- }
1244
- if (!id || opts.port == null) {
1245
- throw new UserError("Sandbox ID and --port are required unless --full is used.", "Use miosa sandbox doctor <sandbox-id> --port <port>, or miosa sandbox doctor --full.");
1246
- }
1247
- const report = await doctorSandbox(id, opts.port, opts.probePath);
1248
- if (isJsonMode(opts)) {
1249
- console.log(JSON.stringify(report, null, 2));
1250
- return;
1251
- }
1252
- renderDoctorReport(report);
1253
- }));
1254
- sandbox
1255
- .command("recover <name-or-id>")
1256
- .description("Inspect a failed or partial sandbox deploy and print recovery commands")
1257
- .option("--port <port>", "App port to diagnose", parseIntegerOption)
1258
- .option("--probe-path <path>", "HTTP path to probe", "/")
1259
- .option("--json", "Output as JSON")
1260
- .action((idOrName, opts) => runAction(async () => {
1261
- const report = await recoverSandboxDeploy(idOrName, opts);
1262
- if (isJsonMode(opts)) {
1263
- console.log(JSON.stringify(report, null, 2));
1264
- return;
1265
- }
1266
- renderRecoverReport(report);
1267
- }));
1268
- // write-file — POST /sandboxes/:id/files with base64 content.
1269
- // If <content-or-file> is an existing local path, reads the file bytes;
1270
- // otherwise treats the argument as literal UTF-8 text.
1271
- sandbox
1272
- .command("write-file <sandbox-id> <remote-path> <content-or-file>")
1273
- .description("Write a file inside a Sandbox (pass literal text or a local file path)")
1274
- .option("--json", "Output as JSON")
1275
- .action(async (id, remotePath, contentArg, opts) => {
1276
- try {
1277
- const contentBytes = fs.existsSync(contentArg)
1278
- ? fs.readFileSync(contentArg)
1279
- : Buffer.from(contentArg, "utf8");
1280
- const result = await writeBytesToSandbox(client(), id, remotePath, contentBytes);
1281
- if (!isJsonMode(opts)) {
1282
- console.log(chalk.green(`Written to ${remotePath}`));
1283
- }
1284
- else {
1285
- printValue(typeof result === "string" ? { content: result } : result, opts);
1286
- }
1287
- }
1288
- catch (err) {
1289
- handleError(err);
1290
- }
1291
- });
1292
- // read-file — GET /sandboxes/:id/files/:path, decode base64 content and print.
1293
- sandbox
1294
- .command("read-file <sandbox-id> <remote-path>")
1295
- .description("Read a file from inside a Sandbox and print its contents")
1296
- .option("--json", "Output raw JSON response")
1297
- .action(async (id, remotePath, opts) => {
1298
- try {
1299
- const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/files/read?path=${enc(remotePath)}`));
1300
- if (isJsonMode(opts)) {
1301
- console.log(JSON.stringify(typeof result === "string" ? { content: result } : result, null, 2));
1302
- return;
1303
- }
1304
- const data = result !== null &&
1305
- typeof result === "object" &&
1306
- !Array.isArray(result)
1307
- ? (result["data"] ?? result)
1308
- : result;
1309
- const contentVal = data !== null &&
1310
- typeof data === "object" &&
1311
- !Array.isArray(data) &&
1312
- "content" in data &&
1313
- typeof data["content"] === "string"
1314
- ? data["content"]
1315
- : null;
1316
- if (contentVal !== null) {
1317
- process.stdout.write(contentVal);
1318
- }
1319
- else if (typeof data === "string") {
1320
- process.stdout.write(data);
1321
- }
1322
- else {
1323
- console.log(JSON.stringify(data, null, 2));
1324
- }
1325
- }
1326
- catch (err) {
1327
- handleError(err);
1328
- }
1329
- });
1330
- // upload — read local binary file, POST to /sandboxes/:id/files as base64.
1331
- sandbox
1332
- .command("upload <sandbox-id> <local-path> <remote-path>")
1333
- .description("Upload a local file into a Sandbox")
1334
- .option("--json", "Output as JSON")
1335
- .action(async (id, localPath, remotePath, opts) => {
1336
- try {
1337
- if (!fs.existsSync(localPath)) {
1338
- console.error(chalk.red(`File not found: ${localPath}`));
1339
- process.exit(1);
1340
- }
1341
- const c = client();
1342
- const result = await writeBytesToSandbox(c, id, remotePath, fs.readFileSync(localPath));
1343
- if (isJsonMode(opts)) {
1344
- printValue(result, opts);
1345
- }
1346
- else {
1347
- const filename = path.basename(localPath);
1348
- console.log(chalk.green(`Uploaded ${filename} → ${remotePath}`));
1349
- }
1350
- }
1351
- catch (err) {
1352
- handleError(err);
1353
- }
1354
- });
1355
- sandbox
1356
- .command("upload-dir <sandbox-id> <local-dir> <remote-dir>")
1357
- .description("Upload a local directory into a Sandbox")
1358
- .option("--delete", "Delete the remote directory contents before extracting")
1359
- .option("--force", "Skip the --delete confirmation prompt")
1360
- .option("--yes", "Alias for --force")
1361
- .option("--json", "Output as JSON")
1362
- .action(async (id, localDir, remoteDir, opts) => {
1363
- try {
1364
- const result = await uploadDirToSandbox(id, localDir, remoteDir, {
1365
- delete: !!opts.delete,
1366
- force: !!(opts.force || opts.yes),
1367
- });
1368
- if (isJsonMode(opts)) {
1369
- console.log(JSON.stringify(result, null, 2));
1370
- return;
1371
- }
1372
- console.log(chalk.green(`Uploaded ${result.files_label} → ${remoteDir}`));
1373
- }
1374
- catch (err) {
1375
- handleError(err);
1376
- }
1377
- });
1378
- sandbox
1379
- .command("sync <local-dir> <remote-dir>")
1380
- .description("Sync a local directory into a sandbox")
1381
- .requiredOption("--sandbox <id>", "Sandbox ID")
1382
- .option("--delete", "Delete the remote directory contents before extracting")
1383
- .option("--force", "Skip the --delete confirmation prompt")
1384
- .option("--yes", "Alias for --force")
1385
- .option("--json", "Output as JSON")
1386
- .action(async (localDir, remoteDir, opts) => {
1387
- try {
1388
- const result = await uploadDirToSandbox(opts.sandbox, localDir, remoteDir, {
1389
- delete: !!opts.delete,
1390
- force: !!(opts.force || opts.yes),
1391
- });
1392
- if (isJsonMode(opts)) {
1393
- console.log(JSON.stringify(result, null, 2));
1394
- return;
1395
- }
1396
- console.log(chalk.green(`Synced ${result.files_label} → ${remoteDir}`));
1397
- }
1398
- catch (err) {
1399
- handleError(err);
1400
- }
1401
- });
1402
- sandbox
1403
- .command("cp <source> <target>")
1404
- .alias("copy")
1405
- .description("Copy a local file or directory into a sandbox, e.g. ./app/. sbx_123:/workspace")
1406
- .option("--delete", "Delete the remote directory contents before extracting directories")
1407
- .option("--force", "Skip the --delete confirmation prompt")
1408
- .option("--yes", "Alias for --force")
1409
- .option("--json", "Output as JSON")
1410
- .action(async (source, target, opts) => {
1411
- try {
1412
- if (isSandboxTarget(source) && !isSandboxTarget(target)) {
1413
- const parsedSource = parseSandboxTarget(source);
1414
- const result = await downloadSandboxPath(parsedSource.sandboxId, parsedSource.remotePath, target);
1415
- if (isJsonMode(opts)) {
1416
- console.log(JSON.stringify(result, null, 2));
1417
- return;
1418
- }
1419
- console.log(chalk.green(`Copied ${source} → ${target}`));
1420
- return;
1421
- }
1422
- if (isSandboxTarget(source) || !isSandboxTarget(target)) {
1423
- throw new UserError("Invalid copy direction.", "Use local → sandbox (`./app/. sbx_123:/workspace`) or sandbox → local (`sbx_123:/workspace/dist ./dist`).");
1424
- }
1425
- const parsed = parseSandboxTarget(target);
1426
- const local = source.endsWith("/.") ? source.slice(0, -2) : source;
1427
- const stat = fs.statSync(local);
1428
- if (stat.isDirectory()) {
1429
- const result = await uploadDirToSandbox(parsed.sandboxId, local, parsed.remotePath, {
1430
- delete: !!opts.delete,
1431
- force: !!(opts.force || opts.yes),
1432
- });
1433
- if (isJsonMode(opts)) {
1434
- console.log(JSON.stringify(result, null, 2));
1435
- return;
1436
- }
1437
- console.log(chalk.green(`Copied ${result.files_label} → ${target}`));
1438
- return;
1439
- }
1440
- const c = client();
1441
- await uploadFileToSandbox(c, parsed.sandboxId, local, parsed.remotePath);
1442
- if (isJsonMode(opts)) {
1443
- console.log(JSON.stringify({
1444
- sandbox_id: parsed.sandboxId,
1445
- local_path: path.resolve(local),
1446
- remote_path: parsed.remotePath,
1447
- }, null, 2));
1448
- return;
1449
- }
1450
- console.log(chalk.green(`Copied ${path.basename(local)} → ${target}`));
1451
- }
1452
- catch (err) {
1453
- handleError(err);
1454
- }
1455
- });
1456
- // download — GET /sandboxes/:id/files/:path; write to --output or stdout.
1457
- sandbox
1458
- .command("download <sandbox-id> <remote-path>")
1459
- .description("Download a file from a Sandbox to a local path or stdout")
1460
- .option("--output <file>", "Write to this local file instead of stdout")
1461
- .option("--json", "Output raw JSON response")
1462
- .action(async (id, remotePath, opts) => {
1463
- try {
1464
- const encoded = enc(remotePath.replace(/^\//, ""));
1465
- const c = client();
1466
- const bytes = await c.apiGetBinary(apiPath(`/sandboxes/${enc(id)}/files/${encoded}`));
1467
- if (opts.output) {
1468
- fs.writeFileSync(opts.output, bytes);
1469
- if (isJsonMode(opts)) {
1470
- console.log(JSON.stringify({
1471
- sandbox_id: id,
1472
- remote_path: remotePath,
1473
- output: opts.output,
1474
- bytes: bytes.length,
1475
- }, null, 2));
1476
- return;
1477
- }
1478
- console.log(chalk.green(`Downloaded ${remotePath} → ${opts.output}`));
1479
- }
1480
- else {
1481
- if (isJsonMode(opts)) {
1482
- console.log(JSON.stringify({
1483
- sandbox_id: id,
1484
- remote_path: remotePath,
1485
- bytes: bytes.length,
1486
- content_base64: bytes.toString("base64"),
1487
- }, null, 2));
1488
- return;
1489
- }
1490
- process.stdout.write(bytes);
1491
- }
1492
- }
1493
- catch (err) {
1494
- handleError(err);
1495
- }
1496
- });
1497
- sandbox
1498
- .command("export <sandbox-id> <remote-paths...>")
1499
- .description("Create a portable export for sandbox-generated files; optionally download it")
1500
- .option("--label <label>", "Human-readable export label")
1501
- .option("--filename <name>", "Filename to use when downloading")
1502
- .option("--output <file>", "Download the file/archive to this local path")
1503
- .option("--json", "Output as JSON")
1504
- .action(async (id, remotePaths, opts) => {
1505
- try {
1506
- if (remotePaths.length === 0) {
1507
- throw new UserError("At least one remote path is required.");
1508
- }
1509
- const body = remotePaths.length === 1
1510
- ? { path: remotePaths[0] }
1511
- : { paths: remotePaths };
1512
- if (opts.label)
1513
- body["label"] = opts.label;
1514
- if (opts.filename)
1515
- body["filename"] = opts.filename;
1516
- const exportData = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/exports`), body));
1517
- if (opts.output) {
1518
- const query = new URLSearchParams();
1519
- if (remotePaths.length === 1) {
1520
- query.set("path", remotePaths[0] ?? "");
1521
- }
1522
- else {
1523
- for (const remotePath of remotePaths) {
1524
- query.append("paths[]", remotePath);
1525
- }
1526
- }
1527
- if (opts.filename)
1528
- query.set("filename", opts.filename);
1529
- const bytes = await client().apiGetBinary(apiPath(`/sandboxes/${enc(id)}/exports/download?${query.toString()}`));
1530
- fs.writeFileSync(opts.output, bytes);
1531
- }
1532
- if (isJsonMode(opts)) {
1533
- console.log(JSON.stringify(opts.output
1534
- ? { ...exportData, downloaded_to: opts.output }
1535
- : exportData, null, 2));
1536
- return;
1537
- }
1538
- console.log();
1539
- console.log(kvPanel([
1540
- { label: "Export", value: chalk.bold(str(exportData["id"])) },
1541
- { label: "Sandbox", value: id },
1542
- {
1543
- label: "Status",
1544
- value: statusColor(str(exportData["status"])),
1545
- },
1546
- {
1547
- label: "Archive",
1548
- value: str(exportData["archive_download_url"]),
1549
- },
1550
- ...(opts.output
1551
- ? [{ label: "Downloaded", value: opts.output }]
1552
- : []),
1553
- ]));
1554
- const files = Array.isArray(exportData["files"])
1555
- ? exportData["files"]
1556
- : [];
1557
- if (files.length > 0) {
1558
- console.log();
1559
- renderTable(files, [
1560
- { header: "PATH", key: "path" },
1561
- {
1562
- header: "DOWNLOAD URL",
1563
- key: "download_url",
1564
- },
1565
- ]);
1566
- }
1567
- console.log();
1568
- }
1569
- catch (err) {
1570
- handleError(err);
1571
- }
1572
- });
1573
- // logs — GET /sandboxes/:id/logs
1574
- sandbox
1575
- .command("logs <sandbox-id>")
1576
- .description("Show Sandbox logs")
1577
- .option("--lines <n>", "Number of log lines to fetch", parseIntegerOption)
1578
- .option("--json", "Output as JSON")
1579
- .action((id, opts) => runAction(async () => {
1580
- const qs = opts.lines != null ? `?lines=${opts.lines}` : "";
1581
- await getAndPrint(`/sandboxes/${enc(id)}/logs${qs}`, opts);
1582
- }));
1583
- // ssh — WS tunnel → local listener → spawn system ssh client
1584
- sandbox
1585
- .command("ssh <sandbox-id>")
1586
- .description("Open an SSH session into a running Sandbox via the platform WS tunnel")
1587
- .option("-p, --port <n>", "Local port for the tunnel listener", parseIntegerOption)
1588
- .option("-l, --user <name>", "SSH user (default: root)")
1589
- .option("--no-spawn", "Print connection info instead of spawning the ssh client")
1590
- .option("--json", "Output as JSON")
1591
- .action(async (id, opts) => {
1592
- try {
1593
- await runSandboxSsh(id, opts);
1594
- }
1595
- catch (err) {
1596
- handleError(err);
1597
- }
1598
- });
1599
- sandbox
1600
- .command("shell <sandbox-id>")
1601
- .description("Open an interactive shell into a running Sandbox")
1602
- .option("-p, --port <n>", "Local port for the tunnel listener", parseIntegerOption)
1603
- .option("-l, --user <name>", "SSH user (default: root)")
1604
- .option("--json", "Output connection info as JSON")
1605
- .action(async (id, opts) => {
1606
- try {
1607
- await runSandboxSsh(id, { ...opts, spawn: !isJsonMode(opts) });
1608
- }
1609
- catch (err) {
1610
- handleError(err);
1611
- }
1612
- });
1613
- // port-forward — TCP listener → WS tunnel → sandbox port
1614
- sandbox
1615
- .command("port-forward <sandbox-id>")
1616
- .alias("forward")
1617
- .description([
1618
- "Forward a local TCP port to a port inside a Sandbox via the platform WS tunnel.",
1619
- "",
1620
- "Examples:",
1621
- " miosa sandbox port-forward sbx_123 --remote 5173",
1622
- " miosa sandbox port-forward sbx_123 --remote 5432 --local 15432",
1623
- ].join("\n"))
1624
- .requiredOption("--remote <port>", "Port inside the sandbox to reach", parseIntegerOption)
1625
- .option("--local <port>", "Local port to listen on (default = remote port)", parseIntegerOption)
1626
- .option("--wait", "Probe the local forwarded URL before returning readiness")
1627
- .option("--timeout <sec>", "Wait timeout in seconds", parseDurationSec, 30)
1628
- .option("--probe-path <path>", "HTTP path to probe when --wait is set", "/")
1629
- .option("--json", "Output as JSON")
1630
- .action(async (id, opts) => {
1631
- try {
1632
- await runSandboxPortForward(id, {
1633
- remotePort: opts.remote,
1634
- localPort: opts.local ?? opts.remote,
1635
- wait: !!opts.wait,
1636
- timeoutSec: opts.timeout,
1637
- probePath: opts.probePath,
1638
- json: !!isJsonMode(opts),
1639
- });
1640
- }
1641
- catch (err) {
1642
- handleError(err);
1643
- }
1644
- });
1645
- // checkpoint — POST /sandboxes/:id/snapshots
1646
- addDataOption(sandbox
1647
- .command("checkpoint <sandbox-id>")
1648
- .description("Create a Checkpoint (memory state snapshot) of a Sandbox"))
1649
- .option("--json", "Output as JSON")
1650
- .action((id, opts) => runAction(() => postAndPrint(`/sandboxes/${enc(id)}/snapshots`, opts, {})));
1651
- sandbox
1652
- .command("snapshot <sandbox-id>")
1653
- .description("Create a Sandbox snapshot")
1654
- .option("--name <name>", "Human-readable snapshot name/comment")
1655
- .option("--comment <comment>", "Snapshot comment")
1656
- .option("--stop", "Pause the sandbox after snapshot creation")
1657
- .option("--expiration <duration>", "Requested retention duration, e.g. 14d")
1658
- .option("--json", "Output as JSON")
1659
- .action((id, opts) => runAction(async () => {
1660
- const body = {};
1661
- const comment = opts.comment ?? opts.name;
1662
- if (comment)
1663
- body["comment"] = comment;
1664
- if (opts.expiration)
1665
- body["expiration"] = opts.expiration;
1666
- const snapshot = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/snapshots`), body));
1667
- if (opts.stop) {
1668
- await client().apiPost(apiPath(`/sandboxes/${enc(id)}/pause`), {});
1669
- }
1670
- if (isJsonMode(opts)) {
1671
- console.log(JSON.stringify({ snapshot, stopped: !!opts.stop }, null, 2));
1672
- return;
1673
- }
1674
- const snap = (snapshot ?? {});
1675
- console.log(chalk.green("Snapshot requested"));
1676
- if (snap["id"])
1677
- console.log(String(snap["id"]));
1678
- if (opts.stop)
1679
- console.log(chalk.dim("Sandbox pause requested."));
1680
- }));
1681
- const snapshots = sandbox
1682
- .command("snapshots")
1683
- .description("List, inspect, and delete Sandbox snapshots");
1684
- snapshots
1685
- .command("list <sandbox-id>")
1686
- .description("List snapshots for a sandbox")
1687
- .option("--json", "Output as JSON")
1688
- .action((id, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}/snapshots`, opts)));
1689
- snapshots
1690
- .command("get <sandbox-id> <snapshot-id>")
1691
- .description("Get a sandbox snapshot")
1692
- .option("--json", "Output as JSON")
1693
- .action((id, snapshotId, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}/snapshots/${enc(snapshotId)}`, opts)));
1694
- snapshots
1695
- .command("delete <sandbox-id> <snapshot-id>")
1696
- .alias("rm")
1697
- .description("Delete a sandbox snapshot")
1698
- .option("--json", "Output as JSON")
1699
- .action((id, snapshotId, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}/snapshots/${enc(snapshotId)}`, opts)));
1700
- // delete-checkpoint / delete-snapshot
1701
- sandbox
1702
- .command("delete-checkpoint <sandbox-id> <checkpoint-id>")
1703
- .alias("delete-snapshot")
1704
- .description("Delete a Sandbox Checkpoint")
1705
- .option("--json", "Output as JSON")
1706
- .action((id, sid, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}/snapshots/${enc(sid)}`, opts)));
1707
- // -------------------------------------------------------------------------
1708
- // sandbox-templates — top-level command group for template management
1709
- // -------------------------------------------------------------------------
1710
- const templates = program
1711
- .command("sandbox-templates")
1712
- .description("Manage Sandbox templates");
1713
- templates
1714
- .command("list")
1715
- .description("List all available Sandbox templates")
1716
- .option("--include-aliases", "Include template aliases in the response")
1717
- .option("--json", "Output as JSON")
1718
- .action((opts) => runAction(async () => {
1719
- const qs = opts.includeAliases ? "?include_aliases=true" : "";
1720
- await getAndPrint(`/sandbox-templates${qs}`, opts);
1721
- }));
1722
- templates
1723
- .command("get <template-id>")
1724
- .description("Show a Sandbox template by ID or slug")
1725
- .option("--json", "Output as JSON")
1726
- .action((id, opts) => runAction(() => getAndPrint(`/sandbox-templates/${enc(id)}`, opts)));
1727
- }
1728
- function registerSandboxConnectorCommands(sandbox) {
1729
- const connectors = sandbox
1730
- .command("connectors")
1731
- .alias("providers")
1732
- .description("Manage brokered provider connector bindings for a Sandbox");
1733
- connectors
1734
- .command("list <sandbox-id>")
1735
- .description("List provider connectors bound to a Sandbox")
1736
- .option("--json", "Output as JSON")
1737
- .action((id, opts) => runAction(async () => {
1738
- const raw = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}/connectors`)));
1739
- printValue(raw, opts);
1740
- }));
1741
- connectors
1742
- .command("attach <sandbox-id> <connector>")
1743
- .description("Attach a connector to a Sandbox as brokered env")
1744
- .requiredOption("--env <name>", "Environment variable name, e.g. ANTHROPIC_API_KEY")
1745
- .option("--mode <mode>", "brokered-env or plain-env", "brokered-env")
1746
- .option("--installation-id <id>", "Provider installation ID")
1747
- .option("--json", "Output as JSON")
1748
- .action((id, connector, opts) => runAction(async () => {
1749
- const body = {
1750
- connector,
1751
- env_name: opts.env,
1752
- mode: normalizeConnectorMode(opts.mode ?? "brokered-env"),
1753
- installation_id: opts.installationId,
1754
- };
1755
- const raw = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/connectors`), body));
1756
- printValue(raw, opts);
1757
- }));
1758
- connectors
1759
- .command("detach <sandbox-id> <binding-id-or-connector>")
1760
- .description("Detach a connector binding from a Sandbox")
1761
- .option("--json", "Output as JSON")
1762
- .action((id, binding, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}/connectors/${enc(binding)}`, opts)));
1763
- connectors
1764
- .command("sync <sandbox-id>")
1765
- .description("Sync connector placeholder env vars into a running Sandbox")
1766
- .option("--json", "Output as JSON")
1767
- .action((id, opts) => runAction(async () => {
1768
- const raw = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/connectors/sync`), {}));
1769
- printValue(raw, opts);
1770
- }));
1771
- }
1772
- function normalizeConnectorMode(value) {
1773
- return value.replaceAll("_", "-");
1774
- }
1775
- // ── sandbox ssh implementation ─────────────────────────────────────────────
1776
- //
1777
- // Protocol:
1778
- // 1. Ensure the user has an SSH keypair at ~/.ssh/miosa_sandbox_ed25519.
1779
- // If not, generate one and register it via POST /sandboxes/:id/ssh-keys.
1780
- // 2. Open a local TCP server on a random (or chosen) port.
1781
- // 3. Each accepted connection is bridged bidirectionally to the platform WS
1782
- // endpoint wss://<api>/api/v1/sandboxes/:id/ssh-tunnel.
1783
- // 4. Spawn `ssh` pointing at localhost:<port> with the key and user.
1784
- // 5. On ssh exit, close the TCP server and exit.
1785
- const SANDBOX_KEY_PATH = path.join(os.homedir(), ".ssh", "miosa_sandbox_ed25519");
1786
- async function ensureSandboxSshKey(id, config) {
1787
- if (!fs.existsSync(SANDBOX_KEY_PATH)) {
1788
- console.log(chalk.dim("Generating SSH keypair for MIOSA sandbox access..."));
1789
- fs.mkdirSync(path.join(os.homedir(), ".ssh"), { recursive: true });
1790
- await new Promise((resolve, reject) => {
1791
- const child = spawn("ssh-keygen", [
1792
- "-t",
1793
- "ed25519",
1794
- "-f",
1795
- SANDBOX_KEY_PATH,
1796
- "-N",
1797
- "",
1798
- "-C",
1799
- "miosa-sandbox",
1800
- ], { stdio: "pipe" });
1801
- child.on("close", (code) => {
1802
- if (code === 0)
1803
- resolve();
1804
- else
1805
- reject(new Error(`ssh-keygen exited with code ${code}`));
1806
- });
1807
- child.on("error", reject);
1808
- });
1809
- }
1810
- // Register the public key with every target sandbox. The local key can
1811
- // already exist from a previous sandbox, but a fresh sandbox still needs it
1812
- // installed in authorized_keys before SSH auth can succeed.
1813
- const pubKey = fs.readFileSync(`${SANDBOX_KEY_PATH}.pub`, "utf8").trim();
1814
- const endpoint = config.endpoint.replace(/\/$/, "");
1815
- const response = await fetch(`${endpoint}${apiPath(`/sandboxes/${encodeURIComponent(id)}/ssh-keys`)}`, {
1816
- method: "POST",
1817
- headers: {
1818
- ...sandboxTransportHeaders(config),
1819
- "Content-Type": "application/json",
1820
- },
1821
- body: JSON.stringify({ public_key: pubKey }),
1822
- });
1823
- if (!response.ok) {
1824
- const rawBody = await response.text();
1825
- let body = {};
1826
- try {
1827
- body = JSON.parse(rawBody);
1828
- }
1829
- catch {
1830
- body = { message: rawBody || `HTTP ${response.status}` };
1831
- }
1832
- throw mapHttpError(response.status, body, rawBody, response.headers.get("x-request-id"));
1833
- }
1834
- console.log(chalk.green("SSH key registered."));
1835
- }
1836
- function sandboxTransportHeaders(config) {
1837
- if (!config.api_key)
1838
- throw new Error("Not authenticated. Run: miosa login");
1839
- const headers = {
1840
- Authorization: `Bearer ${String(config.api_key)}`,
1841
- };
1842
- if (config.tenant)
1843
- headers["X-MIOSA-Tenant"] = config.tenant;
1844
- if (config.workspace)
1845
- headers["X-MIOSA-Workspace"] = config.workspace;
1846
- return headers;
1847
- }
1848
- export function buildSandboxWebSocketRequest(config, id) {
1849
- const base = config.endpoint.replace(/\/$/, "");
1850
- const wsBase = base.replace(/^https?/, (protocol) => protocol === "https" ? "wss" : "ws");
1851
- const url = new URL(`${wsBase}/api/v1/sandboxes/${encodeURIComponent(id)}/ssh-tunnel`);
1852
- const headers = sandboxTransportHeaders(config);
1853
- if (config.tenant) {
1854
- url.searchParams.set("tenant", config.tenant);
1855
- }
1856
- if (config.workspace) {
1857
- url.searchParams.set("workspace", config.workspace);
1858
- }
1859
- return { url: url.toString(), headers };
1860
- }
1861
- function bridgeSandboxWs(socket, request) {
1862
- let closed = false;
1863
- function cleanup() {
1864
- if (closed)
1865
- return;
1866
- closed = true;
1867
- if (!socket.destroyed)
1868
- socket.destroy();
1869
- }
1870
- const ws = new WebSocket(request.url, {
1871
- headers: request.headers,
1872
- });
1873
- ws.on("open", () => {
1874
- socket.on("data", (chunk) => {
1875
- if (ws.readyState === WebSocket.OPEN)
1876
- ws.send(chunk);
1877
- });
1878
- socket.on("close", () => {
1879
- if (ws.readyState === WebSocket.OPEN)
1880
- ws.close();
1881
- });
1882
- socket.on("error", () => ws.close());
1883
- });
1884
- ws.on("message", (data) => {
1885
- if (!socket.destroyed) {
1886
- socket.write(typeof data === "string" ? Buffer.from(data) : data);
1887
- }
1888
- });
1889
- ws.on("close", cleanup);
1890
- ws.on("error", (err) => {
1891
- process.stderr.write(`\r\nWS error for sandbox tunnel: ${err.message}\r\n`);
1892
- cleanup();
1893
- });
1894
- }
1895
- async function runSandboxSsh(id, opts) {
1896
- const config = loadConfig();
1897
- const wsRequest = buildSandboxWebSocketRequest(config, id);
1898
- await ensureSandboxSshKey(id, config);
1899
- // Pick a free local port
1900
- const localPort = opts.port ?? (await pickFreePort());
1901
- const user = opts.user ?? "root";
1902
- if (isJsonMode(opts)) {
1903
- console.log(JSON.stringify({
1904
- sandbox_id: id,
1905
- local_port: localPort,
1906
- user,
1907
- ws_url: wsRequest.url,
1908
- key_path: SANDBOX_KEY_PATH,
1909
- }));
1910
- return;
1911
- }
1912
- const server = createServer((socket) => {
1913
- bridgeSandboxWs(socket, wsRequest);
1914
- });
1915
- await new Promise((resolve, reject) => {
1916
- server.on("error", reject);
1917
- server.listen(localPort, "127.0.0.1", resolve);
1918
- });
1919
- console.log(chalk.green("Tunnel ready.") +
1920
- chalk.dim(` 127.0.0.1:${localPort} → sandbox ${id}`));
1921
- if (opts.spawn === false) {
1922
- console.log(chalk.dim(`Connect manually: ssh -i ${SANDBOX_KEY_PATH} -p ${localPort} ${user}@127.0.0.1`));
1923
- console.log(chalk.dim("Press Ctrl+C to close."));
1924
- await waitForSignal();
1925
- server.close();
1926
- return;
1927
- }
1928
- const sshArgs = [
1929
- "-i",
1930
- SANDBOX_KEY_PATH,
1931
- "-p",
1932
- String(localPort),
1933
- "-o",
1934
- "StrictHostKeyChecking=no",
1935
- "-o",
1936
- "UserKnownHostsFile=/dev/null",
1937
- "-o",
1938
- "LogLevel=ERROR",
1939
- `${user}@127.0.0.1`,
1940
- ];
1941
- const sshProc = spawn("ssh", sshArgs, { stdio: "inherit" });
1942
- const exitCode = await new Promise((resolve) => {
1943
- sshProc.on("close", (code) => resolve(code ?? 1));
1944
- sshProc.on("error", (err) => {
1945
- console.error(chalk.red(`Failed to spawn ssh: ${err.message}`));
1946
- resolve(1);
1947
- });
1948
- });
1949
- server.close();
1950
- process.exit(exitCode);
1951
- }
1952
- async function runSandboxPortForward(id, opts) {
1953
- const preview = await previewSandbox(id, opts.remotePort, {
1954
- wait: true,
1955
- timeout: opts.timeoutSec,
1956
- probePath: opts.probePath,
1957
- });
1958
- const upstreamBase = new URL(preview.url);
1959
- const stats = { active: 0, total: 0, bytesIn: 0, bytesOut: 0 };
1960
- const server = http.createServer((req, res) => {
1961
- stats.active++;
1962
- stats.total++;
1963
- const target = new URL(req.url ?? "/", upstreamBase);
1964
- const headers = { ...req.headers, host: target.host };
1965
- const mod = target.protocol === "https:" ? https : http;
1966
- const proxyReq = mod.request(target, { method: req.method, headers }, (proxyRes) => {
1967
- res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
1968
- proxyRes.on("data", (chunk) => {
1969
- stats.bytesOut += chunk.length;
1970
- });
1971
- proxyRes.pipe(res);
1972
- });
1973
- req.on("data", (chunk) => {
1974
- stats.bytesIn += chunk.length;
1975
- });
1976
- proxyReq.on("error", (err) => {
1977
- if (!res.headersSent)
1978
- res.writeHead(502, { "content-type": "text/plain" });
1979
- res.end(`MIOSA port-forward upstream error: ${err.message}`);
1980
- });
1981
- res.on("close", () => {
1982
- stats.active = Math.max(0, stats.active - 1);
1983
- });
1984
- req.pipe(proxyReq);
1985
- });
1986
- await new Promise((resolve, reject) => {
1987
- server.on("error", (err) => {
1988
- if (err.code === "EADDRINUSE") {
1989
- reject(new Error(`Port ${opts.localPort} is already in use. Choose another with --local.`));
1990
- }
1991
- else {
1992
- reject(err);
1993
- }
1994
- });
1995
- server.listen(opts.localPort, "127.0.0.1", resolve);
1996
- });
1997
- let localProbe = null;
1998
- if (opts.wait) {
1999
- localProbe = await waitForLocalHttp(opts.localPort, opts.probePath, opts.timeoutSec);
2000
- }
2001
- if (isJsonMode(opts)) {
2002
- console.log(JSON.stringify({
2003
- sandbox_id: id,
2004
- remote_port: opts.remotePort,
2005
- local_port: opts.localPort,
2006
- upstream_url: preview.url,
2007
- ready: localProbe?.ok ?? !opts.wait,
2008
- status: localProbe?.status ?? null,
2009
- latency_ms: localProbe?.latency_ms ?? null,
2010
- }));
2011
- }
2012
- else {
2013
- console.log(`${chalk.green(opts.wait ? "Forwarding ready" : "Forwarding")} ${chalk.cyan(`localhost:${opts.localPort}`)} ${chalk.dim("→")} sandbox ${chalk.cyan(id)}:${chalk.bold(String(opts.remotePort))}`);
2014
- console.log(chalk.dim(`Upstream: ${preview.url}`));
2015
- console.log(chalk.dim("Press Ctrl+C to close.\n"));
2016
- }
2017
- const ticker = setInterval(() => {
2018
- if (isJsonMode(opts) || stats.active === 0)
2019
- return;
2020
- process.stderr.write(chalk.dim(`\r[${new Date().toLocaleTimeString()}] connections: ${stats.active} ↑ ${formatBytes(stats.bytesIn)} ↓ ${formatBytes(stats.bytesOut)} `));
2021
- }, 5_000);
2022
- await waitForSignal();
2023
- clearInterval(ticker);
2024
- process.stderr.write("\n");
2025
- server.close();
2026
- }
2027
- class SandboxDeployPartialError extends Error {
2028
- causeError;
2029
- sandboxId;
2030
- recoveryCommand;
2031
- constructor(causeError, sandboxId, recoveryCommand) {
2032
- super(causeError instanceof Error ? causeError.message : String(causeError));
2033
- this.causeError = causeError;
2034
- this.sandboxId = sandboxId;
2035
- this.recoveryCommand = recoveryCommand;
2036
- this.name = "SandboxDeployPartialError";
2037
- }
2038
- }
2039
- async function showSandboxWithPreview(sandboxId, port, probePath) {
2040
- const c = client();
2041
- const sandbox = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
2042
- let preview = null;
2043
- try {
2044
- preview = await previewSandbox(sandboxId, port, {
2045
- wait: false,
2046
- timeout: 1,
2047
- probePath,
2048
- });
2049
- }
2050
- catch (err) {
2051
- preview = {
2052
- url: "",
2053
- ready: false,
2054
- status: null,
2055
- latency_ms: null,
2056
- error: err instanceof Error ? err.message : String(err),
2057
- };
2058
- }
2059
- return {
2060
- ...sandbox,
2061
- ready: preview?.ready ?? false,
2062
- preview: {
2063
- url: preview?.url || null,
2064
- port,
2065
- route_ready: Boolean(preview?.url),
2066
- tls_ready: preview?.ready ?? false,
2067
- last_status: preview?.status ?? null,
2068
- latency_ms: preview?.latency_ms ?? null,
2069
- error: preview?.error,
2070
- },
2071
- };
2072
- }
2073
- async function previewSandbox(sandboxId, port, opts) {
2074
- const c = client();
2075
- const exposed = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), {
2076
- port,
2077
- title: "app preview",
2078
- }));
2079
- const url = extractUrl(exposed);
2080
- if (!url)
2081
- throw new UserError("Sandbox expose did not return a preview URL.");
2082
- const edge = opts.wait
2083
- ? await waitForPublicPreview(url, opts.probePath, opts.timeout)
2084
- : await probePublicPreview(url, opts.probePath);
2085
- return {
2086
- url,
2087
- ready: edge.ok,
2088
- status: edge.status,
2089
- latency_ms: edge.latency_ms ?? null,
2090
- error: edge.error,
2091
- };
2092
- }
2093
- async function waitSandboxReady(sandboxId, port, probePath, timeoutSec) {
2094
- const c = client();
2095
- await waitForSandboxRunning(c, sandboxId, Math.min(timeoutSec, 120));
2096
- const internal = await waitForInternalHttp(c, sandboxId, port, probePath, Math.min(timeoutSec, 60));
2097
- const preview = await previewSandbox(sandboxId, port, {
2098
- wait: true,
2099
- timeout: timeoutSec,
2100
- probePath,
2101
- });
2102
- return {
2103
- sandbox_id: sandboxId,
2104
- port,
2105
- internal_status: internal.status,
2106
- ...preview,
2107
- };
2108
- }
2109
- async function waitSandboxVmReady(sandboxId, timeoutSec) {
2110
- // `sandbox wait` reports readiness for use — confirm command-readiness so a
2111
- // successful wait guarantees exec works.
2112
- const sandbox = await waitForSandboxRunning(client(), sandboxId, timeoutSec, true);
2113
- return {
2114
- ...sandbox,
2115
- sandbox_id: sandboxId,
2116
- ready: true,
2117
- url: null,
2118
- };
2119
- }
2120
- async function startSandboxService(sandboxId, name, opts) {
2121
- validateServiceName(name);
2122
- const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/services`), {
2123
- name,
2124
- command: opts.cmd,
2125
- cwd: opts.cwd,
2126
- }));
2127
- let previewUrl = null;
2128
- if (opts.port != null) {
2129
- const preview = await previewSandbox(sandboxId, opts.port, {
2130
- wait: false,
2131
- timeout: 1,
2132
- probePath: "/",
2133
- });
2134
- previewUrl = preview.url;
2135
- }
2136
- return {
2137
- ...result,
2138
- port: opts.port ?? null,
2139
- preview_url: previewUrl,
2140
- };
2141
- }
2142
- async function createSandboxCommand(sandboxId, command, opts) {
2143
- if (!command.trim())
2144
- throw new UserError("Command is required.");
2145
- const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/commands`), {
2146
- command,
2147
- cwd: opts.cwd,
2148
- env: opts.env,
2149
- user: opts.user,
2150
- sudo: opts.sudo,
2151
- tty: opts.tty,
2152
- interactive: opts.interactive,
2153
- timeout: opts.timeout,
2154
- }));
2155
- return {
2156
- ...result,
2157
- command_id: result["id"],
2158
- status: result["status"] ?? "running",
2159
- };
2160
- }
2161
- function warnOnTemplatePortMismatch(template, port, opts) {
2162
- if (isJsonMode(opts))
2163
- return;
2164
- if ((template === "nextjs" || template === "next-js") && port !== 3000) {
2165
- console.error(chalk.yellow("Next.js sandbox templates default to port 3000. Use --port 3000 unless you intentionally changed the app readiness port."));
2166
- }
2167
- }
2168
- function serviceLogPath(name) {
2169
- return `/tmp/miosa-services/${name}.log`;
2170
- }
2171
- function servicePidPath(name) {
2172
- return `/tmp/miosa-services/${name}.pid`;
2173
- }
2174
- function validateServiceName(name) {
2175
- if (!/^[A-Za-z0-9_.-]+$/.test(name)) {
2176
- throw new UserError(`Invalid service name: ${name}`, "Use only letters, numbers, dot, dash, and underscore.");
2177
- }
2178
- }
2179
- export async function deploySandbox(localDir, opts) {
2180
- const sourceDir = path.resolve(localDir);
2181
- const sourceBacked = !!opts.source;
2182
- if (!sourceBacked &&
2183
- (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory())) {
2184
- throw new UserError(`Local directory not found: ${sourceDir}`);
2185
- }
2186
- const c = client();
2187
- let appManifest = sourceBacked
2188
- ? null
2189
- : (loadAppManifest(sourceDir)?.manifest ?? null);
2190
- const detection = sourceBacked ? null : detectFramework(sourceDir);
2191
- const port = opts.port ??
2192
- opts.publishPort ??
2193
- manifestPort(appManifest) ??
2194
- detection?.port ??
2195
- 5173;
2196
- warnOnTemplatePortMismatch(opts.template ?? appManifest?.template ?? null, port, opts);
2197
- const probePath = opts.probePath ?? manifestProbePath(appManifest) ?? "/";
2198
- let remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
2199
- const start = opts.start ??
2200
- manifestStartCommand(appManifest) ??
2201
- defaultStartCommand(detection?.framework ?? appManifest?.framework, port);
2202
- let sandboxId = opts.sandbox ?? null;
2203
- try {
2204
- if (!sandboxId) {
2205
- deployStep(opts, "Creating sandbox");
2206
- sandboxId = await createSandboxForDeploy(c, opts.template ?? appManifest?.template ?? "miosa-sandbox", opts.name, {
2207
- source: opts.source,
2208
- revision: opts.revision,
2209
- depth: opts.depth,
2210
- });
2211
- }
2212
- deployStep(opts, "Waiting for sandbox");
2213
- await waitForSandboxRunning(c, sandboxId, Math.min(opts.timeout, 120));
2214
- if (sourceBacked) {
2215
- deployStep(opts, "Waiting for source import");
2216
- appManifest = await readRemoteAppManifest(c, sandboxId, opts.timeout);
2217
- remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
2218
- }
2219
- else {
2220
- deployStep(opts, "Uploading files");
2221
- const archivePath = createDeployArchive(sourceDir);
2222
- const remoteArchive = `/tmp/miosa-deploy-${Date.now()}.tgz`;
2223
- try {
2224
- await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
2225
- deployStep(opts, "Extracting workspace");
2226
- await execSandbox(c, sandboxId, `mkdir -p ${shellQuote(remoteWorkdir)} && tar -xzf ${shellQuote(remoteArchive)} -C ${shellQuote(remoteWorkdir)}`, "/");
2227
- }
2228
- finally {
2229
- fs.rmSync(archivePath, { force: true });
2230
- }
2231
- }
2232
- const resolvedPort = opts.port ?? opts.publishPort ?? manifestPort(appManifest) ?? port;
2233
- const resolvedProbePath = opts.probePath ?? manifestProbePath(appManifest) ?? probePath;
2234
- const resolvedStart = opts.start ?? manifestStartCommand(appManifest) ?? start;
2235
- const installCommand = opts.install === false
2236
- ? null
2237
- : (opts.installCommand ??
2238
- (appManifest?.install === false ? null : appManifest?.install) ??
2239
- (sourceBacked ? "npm install" : defaultInstallCommand(sourceDir)));
2240
- if (installCommand) {
2241
- deployStep(opts, `Installing dependencies: ${installCommand}`);
2242
- await execSandbox(c, sandboxId, installCommand, remoteWorkdir, opts.timeout);
2243
- }
2244
- deployStep(opts, `Starting app on port ${resolvedPort}`);
2245
- await execSandbox(c, sandboxId, `fuser -k ${resolvedPort}/tcp >/dev/null 2>&1 || true; nohup sh -lc ${shellQuote(resolvedStart)} > ${shellQuote(`/tmp/miosa-app-${resolvedPort}.log`)} 2>&1 & echo $!`, remoteWorkdir);
2246
- deployStep(opts, "Checking internal app readiness");
2247
- const internal = await waitForInternalHttp(c, sandboxId, resolvedPort, resolvedProbePath, Math.min(opts.timeout, 60));
2248
- deployStep(opts, "Creating public preview route");
2249
- const exposed = await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), { port: resolvedPort, title: "app preview" });
2250
- const previewUrl = extractUrl(unwrap(exposed));
2251
- if (!previewUrl) {
2252
- throw new UserError("Sandbox expose did not return a preview URL.");
2253
- }
2254
- if (opts.wait)
2255
- deployStep(opts, "Checking public preview readiness");
2256
- const edge = opts.wait
2257
- ? await waitForPublicPreview(previewUrl, resolvedProbePath, opts.timeout)
2258
- : { ok: false, status: null };
2259
- return {
2260
- sandbox_id: sandboxId,
2261
- port: resolvedPort,
2262
- preview_url: previewUrl,
2263
- preview_ready: edge.ok,
2264
- internal_status: internal.status,
2265
- edge_status: edge.status,
2266
- latency_ms: edge.latency_ms ?? null,
2267
- };
2268
- }
2269
- catch (err) {
2270
- if (sandboxId) {
2271
- throw new SandboxDeployPartialError(err, sandboxId, recoveryCommandForSandboxDeploy(sandboxId, opts, localDir));
2272
- }
2273
- throw err;
2274
- }
2275
- }
2276
- const NON_TERMINAL_DEPLOY_STATES = new Set([
2277
- "building",
2278
- "pending",
2279
- "queued",
2280
- "deploying",
2281
- ]);
2282
- // Bug 9: look for an existing non-terminal deployment with a matching name so
2283
- // retries attach a new release instead of creating a duplicate app. Defensive:
2284
- // returns null (fall back to create) if the list call fails or finds nothing.
2285
- async function findExistingDeploymentByName(c, name) {
2286
- try {
2287
- const raw = unwrap(await c.apiGet(apiPath("/deployments")));
2288
- const items = Array.isArray(raw)
2289
- ? raw
2290
- : Array.isArray((asRecord(raw) ?? {})["data"])
2291
- ? (asRecord(raw) ?? {})["data"]
2292
- : [];
2293
- for (const item of items) {
2294
- const rec = asRecord(item);
2295
- if (!rec)
2296
- continue;
2297
- if (stringField(rec, "name") !== name)
2298
- continue;
2299
- const state = (stringField(rec, "state") ?? "").toLowerCase();
2300
- const id = stringField(rec, "id");
2301
- if (id && NON_TERMINAL_DEPLOY_STATES.has(state)) {
2302
- return { id, state };
2303
- }
2304
- }
2305
- return null;
2306
- }
2307
- catch {
2308
- return null;
2309
- }
2310
- }
2311
- async function publishSandbox(sandboxId, opts) {
2312
- const c = client();
2313
- const body = {
2314
- output_path: opts.path,
2315
- path: opts.path,
2316
- environment: opts.environment,
2317
- metadata: opts.dockerDeploy
2318
- ? { environment: opts.environment, deployment_product: "docker_deploy" }
2319
- : { environment: opts.environment },
2320
- };
2321
- if (opts.dockerDeploy)
2322
- body["deployment_type"] = "docker-deploy";
2323
- if (opts.app)
2324
- body["deployment_id"] = opts.app;
2325
- // Bug 9: avoid creating a duplicate deployment on retry. When publishing by
2326
- // name (no explicit app id), reuse an existing non-terminal deployment with
2327
- // the same name by attaching a new release to it.
2328
- if (opts.name && !opts.app) {
2329
- const existing = await findExistingDeploymentByName(c, opts.name);
2330
- if (existing) {
2331
- body["deployment_id"] = existing.id;
2332
- deployStep(opts, chalk.dim(`Attaching to existing deployment ${existing.id} (${existing.state})`));
2333
- }
2334
- }
2335
- if (opts.name)
2336
- body["name"] = opts.name;
2337
- if (opts.slug)
2338
- body["slug"] = opts.slug;
2339
- if (opts.buildCommand)
2340
- body["build_command"] = opts.buildCommand;
2341
- if (opts.runCommand)
2342
- body["run_command"] = opts.runCommand;
2343
- if (opts.domain)
2344
- body["domain"] = opts.domain;
2345
- if (opts.port != null)
2346
- body["port"] = opts.port;
2347
- const deploymentType = opts.dockerDeploy
2348
- ? "docker-deploy"
2349
- : opts.deploymentType;
2350
- if (deploymentType)
2351
- body["deployment_type"] = deploymentType;
2352
- const database = parsePublishDatabase(opts.database);
2353
- if (database !== undefined)
2354
- body["database"] = database;
2355
- deployStep(opts, "Publishing sandbox workspace to durable deployment");
2356
- const raw = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/publish`), body));
2357
- const response = asRecord(raw) ?? {};
2358
- const data = asRecord(response["data"]) ?? response;
2359
- const deployment = asRecord(data["deployment"]);
2360
- const version = asRecord(data["version"]);
2361
- const release = asRecord(data["release"]);
2362
- const deploymentId = stringField(response, "deployment_id") ??
2363
- stringField(deployment, "id") ??
2364
- null;
2365
- const versionId = stringField(response, "version_id") ?? stringField(version, "id") ?? null;
2366
- const releaseId = stringField(response, "release_id") ?? stringField(release, "id") ?? null;
2367
- let state = stringField(response, "state") ?? stringField(deployment, "state") ?? null;
2368
- let url = stringField(response, "url") ??
2369
- extractUrl(deployment) ??
2370
- stringField(data, "url") ??
2371
- null;
2372
- let deploymentProduct = stringField(response, "deployment_product") ??
2373
- stringField(data, "deployment_product") ??
2374
- stringField(deployment, "deployment_product") ??
2375
- stringField(asRecord(deployment?.["metadata"]), "deployment_product") ??
2376
- null;
2377
- let dockerDeployHostId = stringField(response, "docker_deploy_host_id") ??
2378
- stringField(data, "docker_deploy_host_id") ??
2379
- stringField(deployment, "docker_deploy_host_id") ??
2380
- stringField(asRecord(deployment?.["metadata"]), "docker_deploy_host_id") ??
2381
- null;
2382
- if (opts.wait && deploymentId) {
2383
- deployStep(opts, "Waiting for durable deployment");
2384
- const waited = await waitForDeploymentReady(c, deploymentId, opts.timeout);
2385
- state = stringField(waited, "state") ?? state;
2386
- url = extractUrl(waited) ?? url;
2387
- deploymentProduct =
2388
- stringField(waited, "deployment_product") ??
2389
- stringField(asRecord(waited["metadata"]), "deployment_product") ??
2390
- deploymentProduct;
2391
- dockerDeployHostId =
2392
- stringField(waited, "docker_deploy_host_id") ??
2393
- stringField(asRecord(waited["metadata"]), "docker_deploy_host_id") ??
2394
- dockerDeployHostId;
2395
- response["state"] = state;
2396
- if (url)
2397
- response["url"] = url;
2398
- data["deployment"] = waited;
2399
- data["promotion_pending"] = false;
2400
- data["app_consistency_pending"] = false;
2401
- if (url)
2402
- data["url"] = url;
2403
- }
2404
- let probe = null;
2405
- if (opts.wait && url) {
2406
- deployStep(opts, "Checking production URL readiness");
2407
- probe = await waitForPublicPreview(url, "/", opts.timeout);
2408
- }
2409
- const ready = (state === null || ["running", "active"].includes(state.toLowerCase())) &&
2410
- (opts.wait ? probe?.ok !== false : true);
2411
- return {
2412
- type: "deployment",
2413
- sandbox_id: sandboxId,
2414
- deployment_id: deploymentId,
2415
- app_id: deploymentId,
2416
- release_id: releaseId,
2417
- version_id: versionId,
2418
- url,
2419
- state,
2420
- deployment_product: deploymentProduct,
2421
- docker_deploy_host_id: dockerDeployHostId,
2422
- ready,
2423
- probe,
2424
- data: raw,
2425
- };
2426
- }
2427
- async function waitForDeploymentReady(c, deploymentId, timeoutSec) {
2428
- const deadline = Date.now() + timeoutSec * 1000;
2429
- let last = null;
2430
- while (Date.now() < deadline) {
2431
- const value = unwrap(await c.apiGet(apiPath(`/deployments/${enc(deploymentId)}`)));
2432
- const deployment = asRecord(value) ?? {};
2433
- last = deployment;
2434
- const state = String(deployment["state"] ?? "").toLowerCase();
2435
- if (state === "running" || state === "active")
2436
- return deployment;
2437
- if (state === "failed" || state === "error") {
2438
- throw new UserError(`Deployment ${deploymentId} entered ${state} state.`, String(deployment["error_message"] ?? deployment["error"] ?? ""));
2439
- }
2440
- await sleep(2000);
2441
- }
2442
- const lastState = last ? String(last["state"] ?? "unknown") : "unknown";
2443
- throw new UserError(`Deployment still building after ${timeoutSec}s — it may still finish. Re-check with \`miosa sandbox show ${deploymentId}\` or \`miosa deploy logs\`.`, `Last state: ${lastState}`);
2444
- }
2445
- function parsePublishDatabase(value) {
2446
- if (!value)
2447
- return undefined;
2448
- const normalized = value.toLowerCase();
2449
- if (normalized === "none")
2450
- return false;
2451
- if (normalized === "postgres" ||
2452
- normalized === "postgresql" ||
2453
- normalized === "create:postgres" ||
2454
- normalized === "create:postgresql") {
2455
- return { engine: "postgresql", engine_version: "16" };
2456
- }
2457
- if (normalized.startsWith("existing:")) {
2458
- return { existing_database_id: value.slice("existing:".length) };
2459
- }
2460
- return value;
2461
- }
2462
- function deployStep(opts, label) {
2463
- if (!isJsonMode(opts))
2464
- console.error(chalk.dim(`→ ${label}`));
2465
- }
2466
- async function doctorSandbox(sandboxId, port, probePath) {
2467
- const c = client();
2468
- const sandbox = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
2469
- const internal = await probeInternalHttp(c, sandboxId, port, probePath);
2470
- let exposeData = {};
2471
- try {
2472
- exposeData = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), {
2473
- port,
2474
- title: "doctor probe",
2475
- }));
2476
- }
2477
- catch (err) {
2478
- exposeData = { error: err instanceof Error ? err.message : String(err) };
2479
- }
2480
- const previewUrl = extractUrl(exposeData);
2481
- const edge = previewUrl
2482
- ? await probePublicPreview(previewUrl, probePath)
2483
- : { ok: false, status: null, error: "No preview URL returned" };
2484
- return {
2485
- sandbox_id: sandboxId,
2486
- sandbox_state: sandbox["state"] ?? sandbox["status"] ?? "unknown",
2487
- process: internal.ok ? "listening" : "not_ready",
2488
- route: previewUrl ? "created" : "missing",
2489
- tls: edge.error && /tls|certificate|ssl/i.test(edge.error)
2490
- ? "not_ready"
2491
- : previewUrl
2492
- ? "checked"
2493
- : "unknown",
2494
- internal_probe: internal,
2495
- edge_probe: edge,
2496
- preview_ready: edge.ok,
2497
- preview_url: previewUrl,
2498
- expose: exposeData,
2499
- };
2500
- }
2501
- function renderDoctorReport(report) {
2502
- console.log();
2503
- console.log(chalk.bold("Sandbox Doctor"));
2504
- console.log();
2505
- console.log(` ${chalk.bold("Sandbox")} ${report["sandbox_id"]}`);
2506
- console.log(` ${chalk.bold("State")} ${report["sandbox_state"]}`);
2507
- console.log(` ${chalk.bold("Process")} ${report["process"]}`);
2508
- console.log(` ${chalk.bold("Route")} ${report["route"]}`);
2509
- console.log(` ${chalk.bold("TLS/edge")} ${report["tls"]}`);
2510
- console.log(` ${chalk.bold("Preview ready")} ${report["preview_ready"] ? chalk.green("yes") : chalk.red("no")}`);
2511
- if (report["preview_url"]) {
2512
- console.log(` ${chalk.bold("Preview URL")} ${chalk.cyan(String(report["preview_url"]))}`);
2513
- }
2514
- console.log();
2515
- if (!report["preview_ready"]) {
2516
- console.log(chalk.yellow(" Preview is not externally ready yet. Internal app health and edge probe details are in --json output."));
2517
- console.log();
2518
- }
2519
- }
2520
- async function recoverSandboxDeploy(idOrName, opts) {
2521
- const c = client();
2522
- const resolved = await resolveSandboxForRecovery(c, idOrName);
2523
- if (!resolved.sandbox) {
2524
- return {
2525
- query: idOrName,
2526
- sandbox: null,
2527
- sandbox_id: null,
2528
- matched_by: null,
2529
- exec_ok: false,
2530
- app_port: opts.port ?? null,
2531
- preview_ready: null,
2532
- preview_url: null,
2533
- doctor: null,
2534
- recommendations: [
2535
- "No matching sandbox was found. Run `miosa sandbox list --json` and retry with a sandbox ID.",
2536
- ],
2537
- commands: {
2538
- list: "miosa sandbox list --json",
2539
- },
2540
- };
2541
- }
2542
- const sandbox = resolved.sandbox;
2543
- const sandboxId = str(sandbox["id"]);
2544
- const name = stringOrNull(sandbox["name"]);
2545
- const template = stringOrNull(sandbox["template_id"]);
2546
- const port = opts.port ?? suggestedRecoveryPort(template);
2547
- const execOk = await checkSandboxExec(c, sandboxId);
2548
- const doctor = port != null
2549
- ? await safeDoctorSandbox(sandboxId, port, opts.probePath)
2550
- : null;
2551
- const previewUrl = stringOrNull(doctor?.["preview_url"]);
2552
- const previewReady = doctor && typeof doctor["preview_ready"] === "boolean"
2553
- ? Boolean(doctor["preview_ready"])
2554
- : null;
2555
- const recommendations = buildRecoveryRecommendations({
2556
- sandbox,
2557
- execOk,
2558
- port,
2559
- previewReady,
2560
- template,
2561
- });
2562
- return {
2563
- query: idOrName,
2564
- sandbox,
2565
- sandbox_id: sandboxId,
2566
- matched_by: resolved.matchedBy,
2567
- exec_ok: execOk,
2568
- app_port: port,
2569
- preview_ready: previewReady,
2570
- preview_url: previewUrl,
2571
- doctor,
2572
- recommendations,
2573
- commands: recoveryCommands(sandboxId, name, port),
2574
- };
2575
- }
2576
- async function resolveSandboxForRecovery(c, idOrName) {
2577
- try {
2578
- const direct = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(idOrName)}`)));
2579
- const sandbox = asRecord(direct);
2580
- if (sandbox)
2581
- return { sandbox, matchedBy: "id" };
2582
- }
2583
- catch {
2584
- // Fall through to name lookup.
2585
- }
2586
- const list = unwrap(await c.apiGet(apiPath("/sandboxes")));
2587
- const items = Array.isArray(list)
2588
- ? list
2589
- : Array.isArray((asRecord(list) ?? {})["data"])
2590
- ? (asRecord(list) ?? {})["data"]
2591
- : [];
2592
- const matches = items
2593
- .map(asRecord)
2594
- .filter((item) => Boolean(item))
2595
- .filter((item) => {
2596
- const name = stringOrNull(item["name"]);
2597
- const id = stringOrNull(item["id"]);
2598
- return name === idOrName || id?.startsWith(idOrName);
2599
- })
2600
- .sort((a, b) => {
2601
- const aTime = Date.parse(stringOrNull(a["created_at"]) ?? "") || 0;
2602
- const bTime = Date.parse(stringOrNull(b["created_at"]) ?? "") || 0;
2603
- return bTime - aTime;
2604
- });
2605
- return { sandbox: matches[0] ?? null, matchedBy: matches[0] ? "name" : null };
2606
- }
2607
- async function checkSandboxExec(c, sandboxId) {
2608
- try {
2609
- const result = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), {
2610
- command: "pwd",
2611
- }));
2612
- const record = asRecord(result);
2613
- return Number(record?.["exit_code"] ?? 0) === 0;
2614
- }
2615
- catch {
2616
- return false;
2617
- }
2618
- }
2619
- async function safeDoctorSandbox(sandboxId, port, probePath) {
2620
- try {
2621
- return await doctorSandbox(sandboxId, port, probePath);
2622
- }
2623
- catch (err) {
2624
- return {
2625
- preview_ready: false,
2626
- error: err instanceof Error ? err.message : String(err),
2627
- };
2628
- }
2629
- }
2630
- function suggestedRecoveryPort(template) {
2631
- if (template === "nextjs" || template === "next-js")
2632
- return 3000;
2633
- return null;
2634
- }
2635
- function buildRecoveryRecommendations(input) {
2636
- const recs = [];
2637
- const state = stringOrNull(input.sandbox["state"]);
2638
- const ready = Boolean(input.sandbox["ready"]);
2639
- if (state !== "running") {
2640
- recs.push(`Sandbox state is ${state ?? "unknown"}; wait or recreate before uploading files.`);
2641
- }
2642
- else if (!ready) {
2643
- recs.push("Sandbox exists but is not fully ready; try exec/write-file before retrying deploy.");
2644
- }
2645
- if (!input.execOk) {
2646
- recs.push("Exec health failed; retry later or recreate the sandbox.");
2647
- }
2648
- else {
2649
- recs.push("Exec works; if upload/deploy failed, use write-file/patch plus service up.");
2650
- }
2651
- if (input.template === "nextjs" && input.port !== 3000) {
2652
- recs.push("Next.js templates default to port 3000; use 3000 unless you intentionally reconfigured readiness.");
2653
- }
2654
- if (input.port != null && input.previewReady === false) {
2655
- recs.push("Preview is not ready; start the app process, then run sandbox wait.");
2656
- }
2657
- if (input.previewReady === true) {
2658
- recs.push("Preview is ready; publish the sandbox to a durable deployment.");
2659
- }
2660
- return recs;
2661
- }
2662
- function recoveryCommands(sandboxId, name, port) {
2663
- const commands = {
2664
- health: `miosa sandbox exec ${sandboxId} --json -- pwd`,
2665
- files: `miosa sandbox write-file ${sandboxId} /workspace/app/page.jsx ./page.jsx --json`,
2666
- };
2667
- if (port != null) {
2668
- commands.start = `miosa sandbox service up ${sandboxId} next --cwd /workspace --port ${port} --cmd "npm run dev -- --hostname 0.0.0.0 --port ${port}" --json`;
2669
- commands.wait = `miosa sandbox wait ${sandboxId} --port ${port} --timeout 180 --json`;
2670
- commands.publish = `miosa sandbox publish ${sandboxId} --path /workspace --name ${shellArg(name ?? "Recovered app")} --build-command "npm run build" --run-command "npm run start" --port ${port} --wait --timeout 900s --json`;
2671
- }
2672
- return commands;
2673
- }
2674
- function renderRecoverReport(report) {
2675
- if (!report.sandbox_id) {
2676
- console.log(chalk.red("No matching sandbox found."));
2677
- console.log(` ${report.commands["list"]}`);
2678
- return;
2679
- }
2680
- console.log(chalk.bold("Sandbox recovery"));
2681
- console.log();
2682
- console.log(` ${chalk.bold("Sandbox")} ${report.sandbox_id}`);
2683
- console.log(` ${chalk.bold("Matched")} ${report.matched_by}`);
2684
- console.log(` ${chalk.bold("Exec")} ${report.exec_ok ? chalk.green("ok") : chalk.red("failed")}`);
2685
- if (report.app_port != null) {
2686
- console.log(` ${chalk.bold("Port")} ${report.app_port}`);
2687
- }
2688
- if (report.preview_url) {
2689
- console.log(` ${chalk.bold("Preview")} ${chalk.cyan(report.preview_url)}`);
2690
- }
2691
- if (report.preview_ready != null) {
2692
- console.log(` ${chalk.bold("Ready")} ${report.preview_ready ? chalk.green("yes") : chalk.yellow("no")}`);
2693
- }
2694
- console.log();
2695
- for (const rec of report.recommendations) {
2696
- console.log(` - ${rec}`);
2697
- }
2698
- console.log();
2699
- console.log(chalk.bold("Next commands"));
2700
- for (const [label, command] of Object.entries(report.commands)) {
2701
- console.log(` ${chalk.dim(label.padEnd(7))} ${command}`);
2702
- }
2703
- }
2704
- async function createSandboxForDeploy(c, template, name, source) {
2705
- const body = { template_id: template };
2706
- if (name)
2707
- body["name"] = name;
2708
- if (source?.source)
2709
- body["source"] = source.source;
2710
- if (source?.revision)
2711
- body["revision"] = source.revision;
2712
- if (source?.depth != null)
2713
- body["depth"] = source.depth;
2714
- const created = unwrap(await c.apiPost(apiPath("/sandboxes"), body));
2715
- const sandboxId = typeof created["id"] === "string" ? created["id"] : "";
2716
- if (!sandboxId)
2717
- throw new UserError("Sandbox create did not return an id.");
2718
- return sandboxId;
2719
- }
2720
- async function readRemoteAppManifest(c, sandboxId, timeoutSec) {
2721
- const deadline = Date.now() + Math.min(timeoutSec, 120) * 1000;
2722
- while (Date.now() < deadline) {
2723
- for (const filename of [
2724
- "miosa.app.yml",
2725
- "miosa.app.yaml",
2726
- "miosa.app.json",
2727
- ]) {
2728
- const result = await c
2729
- .apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), {
2730
- command: `test -f /workspace/${filename} && cat /workspace/${filename}`,
2731
- cwd: "/workspace",
2732
- timeout: 10,
2733
- })
2734
- .then(unwrap)
2735
- .catch(() => null);
2736
- const row = asRecord(result);
2737
- if (Number(row?.["exit_code"] ?? 1) === 0 &&
2738
- typeof row?.["stdout"] === "string") {
2739
- return parseAppManifest(filename, row["stdout"]);
2740
- }
2741
- }
2742
- await sleep(1500);
2743
- }
2744
- return null;
2745
- }
2746
- async function waitForSandboxRunning(c, sandboxId, timeoutSec,
2747
- // Off by default so existing callers (e.g. `sandbox deploy`, which has its
2748
- // own downstream recovery) keep their behavior. The explicit user-facing
2749
- // waits (`create --wait`, `sandbox wait`) opt in.
2750
- requireCommandReady = false) {
2751
- const deadline = Date.now() + timeoutSec * 1000;
2752
- while (Date.now() < deadline) {
2753
- const sandbox = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
2754
- const state = String(sandbox["state"] ?? sandbox["status"] ?? "").toLowerCase();
2755
- if (state === "running" || state === "active") {
2756
- // A running VM is not proof that exec will work: the command agent may
2757
- // not be attached yet (409 SANDBOX_NOT_COMMAND_READY). Confirm
2758
- // command-readiness so `--wait` only reports success once exec works.
2759
- if (requireCommandReady) {
2760
- await confirmCommandReady(c, sandboxId, deadline);
2761
- }
2762
- return sandbox;
2763
- }
2764
- if (state === "error" || state === "failed") {
2765
- throw sandboxStateError(sandboxId, sandbox, state);
2766
- }
2767
- if (state === "destroyed") {
2768
- throw sandboxStateError(sandboxId, sandbox, state);
2769
- }
2770
- await sleep(1500);
2771
- }
2772
- throw new UserError(`Sandbox ${sandboxId} did not become running within ${timeoutSec}s.`);
2773
- }
2774
- /**
2775
- * After a sandbox reaches `running`, poll `GET /sandboxes/:id/readiness` until
2776
- * it is command-ready (exec will succeed). Bounded by the running-wait
2777
- * `deadline`, extended by a command-ready grace window. A missing readiness
2778
- * endpoint (`NOT_FOUND`, older servers) is tolerated — return without blocking.
2779
- * Throws when command-readiness is not reached before the deadline.
2780
- */
2781
- async function confirmCommandReady(c, sandboxId, runningDeadline) {
2782
- const deadline = Math.max(runningDeadline, Date.now() + COMMAND_READY_WAIT_TIMEOUT_SEC * 1000);
2783
- for (;;) {
2784
- try {
2785
- const readiness = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}/readiness`)));
2786
- if (isCommandReady(readiness))
2787
- return;
2788
- }
2789
- catch (err) {
2790
- // Older servers may not expose the readiness endpoint. Don't block the
2791
- // create — treat a missing endpoint as best-effort success.
2792
- if (err instanceof ApiResponseError && err.code === "NOT_FOUND")
2793
- return;
2794
- // Other transient errors: keep polling until the deadline.
2795
- }
2796
- if (Date.now() >= deadline) {
2797
- throw new UserError(`Sandbox ${sandboxId} reached running but its command agent did not attach in time; exec may return SANDBOX_NOT_COMMAND_READY.`, "Retry the command, or poll `miosa sandbox get <id>` before exec.");
2798
- }
2799
- await sleep(COMMAND_READY_POLL_INTERVAL_MS);
2800
- }
2801
- }
2802
- async function resumeSandboxAndPrint(sandboxId, opts) {
2803
- try {
2804
- const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/resume`), {}, opts.idempotencyKey
2805
- ? { "Idempotency-Key": opts.idempotencyKey }
2806
- : undefined));
2807
- printValue(result, opts);
2808
- }
2809
- catch (err) {
2810
- if (err instanceof ApiResponseError && err.code === "SANDBOX_NOT_PAUSED") {
2811
- throw await enrichSandboxLifecycleError(sandboxId, err);
2812
- }
2813
- throw err;
2814
- }
2815
- }
2816
- async function preflightSandboxConnector(sandboxId, params) {
2817
- await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/connectors/preflight`), {
2818
- provider: params.provider,
2819
- connector: params.connector,
2820
- model: params.model,
2821
- cwd: params.cwd,
2822
- });
2823
- }
2824
- /**
2825
- * Propagate a remote command's exit code to the CLI process.
2826
- *
2827
- * A `sandbox exec` whose remote command exits nonzero must make the CLI exit
2828
- * nonzero too, or CI and shell `&&` chains treat a failed command as a success.
2829
- * This applies even in --json mode: the JSON already carries `exit_code`, but a
2830
- * script that pipes to `jq` still relies on `$?`. Exit codes are clamped to the
2831
- * 1-255 range a process can actually report; any nonzero-but-unrepresentable
2832
- * code collapses to 1.
2833
- */
2834
- function applyRemoteExitCode(value) {
2835
- if (!value || typeof value !== "object" || Array.isArray(value))
2836
- return;
2837
- const raw = value["exit_code"];
2838
- if (raw == null)
2839
- return;
2840
- const code = Number(raw);
2841
- if (!Number.isFinite(code) || code === 0)
2842
- return;
2843
- const clamped = Number.isInteger(code) && code >= 1 && code <= 255 ? code : 1;
2844
- process.exitCode = clamped;
2845
- }
2846
- async function postSandboxExecAndPrint(sandboxId, opts, body) {
2847
- try {
2848
- const value = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
2849
- printValue(value, opts);
2850
- applyRemoteExitCode(value);
2851
- }
2852
- catch (err) {
2853
- if (err instanceof ApiResponseError && err.code === "SANDBOX_NOT_RUNNING") {
2854
- throw await enrichSandboxLifecycleError(sandboxId, err);
2855
- }
2856
- throw err;
2857
- }
2858
- }
2859
- async function enrichSandboxLifecycleError(sandboxId, err) {
2860
- let sandbox = null;
2861
- try {
2862
- sandbox = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
2863
- }
2864
- catch {
2865
- // Preserve the original API error when the follow-up lookup is unavailable.
2866
- }
2867
- if (!sandbox)
2868
- return err;
2869
- const state = String(sandbox["state"] ?? sandbox["status"] ?? "unknown");
2870
- const details = sandboxLifecycleDetails(sandbox);
2871
- const message = state === "destroyed"
2872
- ? `Sandbox ${sandboxId} is destroyed, not paused. It cannot be resumed.`
2873
- : `${err.message}. Current sandbox state: ${state}.`;
2874
- return new ApiResponseError(err.code, message, EXIT_USER_ERROR, false, sandboxRecoveryHint(sandboxId, sandbox), details, err.requestId);
2875
- }
2876
- function sandboxStateError(sandboxId, sandbox, state) {
2877
- const reason = sandboxLastErrorReason(sandbox);
2878
- const message = state === "destroyed"
2879
- ? `Sandbox ${sandboxId} is destroyed, not paused. It cannot be resumed.`
2880
- : `Sandbox ${sandboxId} entered ${state} state${reason ? `: ${reason}` : ""}.`;
2881
- return new UserError(message, sandboxRecoveryHint(sandboxId, sandbox));
2882
- }
2883
- function sandboxRecoveryHint(sandboxId, sandbox) {
2884
- const state = String(sandbox["state"] ?? sandbox["status"] ?? "unknown");
2885
- const name = String(sandbox["name"] ?? "new-sandbox");
2886
- const template = String(sandbox["template_id"] ?? "nextjs");
2887
- const timeoutRemainingSec = timeoutRemainingSeconds(sandbox);
2888
- if (state === "running" &&
2889
- timeoutRemainingSec != null &&
2890
- timeoutRemainingSec <= EXPIRING_SANDBOX_THRESHOLD_SEC) {
2891
- return `Sandbox expires soon. Extend it with: miosa sandbox extend ${sandboxId} --timeout 1h`;
2892
- }
2893
- if (state === "paused") {
2894
- return `Resume it with: miosa sandbox resume ${sandboxId}`;
2895
- }
2896
- if (state === "destroyed") {
2897
- return `Create a replacement with: miosa sandbox create --template ${template} --name ${shellQuote(name)} --timeout 1h --wait --json`;
2898
- }
2899
- if (state === "error" || state === "failed") {
2900
- return `Inspect recovery with: miosa sandbox recover ${sandboxId}`;
2901
- }
2902
- return `Inspect it with: miosa sandbox show ${sandboxId} --json`;
2903
- }
2904
- function sandboxLifecycleDetails(sandbox) {
2905
- return {
2906
- sandbox_id: sandbox["id"],
2907
- name: sandbox["name"],
2908
- state: sandbox["state"] ?? sandbox["status"],
2909
- timeout_sec: sandbox["timeout_sec"],
2910
- timeout_remaining_ms: sandbox["timeout_remaining_ms"],
2911
- destroyed_at: sandbox["destroyed_at"],
2912
- last_error: sandboxLastError(sandbox),
2913
- };
2914
- }
2915
- function sandboxLastErrorReason(sandbox) {
2916
- const lastError = sandboxLastError(sandbox);
2917
- if (!lastError)
2918
- return null;
2919
- const reason = lastError["reason"];
2920
- return typeof reason === "string" && reason.length > 0 ? reason : null;
2921
- }
2922
- function sandboxLastError(sandbox) {
2923
- const metadata = sandbox["metadata"];
2924
- if (!metadata || typeof metadata !== "object")
2925
- return null;
2926
- const lastError = metadata["last_error"];
2927
- return lastError && typeof lastError === "object"
2928
- ? lastError
2929
- : null;
2930
- }
2931
- function timeoutRemainingSeconds(sandbox) {
2932
- const ms = sandbox["timeout_remaining_ms"];
2933
- if (typeof ms === "number")
2934
- return Math.ceil(ms / 1000);
2935
- const sec = sandbox["timeout_remaining_sec"];
2936
- return typeof sec === "number" ? sec : null;
2937
- }
2938
- /**
2939
- * Prints a yellow expiry warning when a running sandbox has <5 minutes left.
2940
- * Call this in any non-JSON render path that shows sandbox state.
2941
- */
2942
- function warnIfExpiringSoon(sandbox, sandboxId) {
2943
- const state = String(sandbox["state"] ?? sandbox["status"] ?? "").toLowerCase();
2944
- if (state !== "running" && state !== "active")
2945
- return;
2946
- const remainingSec = timeoutRemainingSeconds(sandbox);
2947
- if (remainingSec == null || remainingSec > EXPIRING_SANDBOX_THRESHOLD_SEC)
2948
- return;
2949
- const mins = Math.floor(remainingSec / 60);
2950
- const secs = remainingSec % 60;
2951
- const humanTime = mins > 0 ? `${mins}m${secs > 0 ? `${secs}s` : ""}` : `${secs}s`;
2952
- console.log(chalk.yellow(` Warning: Sandbox expires in ${humanTime}. Extend: miosa sandbox extend ${sandboxId} --timeout 1h`));
2953
- console.log();
2954
- }
2955
- function createDeployArchive(sourceDir) {
2956
- const archivePath = path.join(os.tmpdir(), `miosa-deploy-${process.pid}-${Date.now()}.tgz`);
2957
- const result = spawnSync("tar", [
2958
- "--exclude",
2959
- ".git",
2960
- "--exclude",
2961
- "node_modules",
2962
- "--exclude",
2963
- ".next",
2964
- "--exclude",
2965
- "dist",
2966
- "--exclude",
2967
- ".DS_Store",
2968
- "--exclude",
2969
- "._*",
2970
- "--exclude",
2971
- "__MACOSX",
2972
- "-czf",
2973
- archivePath,
2974
- "-C",
2975
- sourceDir,
2976
- ".",
2977
- ], { stdio: "pipe", env: { ...process.env, COPYFILE_DISABLE: "1" } });
2978
- if (result.status !== 0) {
2979
- throw new UserError(`Could not archive ${sourceDir}: ${result.stderr.toString().trim() || "tar failed"}`);
2980
- }
2981
- return archivePath;
2982
- }
2983
- async function uploadFileToSandbox(c, sandboxId, localPath, remotePath) {
2984
- return writeBytesToSandbox(c, sandboxId, remotePath, fs.readFileSync(localPath));
2985
- }
2986
- async function writeBytesToSandbox(c, sandboxId, remotePath, bytes) {
2987
- try {
2988
- return await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/files`), {
2989
- path: remotePath,
2990
- content: bytes.toString("base64"),
2991
- });
2992
- }
2993
- catch (err) {
2994
- if (!shouldFallbackSandboxUpload(err))
2995
- throw err;
2996
- await writeBytesToSandboxViaExec(c, sandboxId, remotePath, bytes);
2997
- return {
2998
- data: {
2999
- sandbox_id: sandboxId,
3000
- path: remotePath,
3001
- size: bytes.length,
3002
- transport: "exec_chunked_fallback",
3003
- },
3004
- };
3005
- }
3006
- }
3007
- async function writeBytesToSandboxViaExec(c, sandboxId, remotePath, bytes) {
3008
- const base64 = bytes.toString("base64");
3009
- const base64Path = `${remotePath}.b64`;
3010
- const chunkSize = 48_000;
3011
- await execSandbox(c, sandboxId, `mkdir -p ${shellQuote(path.posix.dirname(remotePath))} && rm -f ${shellQuote(remotePath)} ${shellQuote(base64Path)}`, "/");
3012
- for (let offset = 0; offset < base64.length; offset += chunkSize) {
3013
- const chunk = base64.slice(offset, offset + chunkSize);
3014
- await execSandbox(c, sandboxId, `printf '%s' ${shellQuote(chunk)} >> ${shellQuote(base64Path)}`, "/");
3015
- }
3016
- await execSandbox(c, sandboxId, `base64 -d ${shellQuote(base64Path)} > ${shellQuote(remotePath)} && rm -f ${shellQuote(base64Path)}`, "/");
3017
- }
3018
- function shouldFallbackSandboxUpload(err) {
3019
- if (err instanceof NetworkError || err instanceof ServerError)
3020
- return true;
3021
- if (err instanceof ApiResponseError) {
3022
- return (err.retryable ||
3023
- /AGENT_UNAVAILABLE|SANDBOX_FILE_AGENT_UNAVAILABLE/i.test(err.code));
3024
- }
3025
- if (err instanceof Error) {
3026
- return /fetch failed|ECONNRESET|HTTP 502|AGENT_UNAVAILABLE|other side closed|socket hang up/i.test(err.message);
3027
- }
3028
- return false;
3029
- }
3030
- function recoveryCommandForSandboxDeploy(sandboxId, opts, localDir) {
3031
- const parts = [
3032
- "miosa",
3033
- "sandbox",
3034
- "deploy",
3035
- shellQuote(localDir),
3036
- "--sandbox",
3037
- shellQuote(sandboxId),
3038
- ];
3039
- if (opts.port != null)
3040
- parts.push("--port", String(opts.port));
3041
- if (opts.publishPort != null)
3042
- parts.push("--publish-port", String(opts.publishPort));
3043
- if (opts.installCommand)
3044
- parts.push("--install-command", shellQuote(opts.installCommand));
3045
- if (opts.install === false)
3046
- parts.push("--no-install");
3047
- if (opts.start)
3048
- parts.push("--start", shellQuote(opts.start));
3049
- if (opts.wait)
3050
- parts.push("--wait");
3051
- if (opts.timeout != null)
3052
- parts.push("--timeout", `${opts.timeout}s`);
3053
- if (opts.probePath)
3054
- parts.push("--probe-path", shellQuote(opts.probePath));
3055
- return parts.join(" ");
3056
- }
3057
- function handleSandboxDeployError(err, opts) {
3058
- if (err instanceof SandboxDeployPartialError) {
3059
- const cause = err.causeError;
3060
- const message = cause instanceof Error ? cause.message : String(cause);
3061
- const retryable = cause instanceof ApiResponseError
3062
- ? cause.retryable
3063
- : shouldFallbackSandboxUpload(cause);
3064
- if (isJsonMode(opts)) {
3065
- console.log(JSON.stringify({
3066
- ok: false,
3067
- error: {
3068
- code: errorCodeForDeployCause(cause),
3069
- message,
3070
- retryable,
3071
- ...(cause instanceof MiosaError && cause.requestId
3072
- ? { request_id: cause.requestId }
3073
- : {}),
3074
- },
3075
- partial_resource: {
3076
- type: "sandbox",
3077
- id: err.sandboxId,
3078
- recovery_command: err.recoveryCommand,
3079
- },
3080
- }, null, 2));
3081
- return process.exit(1);
3082
- }
3083
- console.error(chalk.red(`Error: ${message}`));
3084
- console.error(chalk.yellow(`Sandbox ${err.sandboxId} exists. Retry into it with:\n ${err.recoveryCommand}`));
3085
- return process.exit(1);
3086
- }
3087
- handleError(err);
3088
- }
3089
- function errorCodeForDeployCause(err) {
3090
- if (err instanceof ApiResponseError)
3091
- return err.code;
3092
- if (err instanceof NetworkError)
3093
- return "NETWORK";
3094
- if (err instanceof ServerError)
3095
- return "SERVER";
3096
- if (err instanceof MiosaError)
3097
- return err.constructor.name.toUpperCase();
3098
- if (err instanceof Error && /fetch failed|ECONNRESET/i.test(err.message))
3099
- return "NETWORK";
3100
- return "UNEXPECTED_ERROR";
3101
- }
3102
- async function uploadDirToSandbox(sandboxId, localDir, remoteDir, opts) {
3103
- const sourceDir = path.resolve(localDir);
3104
- if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) {
3105
- throw new UserError(`Local directory not found: ${sourceDir}`);
3106
- }
3107
- const deleteTarget = opts.delete
3108
- ? await confirmRemoteDeleteDir(sandboxId, remoteDir, !!opts.force)
3109
- : null;
3110
- const c = client();
3111
- const archivePath = createDeployArchive(sourceDir);
3112
- const remoteArchive = `/tmp/miosa-upload-${Date.now()}.tgz`;
3113
- try {
3114
- await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
3115
- const clean = deleteTarget != null
3116
- ? `rm -rf ${shellQuote(deleteTarget)} && mkdir -p ${shellQuote(deleteTarget)}`
3117
- : `mkdir -p ${shellQuote(remoteDir)}`;
3118
- await execSandbox(c, sandboxId, `${clean} && tar -xzf ${shellQuote(remoteArchive)} -C ${shellQuote(remoteDir)} && rm -f ${shellQuote(remoteArchive)}`, "/");
3119
- }
3120
- finally {
3121
- fs.rmSync(archivePath, { force: true });
3122
- }
3123
- return {
3124
- sandbox_id: sandboxId,
3125
- local_dir: sourceDir,
3126
- remote_dir: remoteDir,
3127
- files_label: path.basename(sourceDir) || sourceDir,
3128
- };
3129
- }
3130
- // --delete runs `rm -rf` inside the sandbox. Refuse protected roots, then
3131
- // require an explicit confirmation (interactive prompt, or --force/--yes)
3132
- // before anything is wiped. Returns the normalized remote dir to delete.
3133
- async function confirmRemoteDeleteDir(sandboxId, remoteDir, force) {
3134
- const target = assertDeletableRemoteDir(remoteDir);
3135
- if (force)
3136
- return target;
3137
- if (!process.stdin.isTTY) {
3138
- throw new UserError(`--delete will permanently wipe ${target} on sandbox ${sandboxId}.`, "Non-interactive session: re-run with --force (or --yes) to confirm the wipe.");
3139
- }
3140
- console.log(chalk.yellow(`--delete will permanently wipe sandbox ${sandboxId}:${target} before uploading.`));
3141
- const { default: inquirer } = await import("inquirer");
3142
- const { confirmed } = await inquirer.prompt([
3143
- {
3144
- type: "confirm",
3145
- name: "confirmed",
3146
- message: `Wipe ${sandboxId}:${target}?`,
3147
- default: false,
3148
- },
3149
- ]);
3150
- if (!confirmed) {
3151
- throw new UserError("Cancelled - nothing was deleted or uploaded.");
3152
- }
3153
- return target;
3154
- }
3155
- async function execSandbox(c, sandboxId, command, cwd, timeout) {
3156
- const body = { command: commandInCwd(command, cwd) };
3157
- if (cwd) {
3158
- body["cwd"] = cwd;
3159
- body["dir"] = cwd;
3160
- }
3161
- if (timeout != null)
3162
- body["timeout"] = timeout;
3163
- const result = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
3164
- const exitCode = Number(result["exit_code"] ?? 0);
3165
- if (exitCode !== 0) {
3166
- throw new UserError(`Sandbox command failed with exit code ${exitCode}: ${command}`, String(result["stderr"] ?? result["stdout"] ?? ""));
3167
- }
3168
- return result;
3169
- }
3170
- // Like execSandbox but does NOT throw on a non-zero exit code.
3171
- async function execSandboxRaw(c, sandboxId, command, cwd, timeout) {
3172
- const body = { command: commandInCwd(command, cwd) };
3173
- if (cwd) {
3174
- body["cwd"] = cwd;
3175
- body["dir"] = cwd;
3176
- }
3177
- if (timeout != null)
3178
- body["timeout"] = timeout;
3179
- return unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
3180
- }
3181
- function sandboxPortsFromResponse(raw) {
3182
- const root = unwrap(raw);
3183
- if (Array.isArray(root))
3184
- return root;
3185
- if (root && typeof root === "object") {
3186
- const row = root;
3187
- if (Array.isArray(row["ports"]))
3188
- return row["ports"];
3189
- if (Array.isArray(row["data"]))
3190
- return row["data"];
3191
- }
3192
- return [];
3193
- }
3194
- function renderResourceMetrics(title, raw) {
3195
- const root = unwrap(raw);
3196
- if (!root || typeof root !== "object") {
3197
- printValue(root, {});
3198
- return;
3199
- }
3200
- const row = root;
3201
- const current = row["current"] && typeof row["current"] === "object"
3202
- ? row["current"]
3203
- : {};
3204
- printBanner({ subtitle: title });
3205
- console.log(kvPanel([
3206
- {
3207
- label: "resource_id",
3208
- value: String(row["resource_id"] ?? row["sandbox_id"] ?? "-"),
3209
- },
3210
- { label: "window", value: String(row["window"] ?? "1h") },
3211
- { label: "state", value: formatState(current["state"]) },
3212
- { label: "ready", value: formatBool(current["ready"]) },
3213
- { label: "cpu", value: formatMaybe(current["cpu_count"]) },
3214
- { label: "memory", value: formatMb(current["memory_mb"]) },
3215
- { label: "disk", value: formatMb(current["disk_size_mb"]) },
3216
- { label: "uptime", value: formatSeconds(current["uptime_sec"]) },
3217
- {
3218
- label: "timeout_remaining",
3219
- value: formatSecondsOrAlwaysOn(current["timeout_remaining_sec"]),
3220
- },
3221
- { label: "node", value: formatMaybe(current["node_id"]) },
3222
- { label: "ip", value: formatMaybe(current["ip_address"]) },
3223
- { label: "boot", value: formatMs(current["boot_ms"]) },
3224
- { label: "envd_ready", value: formatMs(current["envd_ready_ms"]) },
3225
- ]));
3226
- }
3227
- function formatState(value) {
3228
- const state = String(value ?? "unknown");
3229
- if (["running", "active", "healthy", "ready"].includes(state)) {
3230
- return chalk.green(state);
3231
- }
3232
- if (["provisioning", "starting", "building", "pending"].includes(state)) {
3233
- return chalk.yellow(state);
3234
- }
3235
- if (["failed", "error", "unhealthy"].includes(state)) {
3236
- return chalk.red(state);
3237
- }
3238
- return state;
3239
- }
3240
- function formatBool(value) {
3241
- if (value === true)
3242
- return chalk.green("true");
3243
- if (value === false)
3244
- return chalk.red("false");
3245
- return chalk.dim("-");
3246
- }
3247
- function formatMaybe(value) {
3248
- if (value === null || value === undefined || value === "")
3249
- return chalk.dim("-");
3250
- return String(value);
3251
- }
3252
- function formatMb(value) {
3253
- if (typeof value !== "number")
3254
- return formatMaybe(value);
3255
- return formatBytes(value * 1024 * 1024);
3256
- }
3257
- function formatMs(value) {
3258
- if (typeof value !== "number")
3259
- return formatMaybe(value);
3260
- return `${value}ms`;
3261
- }
3262
- function formatSeconds(value) {
3263
- if (typeof value !== "number")
3264
- return formatMaybe(value);
3265
- return formatDuration(value * 1000);
3266
- }
3267
- function formatSecondsOrAlwaysOn(value) {
3268
- if (value === null || value === undefined)
3269
- return chalk.dim("always-on/none");
3270
- return formatSeconds(value);
3271
- }
3272
- // Stream exec output: run in background to a log file, poll-read new bytes
3273
- // until the process exits, then print the final exit code.
3274
- async function runFollowExec(sandboxId, cmd, cwd, env, timeoutSec, opts) {
3275
- if (!cmd) {
3276
- throw new UserError("No command given to follow.");
3277
- }
3278
- const c = client();
3279
- const logPath = `/tmp/miosa-follow-${Date.now()}.log`;
3280
- const envPrefix = Object.entries(env)
3281
- .map(([k, v]) => `${k}=${shellQuote(v)} `)
3282
- .join("");
3283
- // Run command, capture exit code into a sentinel file, in the background.
3284
- const exitPath = `${logPath}.exit`;
3285
- const inner = `${envPrefix}${cmd}`;
3286
- const launch = `nohup sh -lc ${shellQuote(`( ${inner} ) > ${shellQuote(logPath)} 2>&1; echo $? > ${shellQuote(exitPath)}`)} >/dev/null 2>&1 & echo $!`;
3287
- const started = await execSandboxRaw(c, sandboxId, launch, cwd, timeoutSec);
3288
- const pid = String(started["stdout"] ?? "").trim();
3289
- if (!pid) {
3290
- throw new UserError("Could not start background command for --follow.");
3291
- }
3292
- const deadline = timeoutSec ? Date.now() + timeoutSec * 1000 : Infinity;
3293
- let offset = 0;
3294
- for (;;) {
3295
- const read = await execSandboxRaw(c, sandboxId, `tail -c +${offset + 1} ${shellQuote(logPath)} 2>/dev/null`);
3296
- const chunk = String(read["stdout"] ?? "");
3297
- if (chunk.length > 0) {
3298
- process.stdout.write(chunk);
3299
- offset += Buffer.byteLength(chunk);
3300
- }
3301
- const alive = await execSandboxRaw(c, sandboxId, `kill -0 ${shellQuote(pid)} 2>/dev/null && echo alive || echo done`);
3302
- if (String(alive["stdout"] ?? "").trim() === "done")
3303
- break;
3304
- if (Date.now() > deadline) {
3305
- console.error(chalk.yellow(`\nTimed out after ${timeoutSec}s — command may still be running (pid ${pid}).`));
3306
- return;
3307
- }
3308
- await sleep(1000);
3309
- }
3310
- // Drain any trailing output.
3311
- const tail = await execSandboxRaw(c, sandboxId, `tail -c +${offset + 1} ${shellQuote(logPath)} 2>/dev/null`);
3312
- const tailChunk = String(tail["stdout"] ?? "");
3313
- if (tailChunk.length > 0)
3314
- process.stdout.write(tailChunk);
3315
- const exitRead = await execSandboxRaw(c, sandboxId, `cat ${shellQuote(exitPath)} 2>/dev/null`);
3316
- const exitCode = Number(String(exitRead["stdout"] ?? "0").trim() || "0");
3317
- if (!isJsonMode(opts) && exitCode !== 0) {
3318
- console.error(chalk.red(`\nexit code: ${exitCode}`));
3319
- }
3320
- // Streamed commands must fail the CLI process on a nonzero remote exit too,
3321
- // so `--follow` in CI behaves the same as a plain exec.
3322
- applyRemoteExitCode({ exit_code: exitCode });
3323
- }
3324
- async function waitForInternalHttp(c, sandboxId, port, probePath, timeoutSec) {
3325
- const deadline = Date.now() + timeoutSec * 1000;
3326
- let last = { ok: false, status: null };
3327
- while (Date.now() < deadline) {
3328
- last = await probeInternalHttp(c, sandboxId, port, probePath);
3329
- if (last.ok)
3330
- return last;
3331
- await sleep(1500);
3332
- }
3333
- throw new UserError(`App did not answer inside the sandbox on port ${port} within ${timeoutSec}s.`, last.error);
3334
- }
3335
- async function probeInternalHttp(c, sandboxId, port, probePath) {
3336
- try {
3337
- const result = await execSandbox(c, sandboxId, `python3 - <<'PY'\nimport urllib.request, sys\nurl = 'http://127.0.0.1:${port}${probePath.startsWith("/") ? probePath : `/${probePath}`}'\ntry:\n r = urllib.request.urlopen(url, timeout=3)\n print(r.status)\n sys.exit(0 if 200 <= r.status < 400 else 1)\nexcept Exception as e:\n print(e)\n sys.exit(1)\nPY`, "/", 10);
3338
- const status = Number(String(result["stdout"] ?? "")
3339
- .trim()
3340
- .split(/\s+/)[0]);
3341
- return {
3342
- ok: Number.isFinite(status) && status >= 200 && status < 400,
3343
- status,
3344
- };
3345
- }
3346
- catch (err) {
3347
- return {
3348
- ok: false,
3349
- status: null,
3350
- error: err instanceof Error ? err.message : String(err),
3351
- };
3352
- }
3353
- }
3354
- async function waitForPublicPreview(previewUrl, probePath, timeoutSec) {
3355
- const deadline = Date.now() + timeoutSec * 1000;
3356
- let last = { ok: false, status: null };
3357
- while (Date.now() < deadline) {
3358
- last = await probePublicPreview(previewUrl, probePath);
3359
- if (last.ok)
3360
- return last;
3361
- await sleep(2000);
3362
- }
3363
- throw new UserError(`Preview URL was created but did not become publicly ready within ${timeoutSec}s.`, last.error ??
3364
- (last.status ? `Last HTTP status: ${last.status}` : undefined));
3365
- }
3366
- async function probePublicPreview(previewUrl, probePath) {
3367
- try {
3368
- const url = new URL(previewUrl);
3369
- url.pathname = joinUrlPath(url.pathname, probePath);
3370
- const t0 = Date.now();
3371
- const res = await fetch(url, { method: "GET", redirect: "manual" });
3372
- return {
3373
- ok: (res.status >= 200 && res.status < 400) ||
3374
- res.status === 401 ||
3375
- res.status === 403,
3376
- status: res.status,
3377
- latency_ms: Date.now() - t0,
3378
- };
3379
- }
3380
- catch (err) {
3381
- return {
3382
- ok: false,
3383
- status: null,
3384
- error: err instanceof Error ? err.message : String(err),
3385
- };
3386
- }
3387
- }
3388
- async function waitForLocalHttp(localPort, probePath, timeoutSec) {
3389
- const deadline = Date.now() + timeoutSec * 1000;
3390
- let last = { ok: false, status: null };
3391
- const url = `http://127.0.0.1:${localPort}${probePath.startsWith("/") ? probePath : `/${probePath}`}`;
3392
- while (Date.now() < deadline) {
3393
- last = await probePublicPreview(url, "/");
3394
- if (last.ok)
3395
- return last;
3396
- await sleep(1000);
3397
- }
3398
- throw new UserError(`Local forwarded URL did not answer within ${timeoutSec}s.`, last.error ??
3399
- (last.status ? `Last HTTP status: ${last.status}` : undefined));
3400
- }
3401
- function defaultInstallCommand(sourceDir) {
3402
- if (fs.existsSync(path.join(sourceDir, "package.json")))
3403
- return "npm install";
3404
- if (fs.existsSync(path.join(sourceDir, "requirements.txt")))
3405
- return "pip install -r requirements.txt";
3406
- return null;
3407
- }
3408
- function defaultStartCommand(framework, port) {
3409
- if (framework === "nextjs")
3410
- return `npm run dev -- -H 0.0.0.0 -p ${port}`;
3411
- if (framework === "vite-react")
3412
- return `npm run dev -- --host 0.0.0.0 --port ${port}`;
3413
- if (framework === "static")
3414
- return `python3 -m http.server ${port} --bind 0.0.0.0`;
3415
- return `npm run dev -- --host 0.0.0.0 --port ${port}`;
3416
- }
3417
- function normalizeRemoteWorkdir(value) {
3418
- if (!value || value === ".")
3419
- return "/workspace";
3420
- if (!value.startsWith("/"))
3421
- return `/workspace/${value.replace(/^\.\//, "")}`;
3422
- return value;
3423
- }
3424
- function extractUrl(value) {
3425
- if (!value || typeof value !== "object" || Array.isArray(value))
3426
- return null;
3427
- const row = value;
3428
- for (const key of ["url", "preview_url", "public_url"]) {
3429
- if (typeof row[key] === "string" && row[key])
3430
- return row[key];
3431
- }
3432
- return null;
3433
- }
3434
- function asRecord(value) {
3435
- if (!value || typeof value !== "object" || Array.isArray(value))
3436
- return null;
3437
- return value;
3438
- }
3439
- function stringField(row, key) {
3440
- const value = row?.[key];
3441
- return typeof value === "string" && value.length > 0 ? value : null;
3442
- }
3443
- function isSandboxTarget(value) {
3444
- const idx = value.indexOf(":");
3445
- if (idx <= 0)
3446
- return false;
3447
- return value.slice(idx + 1).startsWith("/");
3448
- }
3449
- function parseSandboxTarget(target) {
3450
- const idx = target.indexOf(":");
3451
- if (idx <= 0 || idx === target.length - 1) {
3452
- throw new UserError(`Invalid sandbox target: ${target}`, "Use the form <sandbox-id>:/absolute/path");
3453
- }
3454
- const sandboxId = target.slice(0, idx);
3455
- const remotePath = target.slice(idx + 1);
3456
- if (!remotePath.startsWith("/")) {
3457
- throw new UserError(`Invalid remote path: ${remotePath}`, "Sandbox copy targets must use an absolute path, e.g. sbx_123:/workspace");
3458
- }
3459
- return { sandboxId, remotePath };
3460
- }
3461
- async function downloadSandboxPath(sandboxId, remotePath, localTarget) {
3462
- const c = client();
3463
- const kind = await remotePathKind(c, sandboxId, remotePath);
3464
- if (kind === "directory") {
3465
- const remoteArchive = `/tmp/miosa-copy-${Date.now()}.tgz`;
3466
- await execSandbox(c, sandboxId, `tar -czf ${shellQuote(remoteArchive)} -C ${shellQuote(remotePath)} .`, "/");
3467
- const archiveBytes = await readSandboxFile(c, sandboxId, remoteArchive);
3468
- const localDir = path.resolve(localTarget);
3469
- fs.mkdirSync(localDir, { recursive: true });
3470
- const localArchive = path.join(os.tmpdir(), `miosa-copy-${process.pid}-${Date.now()}.tgz`);
3471
- fs.writeFileSync(localArchive, archiveBytes);
3472
- try {
3473
- const result = spawnSync("tar", ["-xzf", localArchive, "-C", localDir], {
3474
- stdio: "pipe",
3475
- });
3476
- if (result.status !== 0) {
3477
- throw new UserError(`Could not extract ${remotePath}: ${result.stderr.toString().trim() || "tar failed"}`);
3478
- }
3479
- }
3480
- finally {
3481
- fs.rmSync(localArchive, { force: true });
3482
- await execSandbox(c, sandboxId, `rm -f ${shellQuote(remoteArchive)}`, "/").catch(() => ({}));
3483
- }
3484
- return {
3485
- sandbox_id: sandboxId,
3486
- remote_path: remotePath,
3487
- local_path: localDir,
3488
- type: "directory",
3489
- };
3490
- }
3491
- const bytes = await readSandboxFile(c, sandboxId, remotePath);
3492
- const localPath = fs.existsSync(localTarget) && fs.statSync(localTarget).isDirectory()
3493
- ? path.join(localTarget, path.basename(remotePath))
3494
- : localTarget;
3495
- fs.mkdirSync(path.dirname(path.resolve(localPath)), { recursive: true });
3496
- fs.writeFileSync(localPath, bytes);
3497
- return {
3498
- sandbox_id: sandboxId,
3499
- remote_path: remotePath,
3500
- local_path: path.resolve(localPath),
3501
- type: "file",
3502
- };
3503
- }
3504
- async function remotePathKind(c, sandboxId, remotePath) {
3505
- const result = await execSandbox(c, sandboxId, `if [ -d ${shellQuote(remotePath)} ]; then echo directory; elif [ -f ${shellQuote(remotePath)} ]; then echo file; else exit 2; fi`, "/");
3506
- const kind = String(result["stdout"] ?? "").trim();
3507
- if (kind === "directory")
3508
- return "directory";
3509
- return "file";
3510
- }
3511
- async function readSandboxFile(c, sandboxId, remotePath) {
3512
- const encoded = enc(remotePath.replace(/^\//, ""));
3513
- const result = await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}/files/${encoded}`));
3514
- const data = result !== null && typeof result === "object" && !Array.isArray(result)
3515
- ? (result["data"] ?? result)
3516
- : result;
3517
- if (data !== null &&
3518
- typeof data === "object" &&
3519
- !Array.isArray(data) &&
3520
- typeof data["content"] === "string") {
3521
- return Buffer.from(data["content"], "base64");
3522
- }
3523
- throw new UserError(`Could not read sandbox file: ${remotePath}`);
3524
- }
3525
- function commandInCwd(command, cwd) {
3526
- if (!cwd)
3527
- return command;
3528
- return `cd ${shellQuote(cwd)} && ${command}`;
3529
- }
3530
- function supportedRunAgentRunners() {
3531
- return [
3532
- "claude",
3533
- "claude-code",
3534
- "codex",
3535
- "pi",
3536
- "hermes",
3537
- "osa",
3538
- "custom",
3539
- ];
3540
- }
3541
- function runtimeCommandForRunner(runner, runtimeCommand) {
3542
- const normalized = runner.trim().toLowerCase();
3543
- if (normalized === "custom") {
3544
- if (!runtimeCommand?.trim()) {
3545
- throw new Error("--runner custom requires --runtime-command, e.g. --runtime-command 'hermes-agent run'");
3546
- }
3547
- return runtimeCommand.trim();
3548
- }
3549
- const builtIns = {
3550
- claude: "claude",
3551
- "claude-code": "claude",
3552
- codex: "codex",
3553
- pi: "pi",
3554
- hermes: "hermes",
3555
- osa: "osa",
3556
- };
3557
- return builtIns[normalized] ?? null;
3558
- }
3559
- function isSupportedRunAgentRunner(runner) {
3560
- return supportedRunAgentRunners().includes(runner.trim().toLowerCase());
3561
- }
3562
- function backgroundCommand(command) {
3563
- if (!command.trim())
3564
- return command;
3565
- const logPath = `/tmp/miosa-bg-${Date.now()}.log`;
3566
- return `nohup sh -lc ${shellQuote(command)} > ${shellQuote(logPath)} 2>&1 & echo $!`;
3567
- }
3568
- function resolveSandboxCommand(words, opts) {
3569
- const positional = words.length === 1 ? (words[0] ?? "") : joinCommandWords(words);
3570
- const cmd = opts.cmd ?? opts.command ?? positional;
3571
- if (!opts.shellCmd)
3572
- return cmd;
3573
- if (!cmd.trim())
3574
- return opts.shellCmd;
3575
- return `${opts.shellCmd} ${shellQuote(cmd)}`;
3576
- }
3577
- function collectOption(value, previous) {
3578
- return [...previous, value];
3579
- }
3580
- function parseEnvPairs(pairs) {
3581
- const env = {};
3582
- for (const pair of pairs) {
3583
- const idx = pair.indexOf("=");
3584
- if (idx <= 0) {
3585
- throw new UserError(`Invalid --env value: ${pair}`, "Use KEY=VALUE, for example --env DATABASE_URL=postgresql://...");
3586
- }
3587
- env[pair.slice(0, idx)] = pair.slice(idx + 1);
3588
- }
3589
- return env;
3590
- }
3591
- function parseDurationSec(value) {
3592
- const match = String(value)
3593
- .trim()
3594
- .match(/^(\d+)(ms|s|m|h|d)?$/i);
3595
- if (!match)
3596
- throw new UserError(`Invalid duration: ${value}`);
3597
- const amount = Number(match[1]);
3598
- const unit = (match[2] ?? "s").toLowerCase();
3599
- if (unit === "ms")
3600
- return Math.max(1, Math.ceil(amount / 1000));
3601
- if (unit === "s")
3602
- return amount;
3603
- if (unit === "m")
3604
- return amount * 60;
3605
- if (unit === "h")
3606
- return amount * 60 * 60;
3607
- if (unit === "d")
3608
- return amount * 24 * 60 * 60;
3609
- return amount;
3610
- }
3611
- function parseIntegerOption(value) {
3612
- const n = Number.parseInt(String(value), 10);
3613
- if (!Number.isInteger(n))
3614
- throw new UserError(`Invalid integer: ${value}`);
3615
- return n;
3616
- }
3617
- function parseSandboxSize(value) {
3618
- const size = String(value).trim().toLowerCase();
3619
- if (size in SANDBOX_SIZE_CONTRACTS)
3620
- return size;
3621
- throw new UserError(`Invalid sandbox size: ${value}. Expected xs, small, medium, large, or xl.`);
3622
- }
3623
- /**
3624
- * Resolve the resource fields for `sandbox create`.
3625
- *
3626
- * MIOSA bills per resource consumed, not per named tier: any cpu/memory/disk
3627
- * triple is a valid custom shape. A raw --cpu/--memory pair is passed through
3628
- * to the create request UNCHANGED, whether or not it happens to match a
3629
- * published named size (xs/small/medium/large/xl) -- it is never rejected,
3630
- * snapped to the nearest tier, or forced into a size. This mirrors the
3631
- * `--data` path (and the SDK's cpuCount/memoryMb/diskSizeMb), which already
3632
- * accept an off-tier pair; the flag path must behave identically.
3633
- *
3634
- * - No --cpu/--memory/--disk: forward only the named size (or small); the
3635
- * server picks the concrete shape. Never pin a pair from the CLI.
3636
- * - With --cpu and/or --memory: fill the missing member from --size (or
3637
- * small), then forward the resulting pair as-is. If it happens to match a
3638
- * published size, that size is also sent as a convenience label. Disk is a
3639
- * floor the platform grows the rootfs to: `--disk` overrides the size's
3640
- * floor on top of any pair (`--size small --disk 20gb` -> a 2 vCPU / 4 GB
3641
- * box with a 20 GB disk).
3642
- * - The one guard kept is a genuine caller contradiction: an explicit --size
3643
- * that disagrees with the resolved shape of an explicit --cpu/--memory
3644
- * pair still fails fast, since the caller named two different shapes.
3645
- */
3646
- function resolveSandboxResources(opts) {
3647
- // No explicit resource flags → just the named size (or small).
3648
- const anyExplicit = opts.cpu != null || opts.memory != null || opts.disk != null;
3649
- if (!anyExplicit)
3650
- return { size: opts.size ?? "small" };
3651
- const sizeBase = SANDBOX_SIZE_CONTRACTS[opts.size ?? "small"];
3652
- const cpu = opts.cpu ?? sizeBase.cpu;
3653
- const memory = opts.memory ?? sizeBase.memory;
3654
- const shapeEntry = Object.entries(SANDBOX_SIZE_CONTRACTS).find(([, contract]) => contract.cpu === cpu && contract.memory === memory);
3655
- const shape = shapeEntry ? shapeEntry[0] : undefined;
3656
- if (opts.size && opts.size !== shape) {
3657
- throw new UserError(`--cpu/--memory ${cpu}/${memory} match ${shape ?? "no published size"}, ` +
3658
- `not requested size ${opts.size}.`);
3659
- }
3660
- const disk = opts.disk ?? (shape ? SANDBOX_SIZE_CONTRACTS[shape].disk : sizeBase.disk);
3661
- return { size: shape, legacy: { cpu, memory, disk } };
3662
- }
3663
- function parseSizeMb(value) {
3664
- const match = String(value)
3665
- .trim()
3666
- .match(/^(\d+)(mb|m|gb|g)?$/i);
3667
- if (!match)
3668
- throw new UserError(`Invalid size: ${value}`);
3669
- const amount = Number(match[1]);
3670
- const unit = (match[2] ?? "mb").toLowerCase();
3671
- if (unit === "gb" || unit === "g")
3672
- return amount * 1024;
3673
- return amount;
3674
- }
3675
- function buildNetworkPolicy(opts) {
3676
- const allowedDomains = opts.allowedDomain ?? [];
3677
- const allowedCidrs = opts.allowedCidr ?? [];
3678
- const deniedCidrs = opts.deniedCidr ?? [];
3679
- if (!opts.networkPolicy &&
3680
- allowedDomains.length === 0 &&
3681
- allowedCidrs.length === 0 &&
3682
- deniedCidrs.length === 0) {
3683
- return null;
3684
- }
3685
- return {
3686
- mode: opts.networkPolicy ?? "allow-all",
3687
- allowed_domains: allowedDomains,
3688
- allowed_cidrs: allowedCidrs,
3689
- denied_cidrs: deniedCidrs,
3690
- };
3691
- }
3692
- function shellQuote(value) {
3693
- return `'${value.replace(/'/g, "'\\''")}'`;
3694
- }
3695
- // Join argv words back into a shell command, quoting only words that contain
3696
- // whitespace or shell-significant characters. Simple tokens stay bare so
3697
- // `exec <id> ls -la` reads naturally, while `bash -c "cd x && y"` survives.
3698
- function joinCommandWords(words) {
3699
- const SAFE = /^[A-Za-z0-9_./:=@%+-]+$/;
3700
- return words
3701
- .map((word) => word.length > 0 && SAFE.test(word) ? word : shellQuote(word))
3702
- .join(" ");
3703
- }
3704
- function joinUrlPath(basePath, probePath) {
3705
- const probe = probePath.startsWith("/") ? probePath : `/${probePath}`;
3706
- if (!basePath || basePath === "/")
3707
- return probe;
3708
- return `${basePath.replace(/\/$/, "")}${probe}`;
3709
- }
3710
- function sleep(ms) {
3711
- return new Promise((resolve) => setTimeout(resolve, ms));
3712
- }
3713
- // ── shared helpers ─────────────────────────────────────────────────────────
3714
- function pickFreePort() {
3715
- return new Promise((resolve, reject) => {
3716
- const srv = createServer();
3717
- srv.listen(0, "127.0.0.1", () => {
3718
- const addr = srv.address();
3719
- srv.close(() => {
3720
- if (addr && typeof addr === "object")
3721
- resolve(addr.port);
3722
- else
3723
- reject(new Error("Could not pick a free port"));
3724
- });
3725
- });
3726
- });
3727
- }
3728
- function waitForSignal() {
3729
- return new Promise((resolve) => {
3730
- function done() {
3731
- process.off("SIGINT", done);
3732
- process.off("SIGTERM", done);
3733
- resolve();
3734
- }
3735
- process.on("SIGINT", done);
3736
- process.on("SIGTERM", done);
3737
- });
3738
- }
3739
- function openUrl(url) {
3740
- const command = process.platform === "darwin"
3741
- ? "open"
3742
- : process.platform === "win32"
3743
- ? "cmd"
3744
- : "xdg-open";
3745
- const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
3746
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
3747
- child.unref();
3748
- }
3749
- // Sandbox render helpers
3750
- /** Coerce an unknown API field to a display string. */
3751
- function str(v) {
3752
- if (v === null || v === undefined)
3753
- return chalk.dim("-");
3754
- return String(v);
3755
- }
3756
- function stringOrNull(v) {
3757
- return typeof v === "string" && v.trim() ? v.trim() : null;
3758
- }
3759
- function shellArg(value) {
3760
- return `'${value.replace(/'/g, "'\"'\"'")}'`;
3761
- }
3762
- /** Apply semantic color to a sandbox status string. */
3763
- function statusColor(s) {
3764
- const lower = s.toLowerCase().trim();
3765
- if (lower === "running")
3766
- return chalk.green(s);
3767
- if (lower === "paused" || lower === "suspended")
3768
- return chalk.yellow(s);
3769
- if (lower === "stopped" ||
3770
- lower === "error" ||
3771
- lower === "failed" ||
3772
- lower === "destroyed")
3773
- return chalk.red(s);
3774
- if (lower === "starting" || lower === "provisioning" || lower === "pending")
3775
- return chalk.cyan(s);
3776
- return chalk.dim(s);
3777
- }
3778
- /** Render the create-success panel. */
3779
- function renderCreateSuccess(raw, elapsedMs) {
3780
- const sb = (raw ?? {});
3781
- const id = str(sb["id"]);
3782
- printBanner({ subtitle: "Create sandbox" });
3783
- console.log(kvPanel([
3784
- { icon: icon.ok, label: "ID", value: chalk.bold(id) },
3785
- { label: "Name", value: str(sb["name"]) },
3786
- { label: "Status", value: statusColor(str(sb["status"])) },
3787
- ]));
3788
- console.log();
3789
- warnIfExpiringSoon(sb, id);
3790
- console.log(hintBlock("Next", [
3791
- `miosa sandbox show ${id}`,
3792
- `miosa sandbox exec ${id} --command 'python -c print(2+2)'`,
3793
- ]));
3794
- printElapsed(formatDuration(elapsedMs));
3795
- }
3796
- //# sourceMappingURL=sandbox.js.map