syndai 0.1.1 → 0.3.0
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/CHANGELOG.md +31 -0
- package/README.md +76 -52
- package/bin/syndai.js +13 -29
- package/package.json +6 -11
- package/src/auth.js +179 -0
- package/src/changes.js +307 -0
- package/src/errors.js +128 -0
- package/src/http.js +153 -0
- package/src/main.js +212 -0
- package/src/mcp.js +93 -0
- package/src/projects.js +168 -0
- package/src/routes.js +56 -0
- package/src/settings.js +160 -0
- package/src/store.js +134 -0
- package/src/ui.js +133 -0
- package/src/watch.js +292 -0
- package/LICENSE +0 -9
- package/src/api.js +0 -70
- package/src/claude.js +0 -145
- package/src/codex.js +0 -71
- package/src/connect.js +0 -122
- package/src/proc.js +0 -36
- package/src/prompt.js +0 -43
- package/src/tokens.js +0 -45
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `syndai` npm package. Versions follow semver.
|
|
4
|
+
|
|
5
|
+
## 0.3.0
|
|
6
|
+
|
|
7
|
+
The package becomes the Syndai owner CLI.
|
|
8
|
+
|
|
9
|
+
- `login` (browser approval, RFC 8628 device flow), `login --with-token`, `logout`, `whoami`.
|
|
10
|
+
Keys live in `$XDG_CONFIG_HOME/syndai/credentials.json` (0600), one per workspace;
|
|
11
|
+
`SYNDAI_API_KEY` wins. There is no `--api-key` flag.
|
|
12
|
+
- `change` creates a change and follows it to the outcome. Its request id is saved before
|
|
13
|
+
the request, so a retry or a rerun after a crash never makes a second change.
|
|
14
|
+
- `watch` (with `--logs`), `answer`, `review`, `go-live`, `revert`, `cancel`, `status`,
|
|
15
|
+
`edit-text`, `link`, `settings`, `api`.
|
|
16
|
+
- `mcp install claude|cursor|codex` writes the Syndai MCP server into that tool's user
|
|
17
|
+
config with its own key.
|
|
18
|
+
- Exit codes 0-7 and 130 are a stable contract; `--json` (or a non-terminal stdout) prints
|
|
19
|
+
machine-readable results.
|
|
20
|
+
- Node 22 or newer. Zero dependencies. No telemetry.
|
|
21
|
+
|
|
22
|
+
Removed: the 0.2.0 `connect` pointer (connecting a Claude or ChatGPT plan happens in the
|
|
23
|
+
portal, under Connections).
|
|
24
|
+
|
|
25
|
+
## 0.2.0
|
|
26
|
+
|
|
27
|
+
Never published. `connect` printed a pointer to the portal.
|
|
28
|
+
|
|
29
|
+
## 0.1.1
|
|
30
|
+
|
|
31
|
+
Published from the `solozerolabs/syndai-connect` mirror: the retired `connect` pairing flow.
|
package/README.md
CHANGED
|
@@ -1,62 +1,86 @@
|
|
|
1
|
-
# syndai
|
|
1
|
+
# syndai
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
The Syndai owner CLI. Say what to change; Syndai builds it, runs every check, puts
|
|
4
|
+
it live and tells you the URL. Nobody babysits.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
npx -y syndai
|
|
7
|
+
npx -y syndai@latest login
|
|
8
|
+
npx -y syndai@latest change "make the header blue"
|
|
9
|
+
# Changing bakery.syndai.app · goes live when checks pass
|
|
8
10
|
```
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- `
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
- `claude` and/or `codex` on your PATH for the one-command path; otherwise the
|
|
47
|
-
Claude paste fallback still works.
|
|
48
|
-
|
|
49
|
-
## Source & provenance
|
|
50
|
-
|
|
51
|
-
This package is developed in the Syndai monorepo under `tools/syndai-connect/`
|
|
52
|
-
and **mirrored to the public repo
|
|
53
|
-
[`solozerolabs/syndai-connect`](https://github.com/solozerolabs/syndai-connect)**
|
|
54
|
-
for the npm provenance publish (owner action **O1**) — a private monorepo cannot
|
|
55
|
-
publish with sigstore provenance. Published as `syndai` with
|
|
56
|
-
`publishConfig.provenance`.
|
|
12
|
+
Node 22 or newer, zero dependencies, no telemetry. The customer guide is
|
|
13
|
+
`apps/portal/content/docs/cli.mdx` (rendered as the docs site's **CLI** page);
|
|
14
|
+
this README is for whoever works on the package.
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
| command | does |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `login [--with-token]`, `logout`, `whoami` | browser-approved key (device flow); `--with-token` reads a key from stdin |
|
|
21
|
+
| `link [project]` | remember which project this directory changes (`$XDG_CONFIG_HOME/syndai/links.json`, never in the repo) |
|
|
22
|
+
| `change "<what>" [--app A] [--review] [--image F]... [--no-wait] [--timeout D]` | start a change and follow it to the outcome |
|
|
23
|
+
| `edit-text [--app A] [--page /] --from "..." --to "..."` / `--undo <sha>` | instant, free, undoable text swap on a live site |
|
|
24
|
+
| `watch <run> [--timeout D] [--logs]` | follow any change; `--logs` tails its events |
|
|
25
|
+
| `answer <run> [--json body]` | answer an open question |
|
|
26
|
+
| `review <run> [--retry \| --close]` | see why a change is held, then approve it in the browser |
|
|
27
|
+
| `go-live`, `revert`, `cancel <run>` | land actions |
|
|
28
|
+
| `status [--app A]` | what is running, and everything that needs you |
|
|
29
|
+
| `settings [--app A] [get \| set k=v...]` | `autonomy`, `feature_requests`, `bug_scan`, `protected_paths` (`+=` adds) |
|
|
30
|
+
| `mcp install claude\|cursor\|codex` | add the Syndai MCP server to that tool's user config, with its own key |
|
|
31
|
+
| `api <METHOD> <path> [--json body]` | call any route with your key |
|
|
32
|
+
|
|
33
|
+
Exit codes: 0 done, 1 failed/reverted/cancelled, 2 usage or no project, 3 needs you,
|
|
34
|
+
4 live but unverified, 5 auth/scope/plan, 6 `--timeout` reached, 7 service
|
|
35
|
+
unavailable after retries, 130 interrupted (Ctrl-C detaches; it never cancels).
|
|
36
|
+
|
|
37
|
+
## Layout
|
|
38
|
+
|
|
39
|
+
- `bin/syndai.js`: the entrypoint; wires SIGINT to an AbortSignal.
|
|
40
|
+
- `src/main.js`: the command table, argument parsing (`util.parseArgs`), the one error report.
|
|
41
|
+
- `src/routes.js`: every REST route the CLI calls. Code builds paths only through it.
|
|
42
|
+
- `src/http.js`: `fetch`, retries (GETs and keyed POSTs only), `User-Agent: syndai-cli/<v>`.
|
|
43
|
+
- `src/errors.js`: exit codes; a problem+json `code` becomes one sentence plus the next command.
|
|
44
|
+
- `src/auth.js`, `src/store.js`: credentials (0600, by workspace), links, pending change ids.
|
|
45
|
+
- `src/projects.js`: which project a command acts on.
|
|
46
|
+
- `src/watch.js`: the ChangeState long-poll loop, questions, `--logs`.
|
|
47
|
+
- `src/changes.js`, `src/settings.js`, `src/mcp.js`: the commands.
|
|
57
48
|
|
|
58
49
|
## Development
|
|
59
50
|
|
|
60
51
|
```bash
|
|
61
|
-
|
|
52
|
+
cd tools/syndai-connect && node --test
|
|
62
53
|
```
|
|
54
|
+
|
|
55
|
+
The suite runs a fake API on 127.0.0.1 and drives the CLI in-process (plus two
|
|
56
|
+
child-process tests for Ctrl-C and a clean exit). `test/contract.test.js` checks
|
|
57
|
+
every route in `src/routes.js` against the published OpenAPI,
|
|
58
|
+
`docs/api/external-coding-v1.openapi.json`. It goes red when a backend route the
|
|
59
|
+
CLI uses is renamed or not yet published; regenerate the snapshot with
|
|
60
|
+
`cd backend && uv run python scripts/dump_external_openapi.py`, or check a fresh
|
|
61
|
+
render without touching it:
|
|
62
|
+
`SYNDAI_OPENAPI_JSON=/tmp/dump.json node --test test/contract.test.js`.
|
|
63
|
+
|
|
64
|
+
## Publishing (maintainers)
|
|
65
|
+
|
|
66
|
+
Never `npm publish` by hand. `.github/workflows/publish-cli.yml` publishes on a
|
|
67
|
+
`cli-v<version>` tag, with the same trust boundary as `publish-sync.yml`:
|
|
68
|
+
|
|
69
|
+
1. Bump `version` in `package.json` and add a `CHANGELOG.md` entry in a gated commit.
|
|
70
|
+
2. Once that commit is on `main` and CI is green, push a lightweight tag on exactly
|
|
71
|
+
that commit: `git tag cli-v<version> <sha> && git push Syndai refs/tags/cli-v<version>`.
|
|
72
|
+
3. The `build` job (no publish credential) checks the tag matches the version and
|
|
73
|
+
sits on `main`, skips a version already on npm, runs `node --test`, packs with
|
|
74
|
+
`npm pack` and checks the tarball. The `publish` job only downloads that tarball
|
|
75
|
+
and publishes it tokenlessly (npm trusted publishing). No provenance: npm does
|
|
76
|
+
not generate it from a private repository.
|
|
77
|
+
|
|
78
|
+
One-time owner setup (this retires the `solozerolabs/syndai-connect` mirror, which
|
|
79
|
+
published 0.1.1):
|
|
80
|
+
|
|
81
|
+
1. GitHub: add `cli-v*` to the `npm-publish` environment's deployment rule, and a
|
|
82
|
+
tag ruleset on `refs/tags/cli-v*` restricting create, update and delete (admins
|
|
83
|
+
bypass), as for `sync-v*`.
|
|
84
|
+
2. npm: on the `syndai` package, replace the trusted publisher with
|
|
85
|
+
`solozerolabs/Syndai`, workflow `publish-cli.yml`, environment `npm-publish`,
|
|
86
|
+
and keep "disallow tokens".
|
package/bin/syndai.js
CHANGED
|
@@ -1,33 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Entrypoint for the `syndai` CLI
|
|
2
|
+
// Entrypoint for the `syndai` owner CLI (`npx -y syndai@latest <command>`).
|
|
3
|
+
//
|
|
4
|
+
// Ctrl-C aborts the current wait and the command exits 130 after printing how to resume;
|
|
5
|
+
// it never cancels a change. A second Ctrl-C exits at once.
|
|
3
6
|
|
|
4
|
-
import {
|
|
7
|
+
import { main, processIo } from "../src/main.js";
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
case undefined:
|
|
12
|
-
case "-h":
|
|
13
|
-
case "--help":
|
|
14
|
-
case "help":
|
|
15
|
-
console.log("syndai — connect your Claude and ChatGPT plans to a Syndai workspace");
|
|
16
|
-
console.log("");
|
|
17
|
-
console.log("usage: npx -y syndai connect <code>");
|
|
18
|
-
console.log("");
|
|
19
|
-
console.log("Get <code> from your Syndai portal: Connections → Connect a plan.");
|
|
20
|
-
return subcommand ? 0 : 2;
|
|
21
|
-
default:
|
|
22
|
-
console.error(`syndai: unknown command "${subcommand}"`);
|
|
23
|
-
console.error("usage: npx -y syndai connect <code>");
|
|
24
|
-
return 2;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
9
|
+
const interrupt = new AbortController();
|
|
10
|
+
process.once("SIGINT", () => {
|
|
11
|
+
interrupt.abort();
|
|
12
|
+
process.once("SIGINT", () => process.exit(130));
|
|
13
|
+
});
|
|
27
14
|
|
|
28
|
-
main(process.argv.slice(2))
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.error(`syndai: ${e && e.message ? e.message : e}`);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
});
|
|
15
|
+
process.exitCode = await main(process.argv.slice(2), processIo(interrupt.signal));
|
|
16
|
+
// Idle keep-alive sockets or a stray child must not hold the result back.
|
|
17
|
+
setTimeout(() => process.exit(), 1000).unref();
|
package/package.json
CHANGED
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "syndai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The Syndai owner CLI: say what to change, and Syndai builds it, checks it and puts it live. Also installs the Syndai MCP server into Claude Code, Cursor and Codex.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"syndai": "bin/syndai.js"
|
|
8
8
|
},
|
|
9
9
|
"engines": {
|
|
10
|
-
"node": ">=
|
|
10
|
+
"node": ">=22"
|
|
11
11
|
},
|
|
12
|
-
"
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/solozerolabs/syndai-connect.git"
|
|
15
|
-
},
|
|
16
|
-
"homepage": "https://github.com/solozerolabs/syndai-connect#readme",
|
|
12
|
+
"homepage": "https://syndai.ai",
|
|
17
13
|
"license": "MIT",
|
|
18
14
|
"files": [
|
|
19
15
|
"bin/",
|
|
20
16
|
"src/",
|
|
21
|
-
"
|
|
17
|
+
"CHANGELOG.md"
|
|
22
18
|
],
|
|
23
19
|
"scripts": {
|
|
24
|
-
"test": "node --test
|
|
20
|
+
"test": "node --test"
|
|
25
21
|
},
|
|
26
22
|
"publishConfig": {
|
|
27
|
-
"provenance": true,
|
|
28
23
|
"access": "public"
|
|
29
24
|
}
|
|
30
25
|
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Credentials and the browser-approved login (RFC 8628 device flow, like `gh auth login`).
|
|
2
|
+
//
|
|
3
|
+
// Every door uses one credential type, an `sk_` key. `syndai login` mints one only after an
|
|
4
|
+
// owner or admin approves the request in a browser session at app.syndai.ai/cli. The key is
|
|
5
|
+
// stored per workspace in credentials.json (0600). `SYNDAI_API_KEY` wins over it (CI).
|
|
6
|
+
// There is no --api-key flag, so a key never lands in shell history.
|
|
7
|
+
|
|
8
|
+
import { hostname } from "node:os";
|
|
9
|
+
import { ApiError, CliError, EXIT } from "./errors.js";
|
|
10
|
+
import { createClient, DEFAULT_API_URL, interrupted } from "./http.js";
|
|
11
|
+
import { findLink, loadCredentials, removeCredential, saveCredential, storePaths } from "./store.js";
|
|
12
|
+
import { createUi } from "./ui.js";
|
|
13
|
+
|
|
14
|
+
/** The key this invocation uses: env first, then the cwd link's workspace, then the default. */
|
|
15
|
+
export function resolveCredential(io, paths) {
|
|
16
|
+
const envKey = io.env.SYNDAI_API_KEY?.trim();
|
|
17
|
+
if (envKey) return { apiKey: envKey, customerId: null, source: "SYNDAI_API_KEY" };
|
|
18
|
+
return storedCredential(io, paths);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function storedCredential(io, paths) {
|
|
22
|
+
const creds = loadCredentials(paths);
|
|
23
|
+
const link = findLink(paths, io.cwd);
|
|
24
|
+
const customerId = link && creds.customers[link.customer_id] ? link.customer_id : creds.default;
|
|
25
|
+
const entry = customerId ? creds.customers[customerId] : null;
|
|
26
|
+
if (!entry) return null;
|
|
27
|
+
return { apiKey: entry.api_key, customerId, source: paths.credentials };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Per-invocation state shared by every command. */
|
|
31
|
+
export function createContext(io, flags) {
|
|
32
|
+
const paths = storePaths(io.env);
|
|
33
|
+
const ui = createUi(io, { json: flags.json === true, noInput: flags["no-input"] === true });
|
|
34
|
+
const baseUrl = (io.env.SYNDAI_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
35
|
+
let cached;
|
|
36
|
+
const credential = () => {
|
|
37
|
+
cached ??= resolveCredential(io, paths);
|
|
38
|
+
if (!cached) throw new CliError(EXIT.AUTH, "You are not logged in.", { next: "syndai login" });
|
|
39
|
+
return cached;
|
|
40
|
+
};
|
|
41
|
+
const clientFor = (getKey) => createClient({ baseUrl, getKey, io });
|
|
42
|
+
const client = clientFor(async () => credential().apiKey);
|
|
43
|
+
let me;
|
|
44
|
+
return {
|
|
45
|
+
io,
|
|
46
|
+
ui,
|
|
47
|
+
paths,
|
|
48
|
+
flags,
|
|
49
|
+
baseUrl,
|
|
50
|
+
client,
|
|
51
|
+
clientFor,
|
|
52
|
+
credential,
|
|
53
|
+
async me() {
|
|
54
|
+
me ??= await client.call("me");
|
|
55
|
+
return me;
|
|
56
|
+
},
|
|
57
|
+
async customerId() {
|
|
58
|
+
return credential().customerId ?? (await this.me()).customer_id;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Ask for a key and wait until an owner approves it in the browser. Returns the token
|
|
65
|
+
* response `{api_key, customer_id, key_id, expires_at}`.
|
|
66
|
+
*/
|
|
67
|
+
export async function deviceApproval(ctx, clientName) {
|
|
68
|
+
const { client, ui, io } = ctx;
|
|
69
|
+
const start = await client.call("deviceStart", {
|
|
70
|
+
body: { client_name: clientName, host: hostname() },
|
|
71
|
+
auth: false,
|
|
72
|
+
retry: true,
|
|
73
|
+
});
|
|
74
|
+
const url = start.verification_uri_complete ?? start.verification_uri;
|
|
75
|
+
ui.progress(`Your one-time code: ${ui.bold(start.user_code)}`);
|
|
76
|
+
ui.progress(`Approve it in your browser: ${url}`);
|
|
77
|
+
if (ui.interactive) io.openUrl(url);
|
|
78
|
+
ui.progress(ui.dim("Waiting for approval (Ctrl-C to stop)..."));
|
|
79
|
+
|
|
80
|
+
let intervalMs = (start.interval ?? 5) * 1000;
|
|
81
|
+
const expiresAt = io.now() + (start.expires_in ?? 600) * 1000;
|
|
82
|
+
for (;;) {
|
|
83
|
+
try {
|
|
84
|
+
await io.sleep(intervalMs, io.signal);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (io.signal.aborted) throw interrupted();
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
const res = await client.call("deviceToken", {
|
|
90
|
+
body: { device_code: start.device_code },
|
|
91
|
+
auth: false,
|
|
92
|
+
retry: true,
|
|
93
|
+
raw: true,
|
|
94
|
+
});
|
|
95
|
+
if (res.status === 200) return res.body;
|
|
96
|
+
const error = res.body?.error;
|
|
97
|
+
if (error === "authorization_pending" && io.now() < expiresAt) continue;
|
|
98
|
+
if (error === "slow_down") {
|
|
99
|
+
intervalMs += 5000; // RFC 8628 section 3.5
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (error === "access_denied") {
|
|
103
|
+
throw new CliError(EXIT.AUTH, "The request was denied in the browser.", {
|
|
104
|
+
next: "syndai login",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (error === "expired_token" || error === "authorization_pending") {
|
|
108
|
+
throw new CliError(EXIT.AUTH, "The code expired before it was approved.", {
|
|
109
|
+
next: "syndai login",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
throw new ApiError(res.status, res.body, res.headers);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function login(ctx) {
|
|
117
|
+
const { io, ui, paths, flags } = ctx;
|
|
118
|
+
let token;
|
|
119
|
+
if (flags["with-token"]) {
|
|
120
|
+
if (io.stdin.isTTY) ui.progress("Paste a Syndai API key, then press Ctrl-D:");
|
|
121
|
+
const apiKey = (await readAll(io.stdin)).trim();
|
|
122
|
+
if (!apiKey) throw new CliError(EXIT.USAGE, "No key on stdin.", { next: "syndai login" });
|
|
123
|
+
const me = await ctx.clientFor(async () => apiKey).call("me");
|
|
124
|
+
token = { api_key: apiKey, customer_id: me.customer_id, key_id: me.key_id, expires_at: me.expires_at };
|
|
125
|
+
} else {
|
|
126
|
+
token = await deviceApproval(ctx, "CLI");
|
|
127
|
+
}
|
|
128
|
+
saveCredential(paths, token);
|
|
129
|
+
if (io.env.SYNDAI_API_KEY) ui.progress(ui.yellow("Note: SYNDAI_API_KEY is set and wins over this login."));
|
|
130
|
+
const expires = token.expires_at ? ` until ${token.expires_at.slice(0, 10)}` : "";
|
|
131
|
+
ui.result(
|
|
132
|
+
{ customer_id: token.customer_id, key_id: token.key_id, expires_at: token.expires_at },
|
|
133
|
+
`Logged in to workspace ${token.customer_id}${expires}.`,
|
|
134
|
+
);
|
|
135
|
+
return EXIT.OK;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function logout(ctx) {
|
|
139
|
+
const { io, ui, paths } = ctx;
|
|
140
|
+
const stored = storedCredential(io, paths);
|
|
141
|
+
if (!stored) {
|
|
142
|
+
ui.result({ logged_out: false }, "Not logged in.");
|
|
143
|
+
return EXIT.OK;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await ctx.clientFor(async () => stored.apiKey).call("revokeMe");
|
|
147
|
+
} catch (err) {
|
|
148
|
+
// Already revoked or expired: nothing left to revoke, so just forget it.
|
|
149
|
+
if (!(err instanceof ApiError && err.status === 401)) throw err;
|
|
150
|
+
}
|
|
151
|
+
removeCredential(paths, stored.customerId);
|
|
152
|
+
if (io.env.SYNDAI_API_KEY) ui.progress(ui.yellow("Note: SYNDAI_API_KEY is still set in this shell."));
|
|
153
|
+
ui.result(
|
|
154
|
+
{ logged_out: true, customer_id: stored.customerId },
|
|
155
|
+
`Logged out of workspace ${stored.customerId}; its key is revoked.`,
|
|
156
|
+
);
|
|
157
|
+
return EXIT.OK;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function whoami(ctx) {
|
|
161
|
+
const me = await ctx.me();
|
|
162
|
+
const { source } = ctx.credential();
|
|
163
|
+
ctx.ui.result(
|
|
164
|
+
{ ...me, source },
|
|
165
|
+
[
|
|
166
|
+
`Workspace ${me.customer_id}`,
|
|
167
|
+
`Key ${me.key_name ?? me.key_id}${me.expires_at ? ` (expires ${me.expires_at.slice(0, 10)})` : ""}`,
|
|
168
|
+
`Scopes ${(me.scopes ?? []).join(", ")}`,
|
|
169
|
+
`From ${source}`,
|
|
170
|
+
].join("\n"),
|
|
171
|
+
);
|
|
172
|
+
return EXIT.OK;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function readAll(stream) {
|
|
176
|
+
let text = "";
|
|
177
|
+
for await (const chunk of stream) text += chunk;
|
|
178
|
+
return text;
|
|
179
|
+
}
|