mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-23 22:47:19 +00:00
connect to worker's kv with cli (#3613)
This commit is contained in:
+16
-2
@@ -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.
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user