@spendgraph/cli 0.2.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/README.md +149 -0
- package/dist/args.d.ts +13 -0
- package/dist/args.js +44 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +78 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +135 -0
- package/dist/commands/dashboard.d.ts +11 -0
- package/dist/commands/dashboard.js +236 -0
- package/dist/commands/prompts.d.ts +3 -0
- package/dist/commands/prompts.js +133 -0
- package/dist/commands/skills.d.ts +8 -0
- package/dist/commands/skills.js +68 -0
- package/dist/commands/tools.d.ts +3 -0
- package/dist/commands/tools.js +70 -0
- package/dist/connect.d.ts +19 -0
- package/dist/connect.js +38 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +89 -0
- package/dist/env.d.ts +42 -0
- package/dist/env.js +69 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +3 -0
- package/dist/help.d.ts +5 -0
- package/dist/help.js +38 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/output.d.ts +8 -0
- package/dist/output.js +56 -0
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +12 -0
- package/dist/skills.d.ts +11 -0
- package/dist/skills.js +200 -0
- package/dist/store.d.ts +15 -0
- package/dist/store.js +47 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @spendgraph/cli
|
|
2
|
+
|
|
3
|
+
`sg` drives the spendgraph dashboard from a terminal: write a prompt, declare a
|
|
4
|
+
tool, mint a key, read what a project spent.
|
|
5
|
+
|
|
6
|
+
It is a front-end over `@spendgraph/sdk` and nothing else. Every command is one
|
|
7
|
+
SDK call, so the base URL, auth, retries and error shapes are decided in one
|
|
8
|
+
place and this package only decides what to call and how to print it.
|
|
9
|
+
|
|
10
|
+
## Credentials
|
|
11
|
+
|
|
12
|
+
Two halves, and which one you hold decides what you reach.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
export SPENDGRAPH_BASE_URL=https://your-app.workers.dev
|
|
16
|
+
export SPENDGRAPH_API_KEY=sg_… # prompts, tools, usage, stats
|
|
17
|
+
export SPENDGRAPH_SESSION=… # keys, projects, pricing, credentials
|
|
18
|
+
export SPENDGRAPH_PROJECT=prj_… # the default --project
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Better, set them once and forget them:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
sg config set --base-url https://your-app.workers.dev --api-key sg_…
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That writes `~/.config/spendgraph/config.json` (or under `$XDG_CONFIG_HOME`),
|
|
28
|
+
mode `0600` because it holds a key. After it, `sg` works from any directory with
|
|
29
|
+
nothing exported and no `.env` anywhere.
|
|
30
|
+
|
|
31
|
+
Values resolve in layers, highest first:
|
|
32
|
+
|
|
33
|
+
| | |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| 1 | a `--flag` |
|
|
36
|
+
| 2 | the exported environment |
|
|
37
|
+
| 3 | the nearest `.env`, searching upward from the working directory |
|
|
38
|
+
| 4 | the global config |
|
|
39
|
+
|
|
40
|
+
So a project that needs different credentials puts them in its own `.env` and
|
|
41
|
+
they beat the global default, the way a local git config does. Only `.env` is
|
|
42
|
+
read, never `.env.local`; `--env-file <path>` replaces the search.
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
sg config show # what is in effect, and which layer each value came from
|
|
46
|
+
sg config path # where the config and the .env in effect live
|
|
47
|
+
sg config reaches # which groups your credentials actually open
|
|
48
|
+
sg config unset SPENDGRAPH_API_KEY
|
|
49
|
+
sg config clear --yes
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`show` and `set` mask the key rather than echoing it.
|
|
53
|
+
|
|
54
|
+
**Reading is not writing.** An `sg_` key lists and gets prompts and tools and
|
|
55
|
+
writes usage, but every *write* to a prompt or a tool — `create`, `update`,
|
|
56
|
+
`promote` — calls a route that requires a signed-in session, as does everything
|
|
57
|
+
under `key`, `project`, `pricing` and `credential`. `sg config reaches` prints
|
|
58
|
+
the split for the credentials you actually hold.
|
|
59
|
+
|
|
60
|
+
There is no API-key path to the session half, deliberately: `credential` holds
|
|
61
|
+
provider keys and `key` mints API keys. A 401 there is the server saying the
|
|
62
|
+
session is missing, and `sg` says so rather than repeating the status.
|
|
63
|
+
|
|
64
|
+
Any flag beats the environment: `--base-url`, `--api-key`, `--session`,
|
|
65
|
+
`--project`.
|
|
66
|
+
|
|
67
|
+
## Shape
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
sg # the groups
|
|
71
|
+
sg tool # a group's commands
|
|
72
|
+
sg tool create --help # the same, with usage
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Simple fields are flags. Anything structured — a prompt's `blocks`, a tool's
|
|
76
|
+
`args`, a dataset — goes in `--file`, which takes a JSON object. Flags given
|
|
77
|
+
alongside `--file` win, so one field changes without restating the rest:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
sg tool create --file ./tools/deeprecall.json --description "Searches the KB"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`projectId` in the file is enough on its own; `--project` is only needed when
|
|
84
|
+
the file does not carry one.
|
|
85
|
+
|
|
86
|
+
## The four things you probably came for
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
# declare a tool
|
|
90
|
+
sg tool create --name deeprecall \
|
|
91
|
+
--description "Searches the knowledge base and answers from it." \
|
|
92
|
+
--effect readonly --arg question:string:required
|
|
93
|
+
|
|
94
|
+
# change one
|
|
95
|
+
sg tool update tl_123 --file ./tools/deeprecall.json
|
|
96
|
+
|
|
97
|
+
# write a prompt
|
|
98
|
+
sg prompt create --name decompose --file ./prompts/decompose.json
|
|
99
|
+
|
|
100
|
+
# write a new version of one, then make it current
|
|
101
|
+
sg prompt update pr_123 --file ./prompts/decompose.json
|
|
102
|
+
sg prompt promote pr_123 pv_456
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`--arg` is `name:type` or `name:type:required`, comma-separated for more than
|
|
106
|
+
one. It covers a plain argument; a `FieldSpec` with an enum or a description
|
|
107
|
+
belongs in `--file` under `args`.
|
|
108
|
+
|
|
109
|
+
## Output
|
|
110
|
+
|
|
111
|
+
A list of flat rows prints as a table, everything else as JSON. `--json` forces
|
|
112
|
+
JSON whole, which is what you want in a pipe:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
sg tool list --json | jq '.tools[] | select(.effect == "destructive")'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Skills, for an agent
|
|
119
|
+
|
|
120
|
+
`sg` ships its own operating manual as agent skills, generated from the command
|
|
121
|
+
registry rather than written down beside it — a command added anywhere appears
|
|
122
|
+
in the reference on the next run, so it cannot go stale.
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
sg skill list # what is on offer
|
|
126
|
+
sg skill show spendgraph-cli # the SKILL.md, on stdout
|
|
127
|
+
sg skill install # write them under .claude/skills
|
|
128
|
+
sg skill install --dir ./skills --force
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| skill | for |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `spendgraph-cli` | the whole CLI: credentials, flags, output, exit codes, every command |
|
|
134
|
+
| `spendgraph-prompt-authoring` | writing and versioning prompts, declaring tools, the JSON shapes |
|
|
135
|
+
|
|
136
|
+
These three commands never reach the API, so they work with no credentials set —
|
|
137
|
+
an agent can read the manual before it has anything to authenticate with.
|
|
138
|
+
An existing `SKILL.md` is left alone unless `--force` says otherwise.
|
|
139
|
+
|
|
140
|
+
## Exit codes
|
|
141
|
+
|
|
142
|
+
| | |
|
|
143
|
+
| --- | --- |
|
|
144
|
+
| 0 | the server answered |
|
|
145
|
+
| 1 | the server refused |
|
|
146
|
+
| 2 | the command line was wrong, and nothing was sent |
|
|
147
|
+
|
|
148
|
+
The difference between 1 and 2 is worth scripting against: a 2 never reached the
|
|
149
|
+
network, so retrying it changes nothing.
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** One command line, split into the words that name a command and the flags that configure it. */
|
|
2
|
+
export interface Parsed {
|
|
3
|
+
words: string[];
|
|
4
|
+
flags: Record<string, string | boolean>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Splits argv into words and flags.
|
|
8
|
+
*
|
|
9
|
+
* `--name value`, `--name=value` and a bare `--name` all set `name`; `--no-name`
|
|
10
|
+
* clears it. A value starting with a single dash is still a value, so a negative
|
|
11
|
+
* number reaches the command rather than being read as the next flag.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parse(argv: string[]): Parsed;
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const isFlag = (token) => token.startsWith("--");
|
|
2
|
+
/**
|
|
3
|
+
* Splits argv into words and flags.
|
|
4
|
+
*
|
|
5
|
+
* `--name value`, `--name=value` and a bare `--name` all set `name`; `--no-name`
|
|
6
|
+
* clears it. A value starting with a single dash is still a value, so a negative
|
|
7
|
+
* number reaches the command rather than being read as the next flag.
|
|
8
|
+
*/
|
|
9
|
+
export function parse(argv) {
|
|
10
|
+
const words = [];
|
|
11
|
+
const flags = {};
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const token = argv[i];
|
|
14
|
+
if (token === "-h") {
|
|
15
|
+
flags.help = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (!isFlag(token)) {
|
|
19
|
+
words.push(token);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const [name, inline] = split(token.slice(2));
|
|
23
|
+
if (inline !== undefined) {
|
|
24
|
+
flags[name] = inline;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (name.startsWith("no-")) {
|
|
28
|
+
flags[name.slice(3)] = false;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const next = argv[i + 1];
|
|
32
|
+
if (next !== undefined && !isFlag(next)) {
|
|
33
|
+
flags[name] = next;
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
flags[name] = true;
|
|
38
|
+
}
|
|
39
|
+
return { words, flags };
|
|
40
|
+
}
|
|
41
|
+
function split(token) {
|
|
42
|
+
const at = token.indexOf("=");
|
|
43
|
+
return at === -1 ? [token, undefined] : [token.slice(0, at), token.slice(at + 1)];
|
|
44
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Env } from "./env.js";
|
|
2
|
+
/** Where the CLI writes. Injected so a test reads the output instead of the terminal. */
|
|
3
|
+
export interface Io {
|
|
4
|
+
out(text: string): void;
|
|
5
|
+
err(text: string): void;
|
|
6
|
+
}
|
|
7
|
+
/** What a caller may replace: where the environment comes from, and how requests are sent. */
|
|
8
|
+
export interface Options {
|
|
9
|
+
env?: Env;
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
/** Where to look for a `.env`, searching upward. Left unset, none is read. */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** The global config file. Left unset, the one under `$XDG_CONFIG_HOME` is used. */
|
|
14
|
+
store?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Runs one command line and returns the exit code.
|
|
18
|
+
*
|
|
19
|
+
* 0 is a reply, 1 is the server refusing, 2 is the line being wrong — so a
|
|
20
|
+
* script can tell a bad request from a bad password without parsing text.
|
|
21
|
+
*/
|
|
22
|
+
export declare function main(argv: string[], io: Io, opts?: Options): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { SpendgraphError } from "@spendgraph/sdk";
|
|
2
|
+
import { parse } from "./args.js";
|
|
3
|
+
import { connect, credentials } from "./connect.js";
|
|
4
|
+
import { loadEnv } from "./env.js";
|
|
5
|
+
import { readStore, storePath } from "./store.js";
|
|
6
|
+
import { context } from "./context.js";
|
|
7
|
+
import { UsageError } from "./errors.js";
|
|
8
|
+
import { groupHelp, overview } from "./help.js";
|
|
9
|
+
import { render } from "./output.js";
|
|
10
|
+
import { commandNamed, groupNamed } from "./registry.js";
|
|
11
|
+
const HINT = "A dashboard session is needed: for everything under key, project, pricing and credential, and for every write to a prompt or a tool. An sg_ key reads prompts and tools and writes usage. Set SPENDGRAPH_SESSION.";
|
|
12
|
+
/**
|
|
13
|
+
* Runs one command line and returns the exit code.
|
|
14
|
+
*
|
|
15
|
+
* 0 is a reply, 1 is the server refusing, 2 is the line being wrong — so a
|
|
16
|
+
* script can tell a bad request from a bad password without parsing text.
|
|
17
|
+
*/
|
|
18
|
+
export async function main(argv, io, opts = {}) {
|
|
19
|
+
const parsed = parse(argv);
|
|
20
|
+
const [groupName, commandName, ...positional] = parsed.words;
|
|
21
|
+
const wantsHelp = parsed.flags.help === true;
|
|
22
|
+
if (!groupName || groupName === "help") {
|
|
23
|
+
io.out(overview());
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
const group = groupNamed(groupName);
|
|
27
|
+
if (!group) {
|
|
28
|
+
io.err(`No such group "${groupName}". Run \`sg\` for the list.`);
|
|
29
|
+
return 2;
|
|
30
|
+
}
|
|
31
|
+
if (!commandName || wantsHelp) {
|
|
32
|
+
io.out(groupHelp(group));
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
const command = commandNamed(group, commandName);
|
|
36
|
+
if (!command) {
|
|
37
|
+
io.err(`No such command "${groupName} ${commandName}".`);
|
|
38
|
+
io.err(groupHelp(group));
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const base = opts.env ?? process.env;
|
|
43
|
+
const loaded = opts.cwd
|
|
44
|
+
? loadEnv({
|
|
45
|
+
cwd: opts.cwd,
|
|
46
|
+
base,
|
|
47
|
+
explicit: flagged(parsed.flags["env-file"]),
|
|
48
|
+
store: store(base, flagged(parsed.flags.store) ?? opts.store),
|
|
49
|
+
})
|
|
50
|
+
: { env: base, source: {} };
|
|
51
|
+
const sg = command.local ? null : connect(credentials(parsed, loaded.env), opts.fetch);
|
|
52
|
+
const reply = await command.run(context(sg, parsed, positional, loaded));
|
|
53
|
+
io.out(render(reply, parsed.flags.json === true));
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
return failed(err, io);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const flagged = (value) => typeof value === "string" ? value : undefined;
|
|
61
|
+
function store(base, override) {
|
|
62
|
+
const path = override ?? storePath(base);
|
|
63
|
+
return { path, values: readStore(path) };
|
|
64
|
+
}
|
|
65
|
+
function failed(err, io) {
|
|
66
|
+
if (err instanceof UsageError) {
|
|
67
|
+
io.err(err.message);
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
if (err instanceof SpendgraphError) {
|
|
71
|
+
io.err(`${err.status} ${err.code}: ${err.message}`);
|
|
72
|
+
if (err.status === 401 || err.status === 403)
|
|
73
|
+
io.err(HINT);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync, rmSync } from "node:fs";
|
|
2
|
+
import { UsageError } from "../errors.js";
|
|
3
|
+
import { readStore, SETTABLE, storePath, writeStore } from "../store.js";
|
|
4
|
+
const SECRET = new Set(["SPENDGRAPH_API_KEY", "SPENDGRAPH_SESSION"]);
|
|
5
|
+
const FLAGS = {
|
|
6
|
+
SPENDGRAPH_BASE_URL: "base-url",
|
|
7
|
+
SPENDGRAPH_API_KEY: "api-key",
|
|
8
|
+
SPENDGRAPH_SESSION: "session",
|
|
9
|
+
SPENDGRAPH_PROJECT: "project",
|
|
10
|
+
};
|
|
11
|
+
const mask = (value) => value.length <= 8 ? "…" : `${value.slice(0, 4)}…${value.slice(-4)}`;
|
|
12
|
+
const shown = (variable, value) => (SECRET.has(variable) ? mask(value) : value);
|
|
13
|
+
const where = (ctx) => ctx.flag("store") ?? storePath(ctx.loaded.env);
|
|
14
|
+
const NEEDS = {
|
|
15
|
+
either: "an api key or a session",
|
|
16
|
+
session: "a dashboard session",
|
|
17
|
+
key: "an api key",
|
|
18
|
+
};
|
|
19
|
+
const ACCESS = [
|
|
20
|
+
{ group: "prompt", read: "either", write: "session" },
|
|
21
|
+
{ group: "tool", read: "either", write: "session" },
|
|
22
|
+
{ group: "spend", read: "either", write: "key" },
|
|
23
|
+
{ group: "key", read: "session", write: "session" },
|
|
24
|
+
{ group: "project", read: "session", write: "session" },
|
|
25
|
+
{ group: "pricing", read: "session", write: "session" },
|
|
26
|
+
{ group: "credential", read: "session", write: "session" },
|
|
27
|
+
];
|
|
28
|
+
function set(ctx) {
|
|
29
|
+
const path = where(ctx);
|
|
30
|
+
const current = readStore(path);
|
|
31
|
+
const given = Object.fromEntries(SETTABLE.map((variable) => [variable, ctx.flag(FLAGS[variable])]).filter(([, v]) => v));
|
|
32
|
+
if (!Object.keys(given).length) {
|
|
33
|
+
throw new UsageError(`Give at least one of ${SETTABLE.map((v) => `--${FLAGS[v]}`).join(", ")}.`);
|
|
34
|
+
}
|
|
35
|
+
const merged = { ...current, ...given };
|
|
36
|
+
writeStore(path, merged);
|
|
37
|
+
return {
|
|
38
|
+
saved: Object.keys(given).map((variable) => ({
|
|
39
|
+
variable,
|
|
40
|
+
value: shown(variable, merged[variable]),
|
|
41
|
+
file: path,
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function unset(ctx) {
|
|
46
|
+
const path = where(ctx);
|
|
47
|
+
const variable = ctx.need(0, "VARIABLE").toUpperCase();
|
|
48
|
+
if (!SETTABLE.includes(variable)) {
|
|
49
|
+
throw new UsageError(`Not a setting: ${variable}. One of ${SETTABLE.join(", ")}.`);
|
|
50
|
+
}
|
|
51
|
+
const current = readStore(path);
|
|
52
|
+
delete current[variable];
|
|
53
|
+
writeStore(path, current);
|
|
54
|
+
return { removed: [{ variable, file: path }] };
|
|
55
|
+
}
|
|
56
|
+
/** The global config, and what the CLI resolved from it, a `.env` and the environment. */
|
|
57
|
+
export const config = {
|
|
58
|
+
name: "config",
|
|
59
|
+
summary: "credentials kept globally, and what the CLI resolved",
|
|
60
|
+
commands: [
|
|
61
|
+
{
|
|
62
|
+
name: "show",
|
|
63
|
+
summary: "the credentials in effect and which layer each came from",
|
|
64
|
+
local: true,
|
|
65
|
+
run: async (ctx) => ({
|
|
66
|
+
settings: SETTABLE.map((variable) => {
|
|
67
|
+
const value = ctx.loaded.env[variable];
|
|
68
|
+
return {
|
|
69
|
+
variable,
|
|
70
|
+
value: value ? shown(variable, value) : "(unset)",
|
|
71
|
+
from: value ? (ctx.loaded.source[variable] ?? "environment") : "-",
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "set",
|
|
78
|
+
summary: "keep a credential globally, so no .env is needed",
|
|
79
|
+
local: true,
|
|
80
|
+
run: async (ctx) => set(ctx),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "unset",
|
|
84
|
+
summary: "drop one from the global config",
|
|
85
|
+
usage: "<VARIABLE>",
|
|
86
|
+
local: true,
|
|
87
|
+
run: async (ctx) => unset(ctx),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "path",
|
|
91
|
+
summary: "where the global config lives, and the .env in effect",
|
|
92
|
+
local: true,
|
|
93
|
+
run: async (ctx) => {
|
|
94
|
+
const path = where(ctx);
|
|
95
|
+
return {
|
|
96
|
+
files: [
|
|
97
|
+
{ file: "config", path, exists: existsSync(path) },
|
|
98
|
+
{ file: ".env", path: ctx.loaded.file ?? "(none found)", exists: !!ctx.loaded.file },
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "clear",
|
|
105
|
+
summary: "delete the global config entirely",
|
|
106
|
+
local: true,
|
|
107
|
+
run: async (ctx) => {
|
|
108
|
+
const path = where(ctx);
|
|
109
|
+
if (!ctx.bool("yes"))
|
|
110
|
+
throw new UsageError(`Pass --yes to delete ${path}.`);
|
|
111
|
+
if (existsSync(path))
|
|
112
|
+
rmSync(path);
|
|
113
|
+
return { cleared: [{ path }] };
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "reaches",
|
|
118
|
+
summary: "which groups your credentials can read, and which they can write",
|
|
119
|
+
local: true,
|
|
120
|
+
run: async (ctx) => {
|
|
121
|
+
const key = !!ctx.loaded.env.SPENDGRAPH_API_KEY;
|
|
122
|
+
const session = !!ctx.loaded.env.SPENDGRAPH_SESSION;
|
|
123
|
+
const held = { key, session, either: key || session };
|
|
124
|
+
return {
|
|
125
|
+
access: ACCESS.map((row) => ({
|
|
126
|
+
group: row.group,
|
|
127
|
+
read: held[row.read],
|
|
128
|
+
write: held[row.write],
|
|
129
|
+
"write needs": NEEDS[row.write],
|
|
130
|
+
})),
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Group } from "../types.js";
|
|
2
|
+
/** Projects, their budgets and who is in them. */
|
|
3
|
+
export declare const projects: Group;
|
|
4
|
+
/** API keys. Minting one needs a dashboard session, never a key. */
|
|
5
|
+
export declare const keys: Group;
|
|
6
|
+
/** Provider keys the server calls out with. Session only, deliberately. */
|
|
7
|
+
export declare const credentials: Group;
|
|
8
|
+
/** What a model costs, and what the catalogue knows. */
|
|
9
|
+
export declare const pricing: Group;
|
|
10
|
+
/** What was spent, by whom, on what. */
|
|
11
|
+
export declare const spend: Group;
|