From 53b792c52ca683f631824aabdf4346edd0047e85 Mon Sep 17 00:00:00 2001 From: Reynaldi Chernando <12949382+reynaldichernando@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:36:46 +0700 Subject: [PATCH] connect to worker's kv with cli (#3613) --- src/cli/README.md | 18 +++++- src/cli/bin/puter.js | 8 ++- src/cli/package.json | 2 +- src/cli/src/commands/kv.js | 118 +++++++++++++++++++++++++++++++------ src/docs/src/cli.md | 21 +++++-- 5 files changed, 137 insertions(+), 30 deletions(-) diff --git a/src/cli/README.md b/src/cli/README.md index acb1115ae..53860027c 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -58,11 +58,16 @@ puter app get ## Key-value store -Open an interactive JavaScript shell against one app's key-value store. Takes -an app name or a uid. +Open an interactive JavaScript shell against the key-value store of one app or +worker. Takes an app name, a worker name or its `*.puter.work` URL, or a uid — +a worker resolves to the sandbox app it was deployed with, which is where its +`puter.kv` data lives. An app name wins a tie with a worker of the same name; +the worker's URL asks for the worker instead. ```sh puter kv connect +puter kv connect +puter kv connect https://.puter.work ``` Every `puter.kv` method is bound to that app and available bare, so pasted @@ -81,6 +86,15 @@ kv(notes)> puter.kv.incr("visits") 1 ``` +A worker connects the same way, and the prompt says so: + +```console +$ puter kv connect my-api +✔ Connected to worker my-api (app-9a8b7c6d…) · 3 keys +kv(worker:my-api)> list() +[ 'visits' ] +``` + Results are awaited for you — `get("k")` prints the value, not a pending promise — and `_` holds the last one. `.help` lists the methods, `.clear` resets the session, `.exit` (or Ctrl-D) quits. Needs a terminal. diff --git a/src/cli/bin/puter.js b/src/cli/bin/puter.js index e6091f6bc..4afcaeea5 100755 --- a/src/cli/bin/puter.js +++ b/src/cli/bin/puter.js @@ -132,12 +132,14 @@ app // --- kv --------------------------------------------------------------------- -const kv = program.command('kv').description("Explore an app's key-value store"); +const kv = program + .command('kv') + .description("Explore an app's or worker's key-value store"); kv .command('connect') - .description("Open an interactive shell against an app's KV store") - .argument('', 'app name or uid') + .description("Open an interactive shell against an app's or worker's KV store") + .argument('', 'app name, worker name or URL, or app uid') .action(action(kvConnect)); program.parseAsync(process.argv); diff --git a/src/cli/package.json b/src/cli/package.json index c3a130f68..191559cac 100644 --- a/src/cli/package.json +++ b/src/cli/package.json @@ -1,6 +1,6 @@ { "name": "@heyputer/cli", - "version": "0.2.0", + "version": "0.3.0", "description": "Puter CLI - developer tooling from your terminal.", "license": "MIT", "author": "Puter Technologies Inc.", diff --git a/src/cli/src/commands/kv.js b/src/cli/src/commands/kv.js index 54da31f7c..dd8532955 100644 --- a/src/cli/src/commands/kv.js +++ b/src/cli/src/commands/kv.js @@ -1,5 +1,6 @@ -// `puter kv connect ` — an interactive shell against one app's -// key-value store. +// `puter kv connect ` — an interactive shell against one app's +// key-value store, named by app, by worker — name or URL, both resolving to +// the app behind the worker — or by uid. // // The REPL is Node's own (multiline, history, Ctrl-C/Ctrl-D, `_` and // util.inspect formatting come free); we only add two things on top: @@ -13,7 +14,7 @@ import repl from 'node:repl'; import { appsApi } from '../lib/apps.js'; import { ensureClient } from '../lib/auth.js'; import { configPath } from '../lib/config.js'; -import { isInteractive } from '../lib/env.js'; +import { isInteractive, WORKER_DOMAIN } from '../lib/env.js'; import { CLIError, messageOf } from '../lib/errors.js'; import { bindApp, KV_METHODS } from '../lib/kvbind.js'; import * as ui from '../lib/ui.js'; @@ -24,24 +25,98 @@ import * as ui from '../lib/ui.js'; const PROBE_LIMIT = 100; const APPS_HINT = "Run 'puter app list' to see your apps."; +const WORKERS_HINT = "Run 'puter worker list' to see your workers."; +const TARGET_HINT = + "Run 'puter app list' or 'puter worker list' to see what you can connect to."; -// Accept either an app name or the uid itself, so a uid pasted from -// `puter app list` (or the MCP tools) doesn't need a lookup. -async function resolveApp(puter, appArg) { - if (/^app-/.test(appArg)) return { name: appArg, uid: appArg, byUid: true }; +// A worker URL identifies a worker as well as its name does — copy one out of +// `puter worker list` or the browser and it connects. Returns the worker name, +// or null when the argument isn't a worker URL. +function workerNameFromUrl(arg) { + const host = String(arg) + .trim() + .replace(/^[a-z]+:\/\//i, '') // scheme + .replace(/[/?#].*$/, '') // path, query, fragment + .replace(/:\d+$/, '') // port + .toLowerCase(); + + const suffix = `.${WORKER_DOMAIN.toLowerCase()}`; + if (!host.endsWith(suffix)) return null; + const name = host.slice(0, -suffix.length); + // Only the flat `.puter.work` form — a deeper host isn't one of ours. + return name && !name.includes('.') ? name : null; +} + +// "No such app" is the cue to try a worker by the same name; anything else +// (offline, expired token) is a real failure and should be reported as one. +const isNotFound = (err) => + err?.code === 'not_found' || /not found/i.test(messageOf(err)); + +// A worker deployed from a user token gets its own `sandbox-` app, and +// with it a key-value store of its own — so a worker name resolves to the uid +// of that app. Returns null when the account has no worker by that name. +async function resolveWorker(puter, nameArg) { + let worker; + try { + worker = await puter.workers.get(nameArg); + } catch (err) { + throw new CLIError( + `Could not fetch worker '${nameArg}': ${messageOf(err)}`, + { hint: WORKERS_HINT }, + ); + } + if (!worker) return null; + + // Workers deployed by an app (or with `sandbox: false`) have no app identity + // of their own — they read and write the store of whoever deployed them. + if (!worker.app_uid) { + throw new CLIError( + `Worker '${worker.name ?? nameArg}' has no store of its own: it is not sandboxed.`, + { hint: 'Connect to the app that owns it instead.' }, + ); + } + return { name: worker.name ?? nameArg, uid: worker.app_uid, kind: 'worker' }; +} + +// Accept an app name, a worker name, a worker URL, or the uid itself, so a uid +// pasted from `puter app list` (or the MCP tools) doesn't need a lookup. An app +// name wins a tie with a worker of the same name — the worker's own URL is the +// way to ask for the worker instead. +async function resolveTarget(puter, identifier) { + // A URL says which namespace it belongs to, so it never falls back to an app. + const fromUrl = workerNameFromUrl(identifier); + if (fromUrl) { + const worker = await resolveWorker(puter, fromUrl); + if (!worker) { + throw new CLIError(`Worker '${fromUrl}' not found.`, { + hint: WORKERS_HINT, + }); + } + return worker; + } + + if (/^app-/.test(identifier)) { + return { name: identifier, uid: identifier, byUid: true }; + } let app; try { - app = await appsApi(puter).get(appArg); + app = await appsApi(puter).get(identifier); } catch (err) { - throw new CLIError(`Could not fetch '${appArg}': ${messageOf(err)}`, { - hint: APPS_HINT, - }); + if (!isNotFound(err)) { + throw new CLIError(`Could not fetch '${identifier}': ${messageOf(err)}`, { + hint: APPS_HINT, + }); + } } - if (!app?.uid) { - throw new CLIError(`App '${appArg}' not found.`, { hint: APPS_HINT }); - } - return { name: app.name ?? appArg, uid: app.uid }; + if (app?.uid) return { name: app.name ?? identifier, uid: app.uid }; + + const worker = await resolveWorker(puter, identifier); + if (worker) return worker; + + throw new CLIError(`No app or worker named '${identifier}'.`, { + hint: TARGET_HINT, + }); } // One round-trip that both proves the store is reachable and gives us @@ -140,7 +215,7 @@ function startRepl({ kv, prompt }) { }); } -export async function kvConnect(appArg) { +export async function kvConnect(identifier) { if (!isInteractive()) { throw new CLIError('`puter kv connect` needs a terminal.', { hint: 'Run it from an interactive shell.', @@ -148,16 +223,21 @@ export async function kvConnect(appArg) { } const puter = await ensureClient(); - const { name, uid, byUid } = await resolveApp(puter, appArg); + const { name, uid, byUid, kind } = await resolveTarget(puter, identifier); const kv = bindApp(puter.kv, uid); const keys = await probeKeys(kv, name); // A uid is its own name here, so don't print it twice — and keep it out of // the prompt at full length, where it would dwarf what you type. const where = byUid ? '' : ` ${ui.dim(`(${uid})`)}`; - ui.success(`Connected to ${ui.bold(name)}${where} · ${keys}`); + const what = kind === 'worker' ? 'worker ' : ''; + ui.success(`Connected to ${what}${ui.bold(name)}${where} · ${keys}`); ui.info('.help for the kv methods, .exit to quit'); - const label = byUid ? `${uid.slice(0, 12)}…` : name; + // The prompt says which namespace the name came from: an app and a worker + // can share a name, and only one of them is what you're typing against. + const label = byUid + ? `${uid.slice(0, 12)}…` + : `${kind === 'worker' ? 'worker:' : ''}${name}`; await startRepl({ kv, prompt: `kv(${label})> ` }); } diff --git a/src/docs/src/cli.md b/src/docs/src/cli.md index ed5368d77..487563da1 100644 --- a/src/docs/src/cli.md +++ b/src/docs/src/cli.md @@ -3,7 +3,7 @@ title: CLI description: Manage your Puter resources directly from your terminal with the Puter CLI. Deploy static sites and serverless workers without leaving your shell. --- -The [Puter CLI](https://www.npmjs.com/package/@heyputer/cli) lets you manage your Puter resources straight from the terminal: deploy static websites, ship serverless workers, inspect the apps registered to your account, and explore their key-value stores, all without leaving your shell. +The [Puter CLI](https://www.npmjs.com/package/@heyputer/cli) lets you manage your Puter resources straight from the terminal: deploy static websites, ship serverless workers, inspect the apps registered to your account, and explore the key-value stores behind your apps and workers, all without leaving your shell.
The Puter CLI is in beta (0.x). Behavior may change between releases.
@@ -83,7 +83,7 @@ puter app get # show one app's details ## Key-value store -Open an interactive JavaScript shell against one app's [key-value store](/KV/), so you can read and edit its data directly instead of going through the app. Pass an app name or a uid: +Open an interactive JavaScript shell against the [key-value store](/KV/) of one app or [worker](/Workers/), so you can read and edit its data directly instead of going through the app. Pass an app name, a worker name or its `*.puter.work` URL, or a uid: ```sh puter kv connect my-app @@ -102,11 +102,22 @@ kv(notes)> list("gre", true) [ { key: 'greeting', value: 'hi' } ] ``` +A worker connects the same way. Deploying a worker from your account gives it its own sandbox app, and that app's store is where the worker's `puter.kv` data lives — so naming the worker connects you to it, and the prompt says which one you're in: + +```console +$ puter kv connect my-api +✔ Connected to worker my-api (app-9a8b7c6d…) · 3 keys +kv(worker:my-api)> list() +[ 'visits' ] +``` + +Names are looked up as apps first, so if an app and a worker share a name you get the app; pass the worker's URL (`puter kv connect https://my-api.puter.work`) to ask for the worker instead. A worker deployed by an app rather than by you has no store of its own — connect to the app that owns it. + Every [`puter.kv`](/KV/) method is bound to the connected app and available bare, as `kv.set(…)`, and as `puter.kv.set(…)`, so examples copied from these docs run as written. Results are awaited for you — `get("k")` prints the value rather than a pending promise — and `_` holds the last result. It's a full JavaScript REPL, so multi-line input, variables, and history between sessions all work. `.help` lists the kv methods, `.clear` resets the session, and `.exit` (or Ctrl-D) quits. -
Writes through puter kv connect go to the connected app's store, not your user-level store — the same data the app itself reads and writes.
+
Writes through puter kv connect go to the connected app's store, not your user-level store — the same data the app or worker itself reads and writes.
## CLI reference @@ -213,11 +224,11 @@ Show details for one app. ### `puter kv connect` -Open an interactive shell against an app's key-value store. +Open an interactive shell against an app's or worker's key-value store. | Argument | Description | | --- | --- | -| `` | The app whose store to connect to, by name or uid (`app-…`). | +| `` | The store to connect to: an app name, an app uid (`app-…`), a worker name, or a worker URL (`https://my-api.puter.work`). Ambiguous names resolve to the app. | Requires a terminal — in a non-interactive context the command exits with an error rather than hanging.