@manfred-kunze-dev/backbone-cli 2.12.1 → 2.13.0-dev.10
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 +21 -4
- package/dist/commands/analytics.js +3 -1
- package/dist/commands/auth.js +28 -9
- package/dist/commands/config.js +23 -8
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +149 -0
- package/dist/commands/extractions.js +3 -1
- package/dist/commands/prompts.js +3 -1
- package/dist/commands/schemas.js +3 -1
- package/dist/index.js +9 -3
- package/dist/lib/client.js +2 -2
- package/dist/lib/config.d.ts +21 -5
- package/dist/lib/config.js +96 -15
- package/dist/lib/context.d.ts +1 -1
- package/dist/lib/context.js +8 -7
- package/dist/lib/errors.js +1 -0
- package/dist/lib/update-notifier.d.ts +9 -0
- package/dist/lib/update-notifier.js +96 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -5,9 +5,15 @@ A command-line interface for the [Backbone AI](https://backbone.manfred-kunze.de
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
+
# Latest stable release
|
|
8
9
|
npm install -g @manfred-kunze-dev/backbone-cli
|
|
10
|
+
|
|
11
|
+
# Pre-release (dev channel)
|
|
12
|
+
npm install -g @manfred-kunze-dev/backbone-cli@dev
|
|
9
13
|
```
|
|
10
14
|
|
|
15
|
+
The CLI will notify you when a newer version is available.
|
|
16
|
+
|
|
11
17
|
## Quick Start
|
|
12
18
|
|
|
13
19
|
```bash
|
|
@@ -17,9 +23,6 @@ backbone auth login
|
|
|
17
23
|
# Create a project
|
|
18
24
|
backbone projects create -n "My Project"
|
|
19
25
|
|
|
20
|
-
# Set it as default
|
|
21
|
-
backbone config set project <project-id>
|
|
22
|
-
|
|
23
26
|
# Define a schema and run an extraction
|
|
24
27
|
backbone schemas create -n "Invoice"
|
|
25
28
|
backbone extractions create --schema <id> -m gpt-4o --text "Invoice #123, Total: $500"
|
|
@@ -44,12 +47,26 @@ backbone auth status # Verify credentials
|
|
|
44
47
|
backbone auth logout # Clear stored credentials
|
|
45
48
|
```
|
|
46
49
|
|
|
50
|
+
## Contexts
|
|
51
|
+
|
|
52
|
+
The CLI supports kubectl-style contexts for switching between organizations and environments:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Contexts are created automatically via `backbone auth login`
|
|
56
|
+
backbone context list # List all contexts
|
|
57
|
+
backbone context use acme-corp # Switch active context
|
|
58
|
+
backbone context create staging # Create a new context
|
|
59
|
+
backbone context rename old new # Rename a context
|
|
60
|
+
backbone context delete old # Remove a context
|
|
61
|
+
```
|
|
62
|
+
|
|
47
63
|
## Commands
|
|
48
64
|
|
|
49
65
|
| Command | Description |
|
|
50
66
|
|---------|-------------|
|
|
51
67
|
| `auth` | Login, logout, and check auth status |
|
|
52
68
|
| `config` | Get, set, and list configuration values |
|
|
69
|
+
| `context` | Manage CLI contexts for multiple environments |
|
|
53
70
|
| `projects` | List, create, update, and delete projects |
|
|
54
71
|
| `schemas` | Manage schemas, versions, labels, validation, and testing |
|
|
55
72
|
| `prompts` | Manage prompts, versions, labels, compilation, and testing |
|
|
@@ -60,6 +77,7 @@ backbone auth logout # Clear stored credentials
|
|
|
60
77
|
| `providers` | Manage BYOK AI providers |
|
|
61
78
|
| `analytics` | View project usage analytics |
|
|
62
79
|
| `billing` | Check subscription tier and usage limits |
|
|
80
|
+
| `docs` | Browse API documentation from the terminal |
|
|
63
81
|
|
|
64
82
|
### Global Options
|
|
65
83
|
|
|
@@ -67,7 +85,6 @@ backbone auth logout # Clear stored credentials
|
|
|
67
85
|
|------|-------------|
|
|
68
86
|
| `--api-key <key>` | Override the API key |
|
|
69
87
|
| `--base-url <url>` | Override the base URL (default: `https://backbone.manfred-kunze.dev/api`) |
|
|
70
|
-
| `--project <id>` | Override the project ID |
|
|
71
88
|
| `--json` | Output raw JSON instead of formatted tables |
|
|
72
89
|
| `--no-color` | Disable colored output |
|
|
73
90
|
|
|
@@ -8,7 +8,9 @@ function addDateOptions(cmd) {
|
|
|
8
8
|
.option("--end <date>", "End date (YYYY-MM-DD)");
|
|
9
9
|
}
|
|
10
10
|
export function makeAnalyticsCommand() {
|
|
11
|
-
const cmd = new Command("analytics")
|
|
11
|
+
const cmd = new Command("analytics")
|
|
12
|
+
.description("Project analytics")
|
|
13
|
+
.option("--project <id>", "Project ID");
|
|
12
14
|
const summary = new Command("summary").description("Get summary analytics");
|
|
13
15
|
addDateOptions(summary);
|
|
14
16
|
summary.action(async (opts, command) => {
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { store, resolveConfig } from "../lib/config.js";
|
|
3
|
+
import { store, resolveConfig, isJsonOutput, getActiveContextName, getActiveContext, getContextCount, setContext, setActiveContext, deleteContext, } from "../lib/config.js";
|
|
4
4
|
import { getClient, runAction } from "../lib/client.js";
|
|
5
|
-
import { isJsonOutput } from "../lib/config.js";
|
|
6
5
|
import { createInterface } from "node:readline/promises";
|
|
7
6
|
export function makeAuthCommand() {
|
|
8
7
|
const cmd = new Command("auth").description("Manage authentication");
|
|
@@ -14,6 +13,8 @@ export function makeAuthCommand() {
|
|
|
14
13
|
.action(async (opts) => {
|
|
15
14
|
let apiKey = opts.apiKey;
|
|
16
15
|
let baseUrl = opts.baseUrl;
|
|
16
|
+
const contextName = getActiveContextName();
|
|
17
|
+
const currentCtx = getActiveContext();
|
|
17
18
|
if (!apiKey || !baseUrl) {
|
|
18
19
|
const rl = createInterface({
|
|
19
20
|
input: process.stdin,
|
|
@@ -21,9 +22,10 @@ export function makeAuthCommand() {
|
|
|
21
22
|
});
|
|
22
23
|
try {
|
|
23
24
|
if (!baseUrl) {
|
|
24
|
-
|
|
25
|
+
const defaultUrl = currentCtx?.baseUrl ?? "https://backbone.manfred-kunze.dev/api";
|
|
26
|
+
baseUrl = await rl.question(`Base URL [${defaultUrl}]: `);
|
|
25
27
|
if (!baseUrl)
|
|
26
|
-
baseUrl =
|
|
28
|
+
baseUrl = defaultUrl;
|
|
27
29
|
}
|
|
28
30
|
if (!apiKey) {
|
|
29
31
|
apiKey = await rl.question("API Key (sk_...): ");
|
|
@@ -37,8 +39,8 @@ export function makeAuthCommand() {
|
|
|
37
39
|
console.error(chalk.red("API key is required."));
|
|
38
40
|
process.exit(1);
|
|
39
41
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
setContext(contextName, { apiKey, baseUrl });
|
|
43
|
+
setActiveContext(contextName);
|
|
42
44
|
console.log(chalk.green("Credentials saved successfully."));
|
|
43
45
|
console.log(chalk.dim(`Config stored at: ${store.path}`));
|
|
44
46
|
});
|
|
@@ -46,9 +48,16 @@ export function makeAuthCommand() {
|
|
|
46
48
|
.command("logout")
|
|
47
49
|
.description("Clear stored credentials")
|
|
48
50
|
.action(() => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
const contextName = getActiveContextName();
|
|
52
|
+
const count = getContextCount();
|
|
53
|
+
if (count <= 1) {
|
|
54
|
+
// Last context — clear it by resetting the store
|
|
55
|
+
store.store = { activeContext: "default", contexts: {} };
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
deleteContext(contextName);
|
|
59
|
+
console.log(chalk.dim(`Removed context "${contextName}". Switched to "${getActiveContextName()}".`));
|
|
60
|
+
}
|
|
52
61
|
console.log(chalk.green("Credentials cleared."));
|
|
53
62
|
});
|
|
54
63
|
cmd
|
|
@@ -73,6 +82,8 @@ export function makeAuthCommand() {
|
|
|
73
82
|
}
|
|
74
83
|
const { apiKey, baseUrl } = config;
|
|
75
84
|
const keyPreview = apiKey.slice(0, 7) + "..." + apiKey.slice(-4);
|
|
85
|
+
const multiContext = getContextCount() > 1;
|
|
86
|
+
const contextName = getActiveContextName();
|
|
76
87
|
// Try to validate by listing models (lightweight call)
|
|
77
88
|
try {
|
|
78
89
|
const client = getClient(command);
|
|
@@ -81,6 +92,7 @@ export function makeAuthCommand() {
|
|
|
81
92
|
if (json) {
|
|
82
93
|
console.log(JSON.stringify({
|
|
83
94
|
authenticated: true,
|
|
95
|
+
...(multiContext ? { context: contextName } : {}),
|
|
84
96
|
baseUrl,
|
|
85
97
|
apiKeyPreview: keyPreview,
|
|
86
98
|
modelCount,
|
|
@@ -88,6 +100,9 @@ export function makeAuthCommand() {
|
|
|
88
100
|
}
|
|
89
101
|
else {
|
|
90
102
|
console.log(chalk.green("Authenticated"));
|
|
103
|
+
if (multiContext) {
|
|
104
|
+
console.log(` Context: ${contextName}`);
|
|
105
|
+
}
|
|
91
106
|
console.log(` Base URL: ${baseUrl}`);
|
|
92
107
|
console.log(` API Key: ${keyPreview}`);
|
|
93
108
|
console.log(` Models: ${modelCount} available`);
|
|
@@ -97,6 +112,7 @@ export function makeAuthCommand() {
|
|
|
97
112
|
if (json) {
|
|
98
113
|
console.log(JSON.stringify({
|
|
99
114
|
authenticated: false,
|
|
115
|
+
...(multiContext ? { context: contextName } : {}),
|
|
100
116
|
baseUrl,
|
|
101
117
|
apiKeyPreview: keyPreview,
|
|
102
118
|
error: "Failed to validate credentials",
|
|
@@ -104,6 +120,9 @@ export function makeAuthCommand() {
|
|
|
104
120
|
}
|
|
105
121
|
else {
|
|
106
122
|
console.log(chalk.yellow("Credentials configured but validation failed."));
|
|
123
|
+
if (multiContext) {
|
|
124
|
+
console.log(` Context: ${contextName}`);
|
|
125
|
+
}
|
|
107
126
|
console.log(` Base URL: ${baseUrl}`);
|
|
108
127
|
console.log(` API Key: ${keyPreview}`);
|
|
109
128
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { store, isJsonOutput } from "../lib/config.js";
|
|
4
|
-
const ALLOWED_KEYS = ["apiKey", "baseUrl"
|
|
3
|
+
import { store, isJsonOutput, getActiveContext, getActiveContextName, getContextCount, setContext, } from "../lib/config.js";
|
|
4
|
+
const ALLOWED_KEYS = ["apiKey", "baseUrl"];
|
|
5
5
|
const KEY_ALIASES = {
|
|
6
6
|
"api-key": "apiKey",
|
|
7
7
|
"base-url": "baseUrl",
|
|
8
8
|
};
|
|
9
9
|
function validateKey(key) {
|
|
10
|
+
if (key === "project") {
|
|
11
|
+
throw new Error('Project is no longer a stored config. Use --project <id> on each command.');
|
|
12
|
+
}
|
|
10
13
|
const normalized = KEY_ALIASES[key] ?? key;
|
|
11
14
|
if (!ALLOWED_KEYS.includes(normalized)) {
|
|
12
15
|
throw new Error(`Unknown config key: "${key}". Allowed keys: ${ALLOWED_KEYS.join(", ")}`);
|
|
@@ -22,7 +25,14 @@ export function makeConfigCommand() {
|
|
|
22
25
|
.argument("<value>", "Config value")
|
|
23
26
|
.action((key, value, _opts, command) => {
|
|
24
27
|
const validKey = validateKey(key);
|
|
25
|
-
|
|
28
|
+
// Update the active context
|
|
29
|
+
const contextName = getActiveContextName();
|
|
30
|
+
const ctx = getActiveContext() ?? {
|
|
31
|
+
apiKey: "",
|
|
32
|
+
baseUrl: "https://backbone.manfred-kunze.dev/api",
|
|
33
|
+
};
|
|
34
|
+
ctx[validKey] = value;
|
|
35
|
+
setContext(contextName, ctx);
|
|
26
36
|
if (isJsonOutput(command)) {
|
|
27
37
|
console.log(JSON.stringify({ key: validKey, value }));
|
|
28
38
|
}
|
|
@@ -36,7 +46,8 @@ export function makeConfigCommand() {
|
|
|
36
46
|
.argument("<key>", `Config key (${ALLOWED_KEYS.join(", ")})`)
|
|
37
47
|
.action((key, _opts, command) => {
|
|
38
48
|
const validKey = validateKey(key);
|
|
39
|
-
const
|
|
49
|
+
const ctx = getActiveContext();
|
|
50
|
+
const value = ctx?.[validKey];
|
|
40
51
|
if (isJsonOutput(command)) {
|
|
41
52
|
console.log(JSON.stringify({ key: validKey, value: value ?? null }));
|
|
42
53
|
}
|
|
@@ -51,10 +62,11 @@ export function makeConfigCommand() {
|
|
|
51
62
|
.command("list")
|
|
52
63
|
.description("Show all configuration values")
|
|
53
64
|
.action((_opts, command) => {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
const ctx = getActiveContext();
|
|
66
|
+
const values = {
|
|
67
|
+
apiKey: ctx?.apiKey,
|
|
68
|
+
baseUrl: ctx?.baseUrl,
|
|
69
|
+
};
|
|
58
70
|
if (isJsonOutput(command)) {
|
|
59
71
|
console.log(JSON.stringify(values, null, 2));
|
|
60
72
|
}
|
|
@@ -67,6 +79,9 @@ export function makeConfigCommand() {
|
|
|
67
79
|
: value;
|
|
68
80
|
console.log(`${chalk.cyan(key)}: ${display}`);
|
|
69
81
|
}
|
|
82
|
+
if (getContextCount() > 1) {
|
|
83
|
+
console.log(chalk.dim(`\nContext: ${getActiveContextName()} (use "backbone context list" to see all)`));
|
|
84
|
+
}
|
|
70
85
|
console.log(chalk.dim(`\nConfig file: ${store.path}`));
|
|
71
86
|
}
|
|
72
87
|
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import Table from "cli-table3";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { store, isJsonOutput, getAllContexts, getActiveContextName, getContextCount, setContext, deleteContext, renameContext, setActiveContext, validateContextName, } from "../lib/config.js";
|
|
6
|
+
function maskApiKey(key) {
|
|
7
|
+
if (key.length <= 11)
|
|
8
|
+
return "****";
|
|
9
|
+
return key.slice(0, 7) + "..." + key.slice(-4);
|
|
10
|
+
}
|
|
11
|
+
export function makeContextCommand() {
|
|
12
|
+
const cmd = new Command("context").description("Manage CLI contexts for multiple environments");
|
|
13
|
+
cmd
|
|
14
|
+
.command("list")
|
|
15
|
+
.description("List all contexts")
|
|
16
|
+
.action((_opts, command) => {
|
|
17
|
+
const contexts = getAllContexts();
|
|
18
|
+
const activeName = getActiveContextName();
|
|
19
|
+
if (isJsonOutput(command)) {
|
|
20
|
+
console.log(JSON.stringify({ activeContext: activeName, contexts }, null, 2));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const names = Object.keys(contexts);
|
|
24
|
+
if (names.length === 0) {
|
|
25
|
+
console.log(chalk.dim('No contexts configured. Run "backbone auth login" to get started.'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const table = new Table({
|
|
29
|
+
head: ["", "NAME", "BASE URL", "API KEY"].map((h) => chalk.cyan(h)),
|
|
30
|
+
});
|
|
31
|
+
for (const name of names) {
|
|
32
|
+
const ctx = contexts[name];
|
|
33
|
+
table.push([
|
|
34
|
+
name === activeName ? chalk.green("*") : "",
|
|
35
|
+
name,
|
|
36
|
+
ctx.baseUrl,
|
|
37
|
+
maskApiKey(ctx.apiKey),
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
console.log(table.toString());
|
|
41
|
+
});
|
|
42
|
+
cmd
|
|
43
|
+
.command("use")
|
|
44
|
+
.description("Switch active context")
|
|
45
|
+
.argument("<name>", "Context name")
|
|
46
|
+
.action((name, _opts, command) => {
|
|
47
|
+
setActiveContext(name);
|
|
48
|
+
if (isJsonOutput(command)) {
|
|
49
|
+
console.log(JSON.stringify({ activeContext: name }));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(chalk.green(`Switched to context "${name}".`));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
cmd
|
|
56
|
+
.command("create")
|
|
57
|
+
.description("Create a new context")
|
|
58
|
+
.argument("<name>", "Context name")
|
|
59
|
+
.option("--base-url <url>", "Base URL")
|
|
60
|
+
.option("--api-key <key>", "API key")
|
|
61
|
+
.action(async (name, opts) => {
|
|
62
|
+
validateContextName(name);
|
|
63
|
+
const contexts = getAllContexts();
|
|
64
|
+
if (contexts[name]) {
|
|
65
|
+
throw new Error(`Context "${name}" already exists. Delete it first or choose a different name.`);
|
|
66
|
+
}
|
|
67
|
+
let baseUrl = opts.baseUrl;
|
|
68
|
+
let apiKey = opts.apiKey;
|
|
69
|
+
if (!baseUrl || !apiKey) {
|
|
70
|
+
const rl = createInterface({
|
|
71
|
+
input: process.stdin,
|
|
72
|
+
output: process.stdout,
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
if (!baseUrl) {
|
|
76
|
+
baseUrl = await rl.question(`Base URL [https://backbone.manfred-kunze.dev/api]: `);
|
|
77
|
+
if (!baseUrl)
|
|
78
|
+
baseUrl = "https://backbone.manfred-kunze.dev/api";
|
|
79
|
+
}
|
|
80
|
+
if (!apiKey) {
|
|
81
|
+
apiKey = await rl.question("API Key (sk_...): ");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
rl.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!apiKey) {
|
|
89
|
+
console.error(chalk.red("API key is required."));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const entry = { baseUrl, apiKey };
|
|
93
|
+
setContext(name, entry);
|
|
94
|
+
// Auto-set as active if it's the first context
|
|
95
|
+
if (getContextCount() === 1) {
|
|
96
|
+
setActiveContext(name);
|
|
97
|
+
}
|
|
98
|
+
console.log(chalk.green(`Context "${name}" created.`));
|
|
99
|
+
console.log(chalk.dim(`Config stored at: ${store.path}`));
|
|
100
|
+
});
|
|
101
|
+
cmd
|
|
102
|
+
.command("current")
|
|
103
|
+
.description("Show the active context")
|
|
104
|
+
.action((_opts, command) => {
|
|
105
|
+
const name = getActiveContextName();
|
|
106
|
+
const contexts = getAllContexts();
|
|
107
|
+
const ctx = contexts[name];
|
|
108
|
+
if (isJsonOutput(command)) {
|
|
109
|
+
console.log(JSON.stringify({ name, ...(ctx ?? {}) }, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!ctx) {
|
|
113
|
+
console.log(chalk.yellow('No active context configured. Run "backbone auth login" to get started.'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(`${chalk.cyan("Context")}: ${name}`);
|
|
117
|
+
console.log(`${chalk.cyan("Base URL")}: ${ctx.baseUrl}`);
|
|
118
|
+
console.log(`${chalk.cyan("API Key")}: ${maskApiKey(ctx.apiKey)}`);
|
|
119
|
+
});
|
|
120
|
+
cmd
|
|
121
|
+
.command("delete")
|
|
122
|
+
.description("Delete a context")
|
|
123
|
+
.argument("<name>", "Context name")
|
|
124
|
+
.action((name, _opts, command) => {
|
|
125
|
+
deleteContext(name);
|
|
126
|
+
if (isJsonOutput(command)) {
|
|
127
|
+
console.log(JSON.stringify({ deleted: name }));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
console.log(chalk.green(`Context "${name}" deleted.`));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
cmd
|
|
134
|
+
.command("rename")
|
|
135
|
+
.description("Rename a context")
|
|
136
|
+
.argument("<old>", "Current name")
|
|
137
|
+
.argument("<new>", "New name")
|
|
138
|
+
.action((oldName, newName, _opts, command) => {
|
|
139
|
+
renameContext(oldName, newName);
|
|
140
|
+
if (isJsonOutput(command)) {
|
|
141
|
+
console.log(JSON.stringify({ from: oldName, to: newName }));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(chalk.green(`Context "${oldName}" renamed to "${newName}".`));
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return cmd;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -23,7 +23,9 @@ function readImages(paths) {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
export function makeExtractionsCommand() {
|
|
26
|
-
const cmd = new Command("extractions")
|
|
26
|
+
const cmd = new Command("extractions")
|
|
27
|
+
.description("Manage extractions")
|
|
28
|
+
.option("--project <id>", "Project ID");
|
|
27
29
|
cmd
|
|
28
30
|
.command("create")
|
|
29
31
|
.description("Create a new extraction")
|
package/dist/commands/prompts.js
CHANGED
|
@@ -6,7 +6,9 @@ import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
|
|
|
6
6
|
import { makePromptVersionsCommand } from "./prompt-versions.js";
|
|
7
7
|
import { makePromptLabelsCommand } from "./prompt-labels.js";
|
|
8
8
|
export function makePromptsCommand() {
|
|
9
|
-
const cmd = new Command("prompts")
|
|
9
|
+
const cmd = new Command("prompts")
|
|
10
|
+
.description("Manage prompts")
|
|
11
|
+
.option("--project <id>", "Project ID");
|
|
10
12
|
const list = new Command("list").description("List prompts");
|
|
11
13
|
addPaginationOptions(list);
|
|
12
14
|
list.option("-s, --search <term>", "Filter by name");
|
package/dist/commands/schemas.js
CHANGED
|
@@ -8,7 +8,9 @@ import { ensureOpenAiCompatible } from "../lib/schema-compat.js";
|
|
|
8
8
|
import { makeSchemaVersionsCommand } from "./schema-versions.js";
|
|
9
9
|
import { makeSchemaLabelsCommand } from "./schema-labels.js";
|
|
10
10
|
export function makeSchemasCommand() {
|
|
11
|
-
const cmd = new Command("schemas")
|
|
11
|
+
const cmd = new Command("schemas")
|
|
12
|
+
.description("Manage schemas")
|
|
13
|
+
.option("--project <id>", "Project ID");
|
|
12
14
|
const list = new Command("list").description("List schemas");
|
|
13
15
|
addPaginationOptions(list);
|
|
14
16
|
list.option("-s, --search <term>", "Filter by name");
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { Command } from "commander";
|
|
3
4
|
import { makeAuthCommand } from "./commands/auth.js";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const pkg = require("../package.json");
|
|
4
7
|
import { makeConfigCommand } from "./commands/config.js";
|
|
5
8
|
import { makeProjectsCommand } from "./commands/projects.js";
|
|
6
9
|
import { makeSchemasCommand } from "./commands/schemas.js";
|
|
@@ -13,18 +16,21 @@ import { makeProvidersCommand } from "./commands/providers.js";
|
|
|
13
16
|
import { makeAnalyticsCommand } from "./commands/analytics.js";
|
|
14
17
|
import { makeBillingCommand } from "./commands/billing.js";
|
|
15
18
|
import { makeDocsCommand } from "./commands/docs.js";
|
|
19
|
+
import { makeContextCommand } from "./commands/context.js";
|
|
20
|
+
import { checkForUpdates } from "./lib/update-notifier.js";
|
|
21
|
+
const updater = checkForUpdates(pkg.version);
|
|
16
22
|
const program = new Command();
|
|
17
23
|
program
|
|
18
24
|
.name("backbone")
|
|
19
25
|
.description("CLI for the Backbone AI platform")
|
|
20
|
-
.version(
|
|
26
|
+
.version(pkg.version)
|
|
21
27
|
.option("--api-key <key>", "API key (overrides config)")
|
|
22
28
|
.option("--base-url <url>", "Base URL (overrides config)")
|
|
23
|
-
.option("--project <id>", "Project ID (overrides config)")
|
|
24
29
|
.option("--json", "Output as JSON")
|
|
25
30
|
.option("--no-color", "Disable colored output");
|
|
26
31
|
program.addCommand(makeAuthCommand());
|
|
27
32
|
program.addCommand(makeConfigCommand());
|
|
33
|
+
program.addCommand(makeContextCommand());
|
|
28
34
|
program.addCommand(makeProjectsCommand());
|
|
29
35
|
program.addCommand(makeSchemasCommand());
|
|
30
36
|
program.addCommand(makePromptsCommand());
|
|
@@ -36,5 +42,5 @@ program.addCommand(makeProvidersCommand());
|
|
|
36
42
|
program.addCommand(makeAnalyticsCommand());
|
|
37
43
|
program.addCommand(makeBillingCommand());
|
|
38
44
|
program.addCommand(makeDocsCommand());
|
|
39
|
-
program.
|
|
45
|
+
program.parseAsync().then(() => updater.notify());
|
|
40
46
|
//# sourceMappingURL=index.js.map
|
package/dist/lib/client.js
CHANGED
|
@@ -17,8 +17,8 @@ const errorMiddleware = {
|
|
|
17
17
|
}
|
|
18
18
|
throw new BackboneApiError({
|
|
19
19
|
status: body.status ?? response.status,
|
|
20
|
-
error: body.error ?? response.statusText,
|
|
21
|
-
message: body.message ?? `HTTP ${response.status}: ${response.statusText}`,
|
|
20
|
+
error: body.title ?? body.error ?? response.statusText,
|
|
21
|
+
message: body.detail ?? body.message ?? `HTTP ${response.status}: ${response.statusText}`,
|
|
22
22
|
timestamp: body.timestamp ?? new Date().toISOString(),
|
|
23
23
|
});
|
|
24
24
|
},
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import Conf from "conf";
|
|
2
2
|
import type { Command } from "commander";
|
|
3
|
+
export interface ContextEntry {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
apiKey: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BackboneConfigStore {
|
|
8
|
+
activeContext: string;
|
|
9
|
+
contexts: Record<string, ContextEntry>;
|
|
10
|
+
}
|
|
3
11
|
export interface BackboneConfig {
|
|
4
12
|
apiKey: string;
|
|
5
13
|
baseUrl: string;
|
|
6
|
-
project?: string;
|
|
7
14
|
}
|
|
8
|
-
declare const store: Conf<
|
|
15
|
+
declare const store: Conf<BackboneConfigStore>;
|
|
9
16
|
export { store };
|
|
17
|
+
export declare function validateContextName(name: string): void;
|
|
18
|
+
export declare function getActiveContextName(): string;
|
|
19
|
+
export declare function getActiveContext(): ContextEntry | undefined;
|
|
20
|
+
export declare function getAllContexts(): Record<string, ContextEntry>;
|
|
21
|
+
export declare function getContextCount(): number;
|
|
22
|
+
export declare function setContext(name: string, entry: ContextEntry): void;
|
|
23
|
+
export declare function deleteContext(name: string): void;
|
|
24
|
+
export declare function renameContext(oldName: string, newName: string): void;
|
|
25
|
+
export declare function setActiveContext(name: string): void;
|
|
10
26
|
/**
|
|
11
27
|
* Resolve configuration with priority:
|
|
12
|
-
* 1. CLI flags (--api-key, --base-url
|
|
13
|
-
* 2. Environment variables (BACKBONE_API_KEY, BACKBONE_BASE_URL
|
|
28
|
+
* 1. CLI flags (--api-key, --base-url)
|
|
29
|
+
* 2. Environment variables (BACKBONE_API_KEY, BACKBONE_BASE_URL)
|
|
14
30
|
* 3. Local .backbone file
|
|
15
|
-
* 4.
|
|
31
|
+
* 4. Active context from config store
|
|
16
32
|
*/
|
|
17
33
|
export declare function resolveConfig(command: Command): BackboneConfig;
|
|
18
34
|
/** Check if --json flag is set on root command */
|
package/dist/lib/config.js
CHANGED
|
@@ -4,13 +4,96 @@ import { resolve } from "node:path";
|
|
|
4
4
|
const store = new Conf({
|
|
5
5
|
projectName: "backbone",
|
|
6
6
|
projectSuffix: "",
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
project: { type: "string" },
|
|
7
|
+
defaults: {
|
|
8
|
+
activeContext: "default",
|
|
9
|
+
contexts: {},
|
|
11
10
|
},
|
|
12
11
|
});
|
|
12
|
+
// Migrate from old flat config format (apiKey, baseUrl, project) to contexts
|
|
13
|
+
(function migrateIfNeeded() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = JSON.parse(readFileSync(store.path, "utf-8"));
|
|
16
|
+
if (typeof raw.apiKey === "string" && !raw.contexts) {
|
|
17
|
+
store.store = {
|
|
18
|
+
activeContext: "default",
|
|
19
|
+
contexts: {
|
|
20
|
+
default: {
|
|
21
|
+
apiKey: raw.apiKey,
|
|
22
|
+
baseUrl: raw.baseUrl ?? "https://backbone.manfred-kunze.dev/api",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// File doesn't exist or is unparseable — nothing to migrate
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
13
32
|
export { store };
|
|
33
|
+
const CONTEXT_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
34
|
+
export function validateContextName(name) {
|
|
35
|
+
if (!CONTEXT_NAME_RE.test(name)) {
|
|
36
|
+
throw new Error("Context name must contain only letters, numbers, hyphens, and underscores.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function getActiveContextName() {
|
|
40
|
+
return store.get("activeContext") ?? "default";
|
|
41
|
+
}
|
|
42
|
+
export function getActiveContext() {
|
|
43
|
+
const name = getActiveContextName();
|
|
44
|
+
const contexts = store.get("contexts") ?? {};
|
|
45
|
+
return contexts[name];
|
|
46
|
+
}
|
|
47
|
+
export function getAllContexts() {
|
|
48
|
+
return store.get("contexts") ?? {};
|
|
49
|
+
}
|
|
50
|
+
export function getContextCount() {
|
|
51
|
+
return Object.keys(getAllContexts()).length;
|
|
52
|
+
}
|
|
53
|
+
export function setContext(name, entry) {
|
|
54
|
+
validateContextName(name);
|
|
55
|
+
const contexts = getAllContexts();
|
|
56
|
+
contexts[name] = entry;
|
|
57
|
+
store.set("contexts", contexts);
|
|
58
|
+
}
|
|
59
|
+
export function deleteContext(name) {
|
|
60
|
+
const contexts = getAllContexts();
|
|
61
|
+
if (!contexts[name]) {
|
|
62
|
+
throw new Error(`Context "${name}" does not exist.`);
|
|
63
|
+
}
|
|
64
|
+
if (Object.keys(contexts).length === 1) {
|
|
65
|
+
throw new Error("Cannot delete the last remaining context.");
|
|
66
|
+
}
|
|
67
|
+
delete contexts[name];
|
|
68
|
+
store.set("contexts", contexts);
|
|
69
|
+
if (getActiveContextName() === name) {
|
|
70
|
+
const remaining = Object.keys(contexts)[0];
|
|
71
|
+
store.set("activeContext", remaining);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function renameContext(oldName, newName) {
|
|
75
|
+
validateContextName(newName);
|
|
76
|
+
const contexts = getAllContexts();
|
|
77
|
+
if (!contexts[oldName]) {
|
|
78
|
+
throw new Error(`Context "${oldName}" does not exist.`);
|
|
79
|
+
}
|
|
80
|
+
if (contexts[newName]) {
|
|
81
|
+
throw new Error(`Context "${newName}" already exists.`);
|
|
82
|
+
}
|
|
83
|
+
contexts[newName] = contexts[oldName];
|
|
84
|
+
delete contexts[oldName];
|
|
85
|
+
store.set("contexts", contexts);
|
|
86
|
+
if (getActiveContextName() === oldName) {
|
|
87
|
+
store.set("activeContext", newName);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function setActiveContext(name) {
|
|
91
|
+
const contexts = getAllContexts();
|
|
92
|
+
if (!contexts[name]) {
|
|
93
|
+
throw new Error(`Context "${name}" does not exist. Available contexts: ${Object.keys(contexts).join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
store.set("activeContext", name);
|
|
96
|
+
}
|
|
14
97
|
/**
|
|
15
98
|
* Read .backbone file from current directory (if it exists).
|
|
16
99
|
*/
|
|
@@ -19,7 +102,8 @@ function readLocalFile() {
|
|
|
19
102
|
if (!existsSync(localPath))
|
|
20
103
|
return {};
|
|
21
104
|
try {
|
|
22
|
-
|
|
105
|
+
const parsed = JSON.parse(readFileSync(localPath, "utf-8"));
|
|
106
|
+
return { apiKey: parsed.apiKey, baseUrl: parsed.baseUrl };
|
|
23
107
|
}
|
|
24
108
|
catch {
|
|
25
109
|
return {};
|
|
@@ -27,32 +111,29 @@ function readLocalFile() {
|
|
|
27
111
|
}
|
|
28
112
|
/**
|
|
29
113
|
* Resolve configuration with priority:
|
|
30
|
-
* 1. CLI flags (--api-key, --base-url
|
|
31
|
-
* 2. Environment variables (BACKBONE_API_KEY, BACKBONE_BASE_URL
|
|
114
|
+
* 1. CLI flags (--api-key, --base-url)
|
|
115
|
+
* 2. Environment variables (BACKBONE_API_KEY, BACKBONE_BASE_URL)
|
|
32
116
|
* 3. Local .backbone file
|
|
33
|
-
* 4.
|
|
117
|
+
* 4. Active context from config store
|
|
34
118
|
*/
|
|
35
119
|
export function resolveConfig(command) {
|
|
36
120
|
const root = getRootCommand(command);
|
|
37
121
|
const opts = root.opts();
|
|
38
122
|
const local = readLocalFile();
|
|
123
|
+
const activeCtx = getActiveContext();
|
|
39
124
|
const apiKey = opts.apiKey ??
|
|
40
125
|
process.env.BACKBONE_API_KEY ??
|
|
41
126
|
local.apiKey ??
|
|
42
|
-
|
|
127
|
+
activeCtx?.apiKey;
|
|
43
128
|
const baseUrl = opts.baseUrl ??
|
|
44
129
|
process.env.BACKBONE_BASE_URL ??
|
|
45
130
|
local.baseUrl ??
|
|
46
|
-
|
|
131
|
+
activeCtx?.baseUrl ??
|
|
47
132
|
"https://backbone.manfred-kunze.dev/api";
|
|
48
|
-
const project = opts.project ??
|
|
49
|
-
process.env.BACKBONE_PROJECT ??
|
|
50
|
-
local.project ??
|
|
51
|
-
store.get("project");
|
|
52
133
|
if (!apiKey) {
|
|
53
134
|
throw new Error('No API key configured. Run "backbone auth login" or set BACKBONE_API_KEY.');
|
|
54
135
|
}
|
|
55
|
-
return { apiKey, baseUrl
|
|
136
|
+
return { apiKey, baseUrl };
|
|
56
137
|
}
|
|
57
138
|
/** Walk up Commander's parent chain to find root program */
|
|
58
139
|
function getRootCommand(cmd) {
|
package/dist/lib/context.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
/**
|
|
3
|
-
* Resolve the project ID from --project flag
|
|
3
|
+
* Resolve the project ID from --project flag on the command or any parent command.
|
|
4
4
|
* Throws a user-friendly error if no project is configured.
|
|
5
5
|
*/
|
|
6
6
|
export declare function requireProjectId(command: Command): string;
|
package/dist/lib/context.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { resolveConfig } from "./config.js";
|
|
2
1
|
/**
|
|
3
|
-
* Resolve the project ID from --project flag
|
|
2
|
+
* Resolve the project ID from --project flag on the command or any parent command.
|
|
4
3
|
* Throws a user-friendly error if no project is configured.
|
|
5
4
|
*/
|
|
6
5
|
export function requireProjectId(command) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
let cmd = command;
|
|
7
|
+
while (cmd) {
|
|
8
|
+
const project = cmd.opts().project;
|
|
9
|
+
if (project)
|
|
10
|
+
return project;
|
|
11
|
+
cmd = cmd.parent;
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
+
throw new Error("Project ID is required. Use --project <id>.");
|
|
13
14
|
}
|
|
14
15
|
//# sourceMappingURL=context.js.map
|
package/dist/lib/errors.js
CHANGED
|
@@ -13,6 +13,7 @@ export class BackboneApiError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
const HINTS = {
|
|
15
15
|
401: 'Invalid or missing API key. Run "backbone auth login" to configure credentials.',
|
|
16
|
+
402: "Billing limit reached. Check your plan limits or upgrade at the dashboard.",
|
|
16
17
|
403: "You don't have permission for this action. Check your organization role.",
|
|
17
18
|
404: "Resource not found. Verify the ID is correct.",
|
|
18
19
|
409: "Conflict — the resource may already exist or was modified concurrently.",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check for updates in the background. Call early so the fetch
|
|
3
|
+
* runs concurrently with the actual command, then call `notify()`
|
|
4
|
+
* after the command finishes to print the message (if any).
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkForUpdates(currentVersion: string): {
|
|
7
|
+
notify: () => void;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=update-notifier.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
const PKG_NAME = "@manfred-kunze-dev/backbone-cli";
|
|
6
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
7
|
+
const CACHE_DIR = join(homedir(), ".config", "backbone");
|
|
8
|
+
const CACHE_FILE = join(CACHE_DIR, "update-check.json");
|
|
9
|
+
function readCache() {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function writeCache(data) {
|
|
18
|
+
try {
|
|
19
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
20
|
+
writeFileSync(CACHE_FILE, JSON.stringify(data));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Ignore write errors
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function fetchLatestVersion() {
|
|
27
|
+
try {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
30
|
+
const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`, { signal: controller.signal });
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
if (!res.ok)
|
|
33
|
+
return null;
|
|
34
|
+
const data = (await res.json());
|
|
35
|
+
return data.version ?? null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function compareVersions(current, latest) {
|
|
42
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
43
|
+
const [cMaj, cMin, cPat] = parse(current);
|
|
44
|
+
const [lMaj, lMin, lPat] = parse(latest);
|
|
45
|
+
if (lMaj !== cMaj)
|
|
46
|
+
return lMaj > cMaj;
|
|
47
|
+
if (lMin !== cMin)
|
|
48
|
+
return lMin > cMin;
|
|
49
|
+
return lPat > cPat;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check for updates in the background. Call early so the fetch
|
|
53
|
+
* runs concurrently with the actual command, then call `notify()`
|
|
54
|
+
* after the command finishes to print the message (if any).
|
|
55
|
+
*/
|
|
56
|
+
export function checkForUpdates(currentVersion) {
|
|
57
|
+
let message = null;
|
|
58
|
+
// Skip for pre-release versions
|
|
59
|
+
if (currentVersion.includes("-")) {
|
|
60
|
+
return { notify: () => { } };
|
|
61
|
+
}
|
|
62
|
+
const cache = readCache();
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
// Use cached result if recent enough
|
|
65
|
+
if (cache && now - cache.lastCheck < CHECK_INTERVAL_MS) {
|
|
66
|
+
if (compareVersions(currentVersion, cache.latestVersion)) {
|
|
67
|
+
message = formatMessage(currentVersion, cache.latestVersion);
|
|
68
|
+
}
|
|
69
|
+
return { notify: () => message && console.error(message) };
|
|
70
|
+
}
|
|
71
|
+
// Fire off the check in the background
|
|
72
|
+
const pending = fetchLatestVersion().then((latest) => {
|
|
73
|
+
if (latest) {
|
|
74
|
+
writeCache({ lastCheck: now, latestVersion: latest });
|
|
75
|
+
if (compareVersions(currentVersion, latest)) {
|
|
76
|
+
message = formatMessage(currentVersion, latest);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
notify: async () => {
|
|
82
|
+
await pending;
|
|
83
|
+
if (message)
|
|
84
|
+
console.error(message);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function formatMessage(current, latest) {
|
|
89
|
+
return [
|
|
90
|
+
"",
|
|
91
|
+
chalk.yellow(`Update available: ${chalk.dim(current)} → ${chalk.green(latest)}`),
|
|
92
|
+
chalk.yellow(`Run ${chalk.cyan(`npm i -g ${PKG_NAME}`)} to update`),
|
|
93
|
+
"",
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=update-notifier.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manfred-kunze-dev/backbone-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0-dev.10",
|
|
4
4
|
"description": "CLI for the Backbone AI platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"typecheck": "tsc --noEmit"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"chalk": "^5.
|
|
22
|
+
"chalk": "^5.6.2",
|
|
23
23
|
"cli-table3": "^0.6.5",
|
|
24
24
|
"commander": "^13.1.0",
|
|
25
25
|
"conf": "^13.1.0",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^22.19.15",
|
|
31
|
-
"openapi-typescript": "^7.
|
|
32
|
-
"tsx": "^4.
|
|
31
|
+
"openapi-typescript": "^7.13.0",
|
|
32
|
+
"tsx": "^4.21.0",
|
|
33
33
|
"typescript": "^5.9.3"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|