@jlongo78/agent-spaces 1.0.0 → 1.0.2

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 (224) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page.js.nft.json +1 -1
  5. package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/(desktop)/admin/users/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/(desktop)/admin/users/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/(desktop)/analytics/page.js.nft.json +1 -1
  9. package/.next/standalone/.next/server/app/(desktop)/analytics/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/(desktop)/network/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/(desktop)/network/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/(desktop)/page.js.nft.json +1 -1
  13. package/.next/standalone/.next/server/app/(desktop)/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/(desktop)/projects/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/(desktop)/projects/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/(desktop)/sessions/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/(desktop)/sessions/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/(desktop)/settings/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/(desktop)/settings/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/(desktop)/terminal/page.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/(desktop)/terminal/page_client-reference-manifest.js +1 -1
  24. package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page_client-reference-manifest.js +1 -1
  26. package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page_client-reference-manifest.js +1 -1
  28. package/.next/standalone/.next/server/app/(desktop)/workspaces/page.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/(desktop)/workspaces/page_client-reference-manifest.js +1 -1
  30. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  31. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  32. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  33. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  35. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  38. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  39. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  40. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  42. package/.next/standalone/.next/server/app/admin/analytics.html +1 -1
  43. package/.next/standalone/.next/server/app/admin/analytics.rsc +2 -2
  44. package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin/analytics/__PAGE__.segment.rsc +2 -2
  45. package/.next/standalone/.next/server/app/admin/analytics.segments/_full.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/admin/analytics.segments/_tree.segment.rsc +2 -2
  47. package/.next/standalone/.next/server/app/admin/users.html +1 -1
  48. package/.next/standalone/.next/server/app/admin/users.rsc +2 -2
  49. package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin/users/__PAGE__.segment.rsc +2 -2
  50. package/.next/standalone/.next/server/app/admin/users.segments/_full.segment.rsc +2 -2
  51. package/.next/standalone/.next/server/app/admin/users.segments/_tree.segment.rsc +2 -2
  52. package/.next/standalone/.next/server/app/analytics.html +1 -1
  53. package/.next/standalone/.next/server/app/analytics.rsc +2 -2
  54. package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap/analytics/__PAGE__.segment.rsc +2 -2
  55. package/.next/standalone/.next/server/app/analytics.segments/_full.segment.rsc +2 -2
  56. package/.next/standalone/.next/server/app/analytics.segments/_tree.segment.rsc +2 -2
  57. package/.next/standalone/.next/server/app/api/admin/analytics/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/admin/os-users/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/admin/users/[id]/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/admin/users/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/analytics/overview/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/api/auth/login/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/auth/me/route.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/api/auth/totp/setup/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/auth/totp/status/route.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/api/auth/totp/verify/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/api/bulk/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/api/chat/route.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/api/claude/usage/route.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/api/config/route.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/api/events/route.js.nft.json +1 -1
  73. package/.next/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/api/folders/route.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/api/network/discovered/route.js.nft.json +1 -1
  76. package/.next/standalone/.next/server/app/api/network/handshake/route.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/api/network/health/route.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/api/network/identity/route.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/api/network/keys/[id]/route.js.nft.json +1 -1
  80. package/.next/standalone/.next/server/app/api/network/keys/route.js.nft.json +1 -1
  81. package/.next/standalone/.next/server/app/api/network/nodes/[id]/route.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/api/network/nodes/check/route.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/api/network/nodes/route.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/api/network/panes/[id]/route.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/api/network/panes/route.js.nft.json +1 -1
  86. package/.next/standalone/.next/server/app/api/network/projects/route.js.nft.json +1 -1
  87. package/.next/standalone/.next/server/app/api/network/proxy/[nodeId]/[...path]/route.js.nft.json +1 -1
  88. package/.next/standalone/.next/server/app/api/network/search/route.js.nft.json +1 -1
  89. package/.next/standalone/.next/server/app/api/network/sessions/[id]/messages/route.js.nft.json +1 -1
  90. package/.next/standalone/.next/server/app/api/network/sessions/[id]/route.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/api/network/sessions/route.js.nft.json +1 -1
  92. package/.next/standalone/.next/server/app/api/network/workspaces/[id]/route.js.nft.json +1 -1
  93. package/.next/standalone/.next/server/app/api/network/workspaces/route.js.nft.json +1 -1
  94. package/.next/standalone/.next/server/app/api/panes/[id]/diff/route.js.nft.json +1 -1
  95. package/.next/standalone/.next/server/app/api/panes/[id]/route.js.nft.json +1 -1
  96. package/.next/standalone/.next/server/app/api/panes/route.js.nft.json +1 -1
  97. package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/api/proxy/models/[modelId]/[...path]/route.js.nft.json +1 -1
  99. package/.next/standalone/.next/server/app/api/proxy/models/[modelId]/status/route.js.nft.json +1 -1
  100. package/.next/standalone/.next/server/app/api/search/route.js.nft.json +1 -1
  101. package/.next/standalone/.next/server/app/api/server-config/route.js.nft.json +1 -1
  102. package/.next/standalone/.next/server/app/api/sessions/[id]/chat/route.js.nft.json +1 -1
  103. package/.next/standalone/.next/server/app/api/sessions/[id]/messages/route.js.nft.json +1 -1
  104. package/.next/standalone/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
  106. package/.next/standalone/.next/server/app/api/sync/route.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/api/tags/route.js.nft.json +1 -1
  108. package/.next/standalone/.next/server/app/api/tier/route.js.nft.json +1 -1
  109. package/.next/standalone/.next/server/app/api/updates/route.js.nft.json +1 -1
  110. package/.next/standalone/.next/server/app/api/whisper/route.js.nft.json +1 -1
  111. package/.next/standalone/.next/server/app/api/wizard/chat/route.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/api/workspaces/[id]/context/[key]/route.js.nft.json +1 -1
  113. package/.next/standalone/.next/server/app/api/workspaces/[id]/context/route.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/[msgId]/route.js.nft.json +1 -1
  115. package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/route.js.nft.json +1 -1
  116. package/.next/standalone/.next/server/app/api/workspaces/[id]/route.js.nft.json +1 -1
  117. package/.next/standalone/.next/server/app/api/workspaces/[id]/sessions/route.js.nft.json +1 -1
  118. package/.next/standalone/.next/server/app/api/workspaces/[id]/todos/[todoId]/route.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/api/workspaces/[id]/todos/route.js.nft.json +1 -1
  120. package/.next/standalone/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  121. package/.next/standalone/.next/server/app/login/page.js.nft.json +1 -1
  122. package/.next/standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  123. package/.next/standalone/.next/server/app/login.html +1 -1
  124. package/.next/standalone/.next/server/app/login.rsc +2 -2
  125. package/.next/standalone/.next/server/app/login.segments/_full.segment.rsc +2 -2
  126. package/.next/standalone/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  127. package/.next/standalone/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  128. package/.next/standalone/.next/server/app/m/page.js.nft.json +1 -1
  129. package/.next/standalone/.next/server/app/m/page_client-reference-manifest.js +1 -1
  130. package/.next/standalone/.next/server/app/m/projects/page.js.nft.json +1 -1
  131. package/.next/standalone/.next/server/app/m/projects/page_client-reference-manifest.js +1 -1
  132. package/.next/standalone/.next/server/app/m/projects.html +1 -1
  133. package/.next/standalone/.next/server/app/m/projects.rsc +2 -2
  134. package/.next/standalone/.next/server/app/m/projects.segments/_full.segment.rsc +2 -2
  135. package/.next/standalone/.next/server/app/m/projects.segments/_tree.segment.rsc +2 -2
  136. package/.next/standalone/.next/server/app/m/projects.segments/m/projects/__PAGE__.segment.rsc +2 -2
  137. package/.next/standalone/.next/server/app/m/sessions/[id]/page.js.nft.json +1 -1
  138. package/.next/standalone/.next/server/app/m/sessions/[id]/page_client-reference-manifest.js +1 -1
  139. package/.next/standalone/.next/server/app/m/sessions/page.js.nft.json +1 -1
  140. package/.next/standalone/.next/server/app/m/sessions/page_client-reference-manifest.js +1 -1
  141. package/.next/standalone/.next/server/app/m/sessions.html +1 -1
  142. package/.next/standalone/.next/server/app/m/sessions.rsc +2 -2
  143. package/.next/standalone/.next/server/app/m/sessions.segments/_full.segment.rsc +2 -2
  144. package/.next/standalone/.next/server/app/m/sessions.segments/_tree.segment.rsc +2 -2
  145. package/.next/standalone/.next/server/app/m/sessions.segments/m/sessions/__PAGE__.segment.rsc +2 -2
  146. package/.next/standalone/.next/server/app/m/settings/page.js.nft.json +1 -1
  147. package/.next/standalone/.next/server/app/m/settings/page_client-reference-manifest.js +1 -1
  148. package/.next/standalone/.next/server/app/m/settings.html +1 -1
  149. package/.next/standalone/.next/server/app/m/settings.rsc +2 -2
  150. package/.next/standalone/.next/server/app/m/settings.segments/_full.segment.rsc +2 -2
  151. package/.next/standalone/.next/server/app/m/settings.segments/_tree.segment.rsc +2 -2
  152. package/.next/standalone/.next/server/app/m/settings.segments/m/settings/__PAGE__.segment.rsc +2 -2
  153. package/.next/standalone/.next/server/app/m/terminal/page.js.nft.json +1 -1
  154. package/.next/standalone/.next/server/app/m/terminal/page_client-reference-manifest.js +1 -1
  155. package/.next/standalone/.next/server/app/m/terminal.html +1 -1
  156. package/.next/standalone/.next/server/app/m/terminal.rsc +2 -2
  157. package/.next/standalone/.next/server/app/m/terminal.segments/_full.segment.rsc +2 -2
  158. package/.next/standalone/.next/server/app/m/terminal.segments/_tree.segment.rsc +2 -2
  159. package/.next/standalone/.next/server/app/m/terminal.segments/m/terminal/__PAGE__.segment.rsc +2 -2
  160. package/.next/standalone/.next/server/app/m.html +1 -1
  161. package/.next/standalone/.next/server/app/m.rsc +2 -2
  162. package/.next/standalone/.next/server/app/m.segments/_full.segment.rsc +2 -2
  163. package/.next/standalone/.next/server/app/m.segments/_tree.segment.rsc +2 -2
  164. package/.next/standalone/.next/server/app/m.segments/m/__PAGE__.segment.rsc +2 -2
  165. package/.next/standalone/.next/server/app/network.html +1 -1
  166. package/.next/standalone/.next/server/app/network.rsc +2 -2
  167. package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap/network/__PAGE__.segment.rsc +2 -2
  168. package/.next/standalone/.next/server/app/network.segments/_full.segment.rsc +2 -2
  169. package/.next/standalone/.next/server/app/network.segments/_tree.segment.rsc +2 -2
  170. package/.next/standalone/.next/server/app/projects.html +1 -1
  171. package/.next/standalone/.next/server/app/projects.rsc +2 -2
  172. package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap/projects/__PAGE__.segment.rsc +2 -2
  173. package/.next/standalone/.next/server/app/projects.segments/_full.segment.rsc +2 -2
  174. package/.next/standalone/.next/server/app/projects.segments/_tree.segment.rsc +2 -2
  175. package/.next/standalone/.next/server/app/sessions.html +1 -1
  176. package/.next/standalone/.next/server/app/sessions.rsc +2 -2
  177. package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap/sessions/__PAGE__.segment.rsc +2 -2
  178. package/.next/standalone/.next/server/app/sessions.segments/_full.segment.rsc +2 -2
  179. package/.next/standalone/.next/server/app/sessions.segments/_tree.segment.rsc +2 -2
  180. package/.next/standalone/.next/server/app/settings.html +1 -1
  181. package/.next/standalone/.next/server/app/settings.rsc +2 -2
  182. package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap/settings/__PAGE__.segment.rsc +2 -2
  183. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +2 -2
  184. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  185. package/.next/standalone/.next/server/app/terminal.html +1 -1
  186. package/.next/standalone/.next/server/app/terminal.rsc +2 -2
  187. package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap/terminal/__PAGE__.segment.rsc +2 -2
  188. package/.next/standalone/.next/server/app/terminal.segments/_full.segment.rsc +2 -2
  189. package/.next/standalone/.next/server/app/terminal.segments/_tree.segment.rsc +2 -2
  190. package/.next/standalone/.next/server/app/vr/page.js.nft.json +1 -1
  191. package/.next/standalone/.next/server/app/vr/page_client-reference-manifest.js +1 -1
  192. package/.next/standalone/.next/server/app/vr.html +1 -1
  193. package/.next/standalone/.next/server/app/vr.rsc +2 -2
  194. package/.next/standalone/.next/server/app/vr.segments/_full.segment.rsc +2 -2
  195. package/.next/standalone/.next/server/app/vr.segments/_tree.segment.rsc +2 -2
  196. package/.next/standalone/.next/server/app/vr.segments/vr/__PAGE__.segment.rsc +2 -2
  197. package/.next/standalone/.next/server/app/workspaces.html +1 -1
  198. package/.next/standalone/.next/server/app/workspaces.rsc +2 -2
  199. package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap/workspaces/__PAGE__.segment.rsc +2 -2
  200. package/.next/standalone/.next/server/app/workspaces.segments/_full.segment.rsc +2 -2
  201. package/.next/standalone/.next/server/app/workspaces.segments/_tree.segment.rsc +2 -2
  202. package/.next/standalone/.next/server/chunks/_1-inpovedul0t._.js +1 -1
  203. package/.next/standalone/.next/server/chunks/ssr/_1yl1yt70hdi7k._.js +1 -1
  204. package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__00lhc9vpmf8oy._.js +1 -1
  205. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  206. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  207. package/.next/standalone/.next/server/pages/404.html +1 -1
  208. package/.next/standalone/.next/server/pages/500.html +1 -1
  209. package/.next/standalone/.next/server/preview-props.json +3 -3
  210. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  211. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  212. package/.next/standalone/.next/static/chunks/07t-bv_pwp3nh.css +3 -0
  213. package/.next/standalone/package.json +6 -6
  214. package/README.md +29 -3
  215. package/bin/lib/cli-flags.js +2 -0
  216. package/bin/lib/remote.js +124 -15
  217. package/bin/lib/runtime/claude-sessions.js +108 -13
  218. package/bin/lib/runtime/index.js +1 -1
  219. package/bin/spaces.js +330 -57
  220. package/package.json +6 -6
  221. package/.next/standalone/.next/static/chunks/3bk2w5qzafz7n.css +0 -3
  222. /package/.next/standalone/.next/static/{_RTyuAfH-40lIlwVs2azh → B50xUMpZMRp2r4CxAPPFu}/_buildManifest.js +0 -0
  223. /package/.next/standalone/.next/static/{_RTyuAfH-40lIlwVs2azh → B50xUMpZMRp2r4CxAPPFu}/_clientMiddlewareManifest.js +0 -0
  224. /package/.next/standalone/.next/static/{_RTyuAfH-40lIlwVs2azh → B50xUMpZMRp2r4CxAPPFu}/_ssgManifest.js +0 -0
