@jlongo78/agent-spaces 1.0.0 → 1.0.1

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 (221) 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/{3bk2w5qzafz7n.css → 1a5d9qolqzld9.css} +1 -1
  213. package/.next/standalone/package.json +6 -6
  214. package/README.md +19 -0
  215. package/bin/lib/cli-flags.js +2 -0
  216. package/bin/lib/remote.js +124 -15
  217. package/bin/spaces.js +330 -57
  218. package/package.json +6 -6
  219. /package/.next/standalone/.next/static/{_RTyuAfH-40lIlwVs2azh → FHvIomYPw2stnbhS8PRRv}/_buildManifest.js +0 -0
  220. /package/.next/standalone/.next/static/{_RTyuAfH-40lIlwVs2azh → FHvIomYPw2stnbhS8PRRv}/_clientMiddlewareManifest.js +0 -0
  221. /package/.next/standalone/.next/static/{_RTyuAfH-40lIlwVs2azh → FHvIomYPw2stnbhS8PRRv}/_ssgManifest.js +0 -0
package/bin/lib/remote.js CHANGED
@@ -52,18 +52,20 @@ function validateDestination(dest) {
52
52
  */
53
53
  /**
54
54
  * `command` run through the remote user's login shell with nvm loaded — the
55
- * PATH an interactive session would have. `command` is spliced inside single
56
- * quotes, so it must never contain one; every caller passes a constant or
57
- * something built only from validated parts.
55
+ * PATH an interactive session would have — and ~/.local/bin first on it,
56
+ * where `--remote` installs Spaces when npm's own global folder belongs to
57
+ * root. `command` is spliced inside single quotes, so it must never contain
58
+ * one; every caller passes a constant or something built only from
59
+ * validated parts.
58
60
  */
59
61
  function inLoginShell(command) {
60
62
  if (command.includes("'")) throw new Error('a remote command cannot contain a single quote');
61
- return `"\${SHELL:-sh}" -lc '[ -s "$HOME/.nvm/nvm.sh" ] && . "$HOME/.nvm/nvm.sh" >/dev/null 2>&1; ${command}'`;
63
+ return `"\${SHELL:-sh}" -lc '[ -s "$HOME/.nvm/nvm.sh" ] && . "$HOME/.nvm/nvm.sh" >/dev/null 2>&1; PATH="$HOME/.local/bin:$PATH"; ${command}'`;
62
64
  }
63
65
 
64
- function sshStartArgs(dest, { loginShell = false } = {}) {
65
- const command = loginShell ? inLoginShell('spaces start --print-token') : 'spaces start --print-token';
66
- return ['-o', 'ConnectTimeout=15', '--', dest, command];
66
+ function sshStartArgs(dest, { loginShell = false, login = false } = {}) {
67
+ const start = `spaces start --print-token${login ? ' --print-login' : ''}`;
68
+ return ['-o', 'ConnectTimeout=15', '--', dest, loginShell ? inLoginShell(start) : start];
67
69
  }
68
70
 
69
71
  // ─── Installing and updating the remote ────────────────────
@@ -88,8 +90,48 @@ const PROBE = inLoginShell([
88
90
  'else echo SPACES_MISSING=1; fi',
89
91
  ].join(' '));
90
92
 
91
- function sshProbeArgs(dest) {
92
- return ['-o', 'ConnectTimeout=15', '--', dest, PROBE];
93
+ function sshProbeArgs(dest, { batch = false } = {}) {
94
+ return [...(batch ? ['-o', 'BatchMode=yes'] : []), '-o', 'ConnectTimeout=15', '--', dest, PROBE];
95
+ }
96
+
97
+ // ─── Signing in with a key ──────────────────────────────────
98
+
99
+ /** The private keys plain `ssh` tries by default, in the order it tries them. */
100
+ const DEFAULT_KEYS = ['id_ed25519', 'id_ecdsa', 'id_rsa'];
101
+
102
+ /**
103
+ * Why a no-prompt (`BatchMode`) ssh failed: `'password'` (the server wants a
104
+ * password, or another key), `'hostkey'` (ssh would have asked about an
105
+ * unknown host key), or `'other'`.
106
+ */
107
+ function classifySshFailure(stderr) {
108
+ const text = String(stderr);
109
+ if (/Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED/i.test(text)) return 'hostkey';
110
+ if (/Permission denied/i.test(text)) return 'password';
111
+ return 'other';
112
+ }
113
+
114
+ /**
115
+ * One public key line, as `ssh-keygen` writes it, or null. It travels to the
116
+ * remote on stdin rather than in the command, but is still checked: it is
117
+ * about to be trusted for every future sign-in there.
118
+ */
119
+ function publicKeyLine(text) {
120
+ const line = String(text).split(/\r?\n/)[0].trim();
121
+ return /^(ssh-ed25519|ecdsa-sha2-nistp(256|384|521)|ssh-rsa|sk-ssh-ed25519@openssh\.com) [A-Za-z0-9+/]+=*( [^\r\n]*)?$/.test(line)
122
+ ? line
123
+ : null;
124
+ }
125
+
126
+ /**
127
+ * Adds the public key on stdin to ~/.ssh/authorized_keys on the remote,
128
+ * once, creating the file with the permissions sshd insists on. `sh -c`
129
+ * so it runs the same whatever the remote user's own shell is.
130
+ */
131
+ const AUTHORIZE_KEY = `sh -c 'umask 077; mkdir -p "$HOME/.ssh" && k=$(cat) && touch "$HOME/.ssh/authorized_keys" && { grep -qxF "$k" "$HOME/.ssh/authorized_keys" || printf "%s\\n" "$k" >> "$HOME/.ssh/authorized_keys"; }'`;
132
+
133
+ function sshAuthorizeKeyArgs(dest) {
134
+ return ['-o', 'ConnectTimeout=15', '--', dest, AUTHORIZE_KEY];
93
135
  }
94
136
 
95
137
  const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
@@ -165,12 +207,21 @@ function installSpec(localVersion, override) {
165
207
  }
166
208
 
167
209
  /**
168
- * Installs `spec` on the remote; for an update, `spaces restart` then puts
169
- * the new version in charge — the freshly installed one, which knows to go
170
- * through the service when there is one.
210
+ * Installs `spec` on the remote and starts it, in one SSH session — each
211
+ * session can mean another password prompt. Into npm's global folder when
212
+ * this account can write there (nvm's always can), else into ~/.local,
213
+ * which needs no root. For an update, `spaces restart` first puts the new
214
+ * version in charge: the freshly installed one, which knows to go through
215
+ * the service when there is one. Ends with what `spaces start
216
+ * --print-token` prints, so the caller need not ask again.
171
217
  */
172
- function sshInstallArgs(dest, spec, { restart = false } = {}) {
173
- const command = `npm install -g ${spec}${restart ? ' && spaces restart' : ''}`;
218
+ function sshInstallArgs(dest, spec, { restart = false, login = false } = {}) {
219
+ const command = [
220
+ `if [ -w "$(npm prefix -g)/lib" ]; then npm install -g ${spec};`,
221
+ `else npm install -g --prefix "$HOME/.local" ${spec}; fi`,
222
+ restart ? '&& spaces restart' : '',
223
+ `&& spaces start --print-token${login ? ' --print-login' : ''}`,
224
+ ].filter(Boolean).join(' ');
174
225
  return ['-o', 'ConnectTimeout=15', '--', dest, inLoginShell(command)];
175
226
  }
176
227
 
@@ -209,9 +260,13 @@ function installFailureHint(kind, dest, spec) {
209
260
  }
210
261
  }
211
262
 
212
- /** `ssh` arguments for the tunnel: local port → the remote server's loopback. */
263
+ /**
264
+ * `ssh` arguments for the tunnel: local port → the remote server's loopback.
265
+ * `-v` so its log says when the forward is up (see tunnelReady).
266
+ */
213
267
  function sshTunnelArgs(dest, localPort, remotePort) {
214
268
  return [
269
+ '-v',
215
270
  '-N',
216
271
  '-o', 'ExitOnForwardFailure=yes',
217
272
  '-o', 'ServerAliveInterval=30',
@@ -221,6 +276,27 @@ function sshTunnelArgs(dest, localPort, remotePort) {
221
276
  ];
222
277
  }
223
278
 
279
+ /**
280
+ * Whether the tunnel's `ssh -v` log says the forward from `localPort` is
281
+ * listening. Waiting on the log rather than probing the port: a probe is a
282
+ * child process, and on Windows each one that exits resets the console,
283
+ * which garbles a password being typed into ssh's prompt meanwhile.
284
+ */
285
+ function tunnelReady(log, localPort) {
286
+ const port = String(localPort).replace(/\D/g, '');
287
+ return new RegExp(`Local forwarding listening on 127\\.0\\.0\\.1 port ${port}\\b|Local connections to 127\\.0\\.0\\.1:${port} forwarded`).test(String(log));
288
+ }
289
+
290
+ /** Whether the tunnel's log shows ssh got past authentication. */
291
+ function tunnelAuthenticated(log) {
292
+ return /^Authenticated to /m.test(String(log));
293
+ }
294
+
295
+ /** An `ssh -v` log without its debug chatter, for showing a failure. */
296
+ function withoutDebug(log) {
297
+ return String(log).split(/\r?\n/).filter((l) => !/^(debug\d:|OpenSSH_)/.test(l)).join('\n');
298
+ }
299
+
224
300
  /** The port `spaces start` reported, or null. */
225
301
  function parseStartOutput(stdout) {
226
302
  const m = PORT_LINE.exec(String(stdout));
@@ -229,6 +305,29 @@ function parseStartOutput(stdout) {
229
305
  return port >= 1 && port <= 65535 ? port : null;
230
306
  }
231
307
 
308
+ /** The browser sign-in code `spaces start --print-login` reported, or null. */
309
+ function parseStartLogin(stdout) {
310
+ const m = /^SPACES_LOGIN=([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)\s*$/m.exec(String(stdout));
311
+ return m ? m[1] : null;
312
+ }
313
+
314
+ /** The path the remote serves the web app under, or '' (also when malformed). */
315
+ function parseStartBasePath(stdout) {
316
+ const m = /^SPACES_BASE_PATH=(\/[A-Za-z0-9._~\/-]*?)\/?\s*$/m.exec(String(stdout));
317
+ return m ? m[1] : '';
318
+ }
319
+
320
+ /**
321
+ * The local port `spaces open --remote` tunnels from, the same each time for
322
+ * the same destination, so the address stays put in the browser's history.
323
+ * Within 41000–41999; another is used when this one is taken.
324
+ */
325
+ function preferredLocalPort(dest) {
326
+ let h = 0;
327
+ for (const c of String(dest)) h = (h * 31 + c.charCodeAt(0)) >>> 0;
328
+ return 41000 + (h % 1000);
329
+ }
330
+
232
331
  /** The session token `spaces start --print-token` reported, or null. */
233
332
  function parseStartToken(stdout) {
234
333
  const m = TOKEN_LINE.exec(String(stdout));
@@ -283,11 +382,21 @@ module.exports = {
283
382
  sshTunnelArgs,
284
383
  parseStartOutput,
285
384
  parseStartToken,
385
+ parseStartLogin,
386
+ parseStartBasePath,
387
+ preferredLocalPort,
286
388
  classifyStartFailure,
287
389
  installHint,
288
390
  splitRemote,
289
391
  PACKAGE,
392
+ DEFAULT_KEYS,
393
+ classifySshFailure,
394
+ publicKeyLine,
395
+ sshAuthorizeKeyArgs,
290
396
  inLoginShell,
397
+ tunnelReady,
398
+ tunnelAuthenticated,
399
+ withoutDebug,
291
400
  sshProbeArgs,
292
401
  parseProbe,
293
402
  nodeIsSupported,