@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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/admin/users/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/admin/users/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/analytics/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/analytics/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/network/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/network/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/workspaces/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/workspaces/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/analytics.html +1 -1
- package/.next/standalone/.next/server/app/admin/analytics.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin/analytics/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/analytics.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/analytics.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.html +1 -1
- package/.next/standalone/.next/server/app/admin/users.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin/users/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/analytics.html +1 -1
- package/.next/standalone/.next/server/app/analytics.rsc +2 -2
- package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap/analytics/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/analytics.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/analytics.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin/analytics/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin/os-users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin/users/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/analytics/overview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/me/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/totp/setup/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/totp/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/totp/verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/chat/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/claude/usage/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/folders/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/discovered/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/handshake/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/health/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/identity/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/keys/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/keys/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/nodes/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/nodes/check/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/nodes/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/panes/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/panes/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/projects/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/proxy/[nodeId]/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/sessions/[id]/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/sessions/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/workspaces/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/workspaces/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/panes/[id]/diff/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/panes/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/panes/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/proxy/models/[modelId]/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/proxy/models/[modelId]/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/server-config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/chat/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sync/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/tags/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/tier/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/updates/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/whisper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wizard/chat/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/context/[key]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/context/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/[msgId]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/todos/[todoId]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/todos/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/login/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/login.html +1 -1
- package/.next/standalone/.next/server/app/login.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/projects.html +1 -1
- package/.next/standalone/.next/server/app/m/projects.rsc +2 -2
- package/.next/standalone/.next/server/app/m/projects.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/projects.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/projects.segments/m/projects/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/sessions/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/sessions.html +1 -1
- package/.next/standalone/.next/server/app/m/sessions.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions.segments/m/sessions/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/settings.html +1 -1
- package/.next/standalone/.next/server/app/m/settings.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings.segments/m/settings/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/terminal/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/terminal.html +1 -1
- package/.next/standalone/.next/server/app/m/terminal.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal.segments/m/terminal/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m.html +1 -1
- package/.next/standalone/.next/server/app/m.rsc +2 -2
- package/.next/standalone/.next/server/app/m.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m.segments/m/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/network.html +1 -1
- package/.next/standalone/.next/server/app/network.rsc +2 -2
- package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap/network/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/network.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/network.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.html +1 -1
- package/.next/standalone/.next/server/app/projects.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap/projects/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.html +1 -1
- package/.next/standalone/.next/server/app/sessions.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap/sessions/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap/settings/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/terminal.html +1 -1
- package/.next/standalone/.next/server/app/terminal.rsc +2 -2
- package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap/terminal/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/terminal.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/terminal.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/vr/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/vr/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/vr.html +1 -1
- package/.next/standalone/.next/server/app/vr.rsc +2 -2
- package/.next/standalone/.next/server/app/vr.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/vr.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/vr.segments/vr/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/workspaces.html +1 -1
- package/.next/standalone/.next/server/app/workspaces.rsc +2 -2
- package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap/workspaces/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/workspaces.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/workspaces.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/chunks/_1-inpovedul0t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1yl1yt70hdi7k._.js +1 -1
- package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__00lhc9vpmf8oy._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/07t-bv_pwp3nh.css +3 -0
- package/.next/standalone/package.json +6 -6
- package/README.md +29 -3
- package/bin/lib/cli-flags.js +2 -0
- package/bin/lib/remote.js +124 -15
- package/bin/lib/runtime/claude-sessions.js +108 -13
- package/bin/lib/runtime/index.js +1 -1
- package/bin/spaces.js +330 -57
- package/package.json +6 -6
- package/.next/standalone/.next/static/chunks/3bk2w5qzafz7n.css +0 -3
- /package/.next/standalone/.next/static/{_RTyuAfH-40lIlwVs2azh → B50xUMpZMRp2r4CxAPPFu}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{_RTyuAfH-40lIlwVs2azh → B50xUMpZMRp2r4CxAPPFu}/_clientMiddlewareManifest.js +0 -0
- /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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
const {
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
|
869
|
-
console.error(`
|
|
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
|
-
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
|
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.
|
|
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.
|
|
126
|
-
"@jlongo78/agent-spaces-tui-darwin-x64": "1.0.
|
|
127
|
-
"@jlongo78/agent-spaces-tui-linux-arm64": "1.0.
|
|
128
|
-
"@jlongo78/agent-spaces-tui-linux-x64": "1.0.
|
|
129
|
-
"@jlongo78/agent-spaces-tui-win32-x64": "1.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
|
}
|