connect to worker's kv with cli (#3613)

This commit is contained in:
Reynaldi Chernando
2026-08-20 08:36:46 +07:00
committed by GitHub
parent 2dd7073034
commit 53b792c52c
5 changed files with 137 additions and 30 deletions
+16 -2
View File
@@ -58,11 +58,16 @@ puter app get <name>
## 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 <app>
puter kv connect <worker>
puter kv connect https://<worker>.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.
+5 -3
View File
@@ -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>', 'app name or uid')
.description("Open an interactive shell against an app's or worker's KV store")
.argument('<identifier>', 'app name, worker name or URL, or app uid')
.action(action(kvConnect));
program.parseAsync(process.argv);
+1 -1
View File
@@ -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.",
+99 -19
View File
@@ -1,5 +1,6 @@
// `puter kv connect <app>` — an interactive shell against one app's
// key-value store.
// `puter kv connect <identifier>` — 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 `<name>.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-<name>` 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})> ` });
}
+16 -5
View File
@@ -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.
<div class="info">The Puter CLI is in beta (0.x). Behavior may change between releases.</div>
@@ -83,7 +83,7 @@ puter app get <name> # 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.
<div class="info">Writes through <code>puter kv connect</code> go to the connected app's store, not your user-level store — the same data the app itself reads and writes.</div>
<div class="info">Writes through <code>puter kv connect</code> go to the connected app's store, not your user-level store — the same data the app or worker itself reads and writes.</div>
## 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 |
| --- | --- |
| `<app>` | The app whose store to connect to, by name or uid (`app-…`). |
| `<identifier>` | 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.