package/bin/spaces.js CHANGED
@@ -198,6 +198,9 @@ if (cliFlags.help) {
198
198
  update it when it is older than this machine's
199
199
  spaces --remote you@host --yes
200
200
  The same, installing or updating without asking
201
+ spaces open --remote you@host
202
+ The web app for Spaces on another machine, in your
203
+ browser, through an SSH tunnel (Ctrl+C closes it)
201
204
  spaces start Start the server in the background, if it is not running
202
205
  spaces open Open the web app in your browser, signed in
203
206
  spaces signout Sign out every browser and terminal client
@@ -244,8 +247,8 @@ if (cliFlags.help) {
244
247
  const subcommand = args[0];
245
248
  // `--remote` means "the terminal client, over there". With any other command
246
249
  // it would be silently ignored and the command would run here instead.
247
- if (cliFlags.remote && subcommand && subcommand !== 'tui' && !subcommand.startsWith('-')) {
248
- console.error(` --remote works with "spaces" and "spaces tui", not "spaces ${subcommand}".`);
250
+ if (cliFlags.remote && subcommand && subcommand !== 'tui' && subcommand !== 'open' && !subcommand.startsWith('-')) {
251
+ console.error(` --remote works with "spaces", "spaces tui" and "spaces open", not "spaces ${subcommand}".`);
249
252
  console.error(` To run that on the other machine: ssh ${cliFlags.remote} spaces ${subcommand}`);
250
253
  process.exit(2);
251
254
  }
@@ -298,7 +301,7 @@ if (subcommand === 'service') {
298
301
  }
299
302
 
300
303
  // ─── Route tui to the platform terminal-client binary ─────
301
- if (subcommand === 'tui' || cliFlags.remote) {
304
+ if (subcommand === 'tui' || (cliFlags.remote && subcommand !== 'open')) {
302
305
  // Explicit `spaces tui` never silently opens a browser: the user asked
303
306
  // for the terminal client by name, so a failure is reported as one. Bare
304
307
  // `spaces --remote host` asks for the same thing.
@@ -322,7 +325,11 @@ if (subcommand === 'signout') {
322
325
  }
323
326
 
324
327
  // ─── Open the web app, signed in / setup wizard / server ──
325
- if (subcommand === 'open') {
328
+ if (subcommand === 'open' && cliFlags.remote) {
329
+ // Waits for the tunnel on the event loop: part of this chain for the
330
+ // same reason as `open` below.
331
+ openRemote(cliFlags.remote);
332
+ } else if (subcommand === 'open') {
326
333
  // Exits by itself: at once, or on Linux once the timer that removes the
327
334
  // sign-in redirect page fires. Part of this chain so it no longer falls
328
335
  // through to "Unknown command" while that timer is pending.
@@ -591,6 +598,19 @@ function startDetached(startArgs) {
591
598
  const session = desktopSessionToken();
592
599
  if (session) console.log(`SPACES_TOKEN=${session}`);
593
600
  }
601
+ // `spaces open --remote` asks for this: a browser sign-in code, the kind
602
+ // `spaces open` puts in its link (one use, two minutes), and the path the
603
+ // web app is served under. The secret itself never leaves this machine.
604
+ if (!multiuser && startArgs.includes('--print-login')) {
605
+ if (!listenerIsMine(port)) refuseForeignListener(port);
606
+ const secret = require('./lib/desktop-auth').readSecret(SPACES_DIR);
607
+ if (secret) {
608
+ const { createLoginCodes } = require('./lib/desktop-auth');
609
+ console.log(`SPACES_LOGIN=${createLoginCodes(() => secret).mint()}`);
610
+ const basePath = process.env.SPACES_BASE_PATH || readSavedConfig().basePath || '';
611
+ if (basePath) console.log(`SPACES_BASE_PATH=${basePath}`);
612
+ }
613
+ }
594
614
  process.exit(0);
595
615
  }
596
616
 
@@ -783,16 +803,55 @@ function launchRemote(dest, tuiArgs) {
783
803
  process.exit(1);
784
804
  }
785
805
 
806
+ const { start: r, localPort, closeTunnel } = connectRemote(dest);
807
+
808
+ // 3. The client, through the tunnel. The remote trusts it as local,
809
+ // because over there that is what it is.
810
+ const { token, rest } = splitToken(clientArgs);
811
+ const env = { ...process.env };
812
+ // The remote's own session token, handed back over SSH by
813
+ // `spaces start --print-token`; the remote's secret never leaves it.
814
+ const remoteToken = token || remote.parseStartToken(r.stdout);
815
+ if (!remoteToken) {
816
+ closeTunnel();
817
+ console.error(` Spaces on ${dest} did not hand back a sign-in token. It may be an older`);
818
+ console.error(` version; update it there (ssh ${dest} 'npm install -g @jlongo78/agent-spaces').`);
819
+ process.exit(1);
820
+ }
821
+ env.SPACES_TOKEN = remoteToken;
822
+ const status = runTuiBinary(tuiBin, buildTuiArgs(rest, localPort), { env });
823
+ closeTunnel();
824
+ console.log(` Spaces is still running on ${dest} — "ssh ${dest} spaces stop" shuts it down.`);
825
+ process.exit(status);
826
+ }
827
+
828
+ /**
829
+ * What `spaces --remote` and `spaces open --remote` share: Spaces running on
830
+ * `dest` (installed or updated first, with the user's say-so) and an SSH
831
+ * tunnel to it from a local port. `login` also asks for a browser sign-in
832
+ * code; `localPort` is tried before any free port. Returns what `spaces
833
+ * start` printed over there, the local end of the tunnel, and ways to ask
834
+ * after it and close it. Exits, with the reason, when any step fails.
835
+ */
836
+ function connectRemote(dest, { login = false, localPort: wantedPort = null } = {}) {
837
+ const remote = require('./lib/remote');
838
+ const { tailLines } = require('./spaces-tui');
839
+ const { spawnSync } = require('child_process');
840
+
786
841
  // 1. Make sure Spaces is running over there, and learn its port —
787
842
  // installing or updating it first, with the user's say-so.
788
843
  console.log(` Connecting to ${dest}...`);
789
- ensureRemoteInstall(dest);
790
- const runStart = (loginShell) => spawnSync('ssh', remote.sshStartArgs(dest, { loginShell }), {
844
+ const prepared = ensureRemoteInstall(dest, { login });
845
+ const runStart = (loginShell) => spawnSync('ssh', sshWithKey(remote.sshStartArgs(dest, { loginShell, login })), {
791
846
  // No stdin, as for the probe in ensureRemoteInstall: nothing reads it.
792
847
  stdio: ['ignore', 'pipe', 'pipe'],
793
848
  encoding: 'utf-8',
794
849
  });
795
- let r = runStart(false);
850
+ // An install already started it. Otherwise start it, through the login
851
+ // shell first when the probe showed the remote has one: one SSH session
852
+ // rather than two, and each can mean another password prompt.
853
+ if (!prepared.started) console.log(` Starting Spaces on ${dest}...`);
854
+ let r = prepared.started || runStart(prepared.shell);
796
855
  if (r.error) {
797
856
  console.error(r.error.code === 'ENOENT'
798
857
  ? ' --remote needs the ssh client, and none was found on the PATH.'
@@ -800,8 +859,8 @@ function launchRemote(dest, tuiArgs) {
800
859
  process.exit(1);
801
860
  }
802
861
  let remotePort = r.status === 0 ? remote.parseStartOutput(r.stdout) : null;
803
- if (remotePort === null && remote.classifyStartFailure(r.status, r.stderr) === 'missing') {
804
- r = runStart(true);
862
+ if (remotePort === null && !prepared.started && remote.classifyStartFailure(r.status, r.stderr) === 'missing') {
863
+ r = runStart(!prepared.shell);
805
864
  remotePort = r.status === 0 ? remote.parseStartOutput(r.stdout) : null;
806
865
  }
807
866
  const relay = (text) => {
@@ -816,6 +875,12 @@ function launchRemote(dest, tuiArgs) {
816
875
  process.exit(1);
817
876
  }
818
877
  const kind = remote.classifyStartFailure(r.status, r.stderr);
878
+ if (login && /Unknown option: --print-login/.test(`${r.stdout}\n${r.stderr}`)) {
879
+ console.error(` Spaces on ${dest} is too old to sign a browser in; that needs 1.0.1 or newer there.`);
880
+ console.error(` Update it there: ssh ${dest} 'npm install -g @jlongo78/agent-spaces'`);
881
+ console.error(' (spaces --remote offers to, once this machine has a newer version than that one).');
882
+ process.exit(1);
883
+ }
819
884
  if (kind === 'missing') {
820
885
  console.error(remote.installHint(dest));
821
886
  } else if (kind === 'ssh') {
@@ -828,8 +893,8 @@ function launchRemote(dest, tuiArgs) {
828
893
  process.exit(1);
829
894
  }
830
895
 
831
- // 2. Tunnel a free local port to it.
832
- const localPort = freeLocalPort();
896
+ // 2. Tunnel a local port to it.
897
+ const localPort = (wantedPort && localPortIsFree(wantedPort)) ? wantedPort : freeLocalPort();
833
898
  if (!localPort) {
834
899
  console.error(' Could not find a free local port for the tunnel.');
835
900
  process.exit(1);
@@ -837,45 +902,113 @@ function launchRemote(dest, tuiArgs) {
837
902
  fs.mkdirSync(SPACES_DIR, { recursive: true });
838
903
  const logPath = path.join(SPACES_DIR, 'remote-tunnel.log');
839
904
  const log = fs.openSync(logPath, 'w');
840
- const tunnel = spawn('ssh', remote.sshTunnelArgs(dest, localPort, remotePort), {
905
+ // Its own SSH session, which may ask for the password again: say so,
906
+ // or a third prompt arrives with no explanation after two that looked
907
+ // like the whole sign-in.
908
+ console.log(` Opening an SSH tunnel to Spaces on ${dest}...`);
909
+ const tunnel = spawn('ssh', sshWithKey(remote.sshTunnelArgs(dest, localPort, remotePort)), {
841
910
  stdio: ['ignore', log, log],
842
911
  });
843
912
  fs.closeSync(log);
844
913
  const closeTunnel = () => { try { tunnel.kill(); } catch { /* already gone */ } };
845
- const outcome = waitForServer({
846
- timeoutMs: 30000,
847
- probe: () => portIsListening(localPort, 1000),
848
- alive: () => { try { process.kill(tunnel.pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } },
849
- });
850
- if (outcome !== 'up') {
914
+ // Waits on ssh's own log, starting no process meanwhile: on Windows each
915
+ // process that exits resets the shared console, and that garbles a
916
+ // password being typed into the tunnel's prompt. Minutes to type one;
917
+ // thirty seconds for the forward once signed in.
918
+ const readLog = () => { try { return fs.readFileSync(logPath, 'utf-8'); } catch { return ''; } };
919
+ const tunnelAlive = () => { try { process.kill(tunnel.pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } };
920
+ const nap = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
921
+ let deadline = Date.now() + 5 * 60 * 1000;
922
+ let signedIn = false;
923
+ let up = false;
924
+ for (;;) {
925
+ const text = readLog();
926
+ if (remote.tunnelReady(text, localPort)) { up = true; break; }
927
+ if (!signedIn && remote.tunnelAuthenticated(text)) {
928
+ signedIn = true;
929
+ deadline = Math.min(deadline, Date.now() + 30000);
930
+ }
931
+ if (!tunnelAlive() || Date.now() > deadline) break;
932
+ nap(100);
933
+ }
934
+ if (!up) {
851
935
  closeTunnel();
852
936
  console.error(` Could not open the SSH tunnel to ${dest}:`);
853
- let output = '';
854
- try { output = fs.readFileSync(logPath, 'utf-8'); } catch { /* nothing logged */ }
855
- relay(output);
937
+ relay(remote.withoutDebug(readLog()));
856
938
  process.exit(1);
857
939
  }
940
+ return { start: r, localPort, tunnel, closeTunnel, tunnelAlive };
941
+ }
858
942
 
859
- // 3. The client, through the tunnel. The remote trusts it as local,
860
- // because over there that is what it is.
861
- const { token, rest } = splitToken(clientArgs);
862
- const env = { ...process.env };
863
- // The remote's own session token, handed back over SSH by
864
- // `spaces start --print-token`; the remote's secret never leaves it.
865
- const remoteToken = token || remote.parseStartToken(r.stdout);
866
- if (!remoteToken) {
943
+ /** True when nothing on this machine listens on 127.0.0.1:`port`. */
944
+ function localPortIsFree(port) {
945
+ const { spawnSync } = require('child_process');
946
+ const r = spawnSync(process.execPath, ['-e', `
947
+ const s = require('net').createServer();
948
+ s.on('error', () => process.exit(1));
949
+ s.listen(${Number(port)}, '127.0.0.1', () => s.close(() => process.exit(0)));
950
+ `], { stdio: 'ignore', timeout: 5000 });
951
+ return r.status === 0;
952
+ }
953
+
954
+ /**
955
+ * `spaces open --remote <dest>`: the web app for Spaces on another machine.
956
+ * Connects as `spaces --remote` does, then signs the browser in through
957
+ * the tunnel with a one-time code made over there, and keeps the tunnel up
958
+ * until Ctrl+C.
959
+ *
960
+ * 127.0.0.1 rather than localhost: browsers keep cookies per host name, not
961
+ * per port, so a remote signed in at localhost would replace the sign-in to
962
+ * this machine's own Spaces at localhost:3457, and back again.
963
+ */
964
+ function openRemote(dest) {
965
+ const remote = require('./lib/remote');
966
+ try {
967
+ remote.validateDestination(dest);
968
+ } catch (e) {
969
+ console.error(` ${e.message}`);
970
+ process.exit(2);
971
+ }
972
+ if (cliFlags.port || cliFlags.basePath) {
973
+ console.error(' --port and --base-path describe this machine\'s server, not the remote\'s.');
974
+ process.exit(2);
975
+ }
976
+ const { start, localPort, closeTunnel, tunnel } = connectRemote(dest, {
977
+ login: true,
978
+ localPort: remote.preferredLocalPort(dest),
979
+ });
980
+ const code = remote.parseStartLogin(start.stdout);
981
+ if (!code) {
867
982
  closeTunnel();
868
- console.error(` Spaces on ${dest} did not hand back a sign-in token. It may be an older`);
869
- console.error(` version; update it there (ssh ${dest} 'npm install -g @jlongo78/agent-spaces').`);
983
+ console.error(` Spaces on ${dest} did not hand back a browser sign-in. It needs Spaces 1.0.1 or`);
984
+ console.error(` newer there; "spaces --remote ${dest}" offers to update it.`);
870
985
  process.exit(1);
871
986
  }
872
- env.SPACES_TOKEN = remoteToken;
873
- const status = runTuiBinary(tuiBin, buildTuiArgs(rest, localPort), { env });
874
- closeTunnel();
875
- console.log(` Spaces is still running on ${dest} — "ssh ${dest} spaces stop" shuts it down.`);
876
- process.exit(status);
987
+ const home = `http://127.0.0.1:${localPort}${remote.parseStartBasePath(start.stdout)}`;
988
+ const url = `${home}/auth/desktop?code=${code}`;
989
+ if (!openSignInLink(url)) {
990
+ console.log(' Open this link in your browser within two minutes (it works once):');
991
+ console.log(` ${url}`);
992
+ }
993
+ console.log(` Spaces on ${dest} is at ${home} while this runs. Ctrl+C closes the tunnel.`);
994
+ let stopping = false;
995
+ const stop = () => {
996
+ stopping = true;
997
+ closeTunnel();
998
+ console.log(`\n Closed the tunnel to ${dest}; Spaces keeps running there.`);
999
+ process.exit(0);
1000
+ };
1001
+ process.on('SIGINT', stop);
1002
+ process.on('SIGTERM', stop);
1003
+ // Ctrl+C reaches ssh too, so it may exit before the handler above runs.
1004
+ tunnel.on('exit', () => {
1005
+ if (stopping) return;
1006
+ console.error(` The SSH tunnel to ${dest} closed; run this again to reconnect.`);
1007
+ process.exit(1);
1008
+ });
877
1009
  }
878
1010
 
1011
+
879
1012
  // ─── TLS certificate ────────────────────────────────────────
880
1013
  // Uses a real cert if configured in server.json (tlsCert, tlsKey, optional
881
1014
  // tlsCa for the full chain), otherwise generates a self-signed cert so
@@ -1678,29 +1811,171 @@ async function printStatus(porcelain) {
1678
1811
  process.exit(0);
1679
1812
  }
1680
1813
 
1814
+ /**
1815
+ * `args` for an ssh that `--remote` runs, signing in with SPACES_SSH_KEY
1816
+ * when it names a key. Only tests set it: everyone else signs in with the
1817
+ * keys plain `ssh` uses, which is what the key offer below sets up.
1818
+ */
1819
+ function sshWithKey(args) {
1820
+ const key = process.env.SPACES_SSH_KEY;
1821
+ return key ? ['-i', key, ...args] : args;
1822
+ }
1823
+
1824
+ /**
1825
+ * Where the destinations whose key offer was declined are kept. A function,
1826
+ * not a constant: `--remote` runs while this file is still being read, and
1827
+ * a constant declared down here would not exist yet.
1828
+ */
1829
+ function keyOfferDeclinedPath() {
1830
+ return path.join(SPACES_DIR, 'remote-key-declined.json');
1831
+ }
1832
+
1833
+ function keyOfferDeclined(dest) {
1834
+ try { return JSON.parse(fs.readFileSync(keyOfferDeclinedPath(), 'utf-8')).includes(dest); } catch { return false; }
1835
+ }
1836
+
1837
+ function declineKeyOffer(dest) {
1838
+ let list = [];
1839
+ try { list = JSON.parse(fs.readFileSync(keyOfferDeclinedPath(), 'utf-8')); } catch { /* none yet */ }
1840
+ if (!Array.isArray(list)) list = [];
1841
+ if (!list.includes(dest)) list.push(dest);
1842
+ try {
1843
+ fs.mkdirSync(SPACES_DIR, { recursive: true });
1844
+ fs.writeFileSync(keyOfferDeclinedPath(), JSON.stringify(list, null, 2));
1845
+ } catch { /* asked again next time, then */ }
1846
+ }
1847
+
1848
+ /**
1849
+ * The key to sign in with: SPACES_SSH_KEY, else the first of ssh's default
1850
+ * keys that has a public half, else a new ~/.ssh/id_ed25519. A new key has
1851
+ * no passphrase (the account's file permissions protect it, as with any
1852
+ * key ssh-keygen makes by default); on Windows its permissions are narrowed
1853
+ * to this user, since OpenSSH there ignores a private key anyone else can
1854
+ * read. Null, with the reason printed, when no key could be had.
1855
+ */
1856
+ function localSshKey() {
1857
+ const remote = require('./lib/remote');
1858
+ const { spawnSync } = require('child_process');
1859
+ const sshDir = path.join(os.homedir(), '.ssh');
1860
+ const wanted = process.env.SPACES_SSH_KEY;
1861
+ if (!wanted) {
1862
+ for (const name of remote.DEFAULT_KEYS) {
1863
+ const key = path.join(sshDir, name);
1864
+ if (fs.existsSync(key) && fs.existsSync(`${key}.pub`)) return { key, created: false };
1865
+ }
1866
+ }
1867
+ const key = wanted || path.join(sshDir, 'id_ed25519');
1868
+ if (fs.existsSync(key) && fs.existsSync(`${key}.pub`)) return { key, created: false };
1869
+ if (fs.existsSync(key)) {
1870
+ console.error(` ${key} exists without its .pub; not overwriting it.`);
1871
+ return null;
1872
+ }
1873
+ fs.mkdirSync(path.dirname(key), { recursive: true, mode: 0o700 });
1874
+ const comment = `${os.userInfo().username}@${os.hostname()}`.replace(/\s+/g, '-');
1875
+ const r = spawnSync('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', comment, '-f', key], {
1876
+ stdio: ['ignore', 'ignore', 'pipe'], encoding: 'utf-8',
1877
+ });
1878
+ if (r.error || r.status !== 0 || !fs.existsSync(`${key}.pub`)) {
1879
+ console.error(` Could not create an SSH key with ssh-keygen: ${r.error ? r.error.message : (r.stderr || '').trim()}`);
1880
+ return null;
1881
+ }
1882
+ if (process.platform === 'win32') {
1883
+ spawnSync('icacls', [key, '/inheritance:r', '/grant:r', `${os.userInfo().username}:F`], { stdio: 'ignore' });
1884
+ }
1885
+ return { key, created: true };
1886
+ }
1887
+
1888
+ /**
1889
+ * When `dest` wants a password: offers to set up key sign-in, once per
1890
+ * destination (a "no" is remembered). Adds this machine's public key to
1891
+ * ~/.ssh/authorized_keys there — the last password prompt — then checks
1892
+ * the key works. True when it does.
1893
+ */
1894
+ function offerSshKey(dest) {
1895
+ const remote = require('./lib/remote');
1896
+ const { ask } = require('./lib/prompt');
1897
+ const { spawnSync } = require('child_process');
1898
+ if (keyOfferDeclined(dest)) return false;
1899
+
1900
+ console.log(` ${dest} asks for your password on every connection, and --remote makes three.`);
1901
+ if (ask(' Set up an SSH key so it stops asking? [Y/n] ', ['y', 'n'], 'y') !== 'y') {
1902
+ declineKeyOffer(dest);
1903
+ console.log(` Not asking again for ${dest}; delete ${keyOfferDeclinedPath()} to be asked again.`);
1904
+ return false;
1905
+ }
1906
+
1907
+ const local = localSshKey();
1908
+ if (!local) return false;
1909
+ let pub = null;
1910
+ try { pub = remote.publicKeyLine(fs.readFileSync(`${local.key}.pub`, 'utf-8')); } catch { /* reported below */ }
1911
+ if (!pub) {
1912
+ console.error(` ${local.key}.pub is not a public key ssh can use.`);
1913
+ return false;
1914
+ }
1915
+ if (local.created) console.log(` Created ${local.key} (and .pub).`);
1916
+ console.log(` Adding it to ~/.ssh/authorized_keys on ${dest}. Your password, one last time:`);
1917
+ const add = spawnSync('ssh', sshWithKey(remote.sshAuthorizeKeyArgs(dest)), {
1918
+ input: `${pub}\n`, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8',
1919
+ });
1920
+ if (add.error || add.status !== 0) {
1921
+ console.error(` Could not add the key on ${dest}:`);
1922
+ for (const line of require('./spaces-tui').tailLines(`${add.stderr || ''}`, 4)) console.error(` ${line.trim()}`);
1923
+ return false;
1924
+ }
1925
+
1926
+ const check = spawnSync('ssh', sshWithKey(['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15', '--', dest, 'true']), {
1927
+ stdio: ['ignore', 'ignore', 'pipe'], encoding: 'utf-8',
1928
+ });
1929
+ if (check.status !== 0) {
1930
+ console.error(` The key is on ${dest}, but signing in with it still failed.`);
1931
+ if (process.platform === 'win32') {
1932
+ console.error(' OpenSSH on Windows ignores a private key others can read; to make it yours alone:');
1933
+ console.error(` icacls "${local.key}" /inheritance:r /grant:r "%USERNAME%":F`);
1934
+ }
1935
+ return false;
1936
+ }
1937
+ console.log(` Done: ${dest} now signs you in with ${path.basename(local.key)}, here and with plain ssh.`);
1938
+ return true;
1939
+ }
1940
+
1681
1941
  /**
1682
1942
  * Before `--remote` connects: installs Spaces on `dest` when it is missing,
1683
1943
  * or updates it when it is older than this machine's — asking first unless
1684
1944
  * `--yes`, and saying how many agents an update would stop. A remote that
1685
1945
  * cannot be asked (an ssh failure, a shell that is not sh-like) is left to
1686
1946
  * the connection step, which reports it as it always has.
1947
+ *
1948
+ * Returns `shell` (the remote answered through an sh-like login shell) and
1949
+ * `started` (the install's ssh result, which already started the server),
1950
+ * so the connection step does not open sessions it needs no longer.
1687
1951
  */
1688
- function ensureRemoteInstall(dest) {
1952
+ function ensureRemoteInstall(dest, { login = false } = {}) {
1689
1953
  const remote = require('./lib/remote');
1690
1954
  const { spawnSync } = require('child_process');
1691
1955
  const local = require('../package.json').version;
1692
- // No stdin: neither command reads it, and a session holding the terminal
1693
- // open can outlive its command. ssh asks for passwords on /dev/tty anyway.
1694
- const probe = spawnSync('ssh', remote.sshProbeArgs(dest), {
1695
- stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8',
1696
- });
1697
- if (probe.error || probe.status === 255) return;
1698
- const plan = remote.planRemote(remote.parseProbe(probe.stdout), local);
1699
1956
  // isatty, not process.stdin.isTTY: touching process.stdin hands the
1700
- // terminal to Node's own reader, and the question below would never be
1957
+ // terminal to Node's own reader, and the questions below would never be
1701
1958
  // answered (see lib/prompt.js).
1702
1959
  const interactive = require('tty').isatty(0) && Boolean(process.stdout.isTTY);
1703
1960
  const { ask } = require('./lib/prompt');
1961
+ // No stdin: neither command reads it, and a session holding the terminal
1962
+ // open can outlive its command. ssh asks for passwords on the console.
1963
+ const runProbe = (batch) => spawnSync('ssh', sshWithKey(remote.sshProbeArgs(dest, { batch })), {
1964
+ stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8',
1965
+ });
1966
+ // First without prompts: a key that works needs nothing more. A server
1967
+ // that wants a password gets the offer of a key instead; anything else
1968
+ // (an unknown host key, say) goes to ssh's own questions, as before.
1969
+ let probe = runProbe(true);
1970
+ if (!probe.error && probe.status === 255) {
1971
+ const why = remote.classifySshFailure(probe.stderr);
1972
+ if (why === 'password' && interactive && offerSshKey(dest)) probe = runProbe(true);
1973
+ if (probe.status === 255) probe = runProbe(false);
1974
+ }
1975
+ if (probe.error || probe.status === 255) return { shell: false, started: null };
1976
+ const probed = remote.parseProbe(probe.stdout);
1977
+ const plan = remote.planRemote(probed, local);
1978
+ const done = { shell: probed !== null, started: null };
1704
1979
 
1705
1980
  let spec;
1706
1981
  try {
@@ -1711,12 +1986,12 @@ function ensureRemoteInstall(dest) {
1711
1986
  }
1712
1987
  const install = ({ restart }) => {
1713
1988
  console.log(` Installing ${spec} on ${dest}. Compiling the terminal library can take a minute...`);
1714
- const r = spawnSync('ssh', remote.sshInstallArgs(dest, spec, { restart }), {
1989
+ const r = spawnSync('ssh', sshWithKey(remote.sshInstallArgs(dest, spec, { restart, login })), {
1715
1990
  stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8',
1716
1991
  });
1717
1992
  if (r.status === 0) {
1718
1993
  console.log(` Spaces ${local} is installed on ${dest}.`);
1719
- return;
1994
+ return r;
1720
1995
  }
1721
1996
  const output = `${r.stdout || ''}\n${r.stderr || ''}`;
1722
1997
  for (const line of require('./spaces-tui').tailLines(output, 8)) console.error(` ${line.trim()}`);
@@ -1726,11 +2001,11 @@ function ensureRemoteInstall(dest) {
1726
2001
 
1727
2002
  switch (plan.action) {
1728
2003
  case 'connect':
1729
- return;
2004
+ return done;
1730
2005
  case 'newer':
1731
2006
  console.log(` Spaces on ${dest} is ${plan.remote}, newer than this machine's ${local}.`);
1732
2007
  console.log(` To update this machine: npm install -g ${remote.PACKAGE}`);
1733
- return;
2008
+ return done;
1734
2009
  case 'no-node':
1735
2010
  console.error(` Spaces is not installed on ${dest}, and neither is Node.js, which it needs (20.9 or newer).`);
1736
2011
  console.error(' Install Node.js there first; nvm is the easiest way: https://github.com/nvm-sh/nvm');
@@ -1752,20 +2027,19 @@ function ensureRemoteInstall(dest) {
1752
2027
  process.exit(1);
1753
2028
  }
1754
2029
  }
1755
- install({ restart: false });
1756
- return;
2030
+ return { ...done, started: install({ restart: false }) };
1757
2031
  }
1758
2032
  case 'update': {
1759
2033
  const stops = plan.running === false ? 0 : plan.liveAgents;
1760
2034
  if (!cliFlags.yes) {
1761
2035
  if (!interactive) {
1762
2036
  console.log(` Spaces on ${dest} is ${plan.from}, older than this machine's ${local}; run with --yes to update it.`);
1763
- return;
2037
+ return done;
1764
2038
  }
1765
2039
  if (stops === 0) {
1766
2040
  const restarting = plan.running ? ' It restarts Spaces there; no agents are running.' : '';
1767
2041
  if (ask(` Spaces on ${dest} is ${plan.from}, older than this machine's ${local}. Update it?${restarting} [Y/n] `, ['y', 'n'], 'y') !== 'y') {
1768
- return;
2042
+ return done;
1769
2043
  }
1770
2044
  } else {
1771
2045
  console.log(` Spaces on ${dest} is ${plan.from}, older than this machine's ${local}.`);
@@ -1774,14 +2048,13 @@ function ensureRemoteInstall(dest) {
1774
2048
  : ` Updating restarts Spaces on ${dest} and stops its ${stops} running ${stops === 1 ? 'agent' : 'agents'}.`);
1775
2049
  const choice = ask(` [u]pdate now, [c]onnect to ${plan.from} as it is, or [q]uit? [u/c/q] `, ['u', 'c', 'q'], null);
1776
2050
  if (choice === 'q') process.exit(0);
1777
- if (choice !== 'u') return;
2051
+ if (choice !== 'u') return done;
1778
2052
  }
1779
2053
  }
1780
- install({ restart: plan.running !== false });
1781
- return;
2054
+ return { ...done, started: install({ restart: plan.running !== false }) };
1782
2055
  }
1783
2056
  default:
1784
- return;
2057
+ return done;
1785
2058
  }
1786
2059
  }
1787
2060
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jlongo78/agent-spaces",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Agent workspace manager for Claude, Codex, Gemini, and more",
5
5
  "bin": {
6
6
  "spaces": "./bin/spaces.js",
@@ -122,11 +122,11 @@
122
122
  "xterm": "^5.3.0"
123
123
  },
124
124
  "optionalDependencies": {
125
- "@jlongo78/agent-spaces-tui-darwin-arm64": "1.0.0",
126
- "@jlongo78/agent-spaces-tui-darwin-x64": "1.0.0",
127
- "@jlongo78/agent-spaces-tui-linux-arm64": "1.0.0",
128
- "@jlongo78/agent-spaces-tui-linux-x64": "1.0.0",
129
- "@jlongo78/agent-spaces-tui-win32-x64": "1.0.0",
125
+ "@jlongo78/agent-spaces-tui-darwin-arm64": "1.0.2",
126
+ "@jlongo78/agent-spaces-tui-darwin-x64": "1.0.2",
127
+ "@jlongo78/agent-spaces-tui-linux-arm64": "1.0.2",
128
+ "@jlongo78/agent-spaces-tui-linux-x64": "1.0.2",
129
+ "@jlongo78/agent-spaces-tui-win32-x64": "1.0.2",
130
130
  "otpauth": "^9.5.0",
131
131
  "qrcode": "^1.5.4"
132
132
  }