@beaglabs/chaveta 0.0.1

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.
Files changed (2) hide show
  1. package/dist/index.js +303 -0
  2. package/package.json +40 -0
package/dist/index.js ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { program } from "commander";
5
+
6
+ // src/commands/login.ts
7
+ import { createServer } from "http";
8
+ import open from "open";
9
+
10
+ // src/config.ts
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
+ import { homedir } from "os";
13
+ import { join } from "path";
14
+ var CONFIG_DIR = join(homedir(), ".chaveta");
15
+ var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
16
+ function ensureDir() {
17
+ if (!existsSync(CONFIG_DIR)) {
18
+ mkdirSync(CONFIG_DIR, { recursive: true });
19
+ }
20
+ }
21
+ function saveCredentials(creds) {
22
+ ensureDir();
23
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), "utf-8");
24
+ }
25
+ function loadCredentials() {
26
+ if (!existsSync(CREDENTIALS_FILE)) return null;
27
+ try {
28
+ return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function clearCredentials() {
34
+ if (existsSync(CREDENTIALS_FILE)) {
35
+ writeFileSync(CREDENTIALS_FILE, "", "utf-8");
36
+ }
37
+ }
38
+ function getBaseUrl() {
39
+ return process.env.CHAVETA_API_URL ?? "https://chaveta.beaglabs.com";
40
+ }
41
+ function getAuthHeaders() {
42
+ const clientId = process.env.CHAVETA_CLIENT_ID;
43
+ const clientSecret = process.env.CHAVETA_CLIENT_SECRET;
44
+ if (clientId && clientSecret) {
45
+ const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
46
+ return { authorization: `Basic ${basic}` };
47
+ }
48
+ const creds = loadCredentials();
49
+ if (creds?.accessToken) {
50
+ return { authorization: `Bearer ${creds.accessToken}` };
51
+ }
52
+ return {};
53
+ }
54
+
55
+ // src/commands/login.ts
56
+ async function login() {
57
+ const baseUrl = getBaseUrl();
58
+ const port = 9876;
59
+ const redirectUri = `http://localhost:${port}/callback`;
60
+ console.log("Opening browser for authentication...");
61
+ const tokenPromise = new Promise((resolve, reject) => {
62
+ const server = createServer(async (req, res) => {
63
+ const url = new URL(req.url, `http://localhost:${port}`);
64
+ if (url.pathname === "/callback") {
65
+ const code = url.searchParams.get("code");
66
+ if (!code) {
67
+ res.writeHead(400);
68
+ res.end("Missing authorization code");
69
+ reject(new Error("Missing authorization code"));
70
+ server.close();
71
+ return;
72
+ }
73
+ try {
74
+ const tokenRes = await fetch(`${baseUrl}/api/auth/token`, {
75
+ method: "POST",
76
+ headers: { "content-type": "application/json" },
77
+ body: JSON.stringify({
78
+ grant_type: "authorization_code",
79
+ code,
80
+ redirect_uri: redirectUri
81
+ })
82
+ });
83
+ if (!tokenRes.ok) {
84
+ throw new Error(`Token exchange failed: ${tokenRes.status}`);
85
+ }
86
+ const data = await tokenRes.json();
87
+ saveCredentials({
88
+ accessToken: data.access_token,
89
+ refreshToken: data.refresh_token,
90
+ expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
91
+ });
92
+ res.writeHead(200, { "content-type": "text/html" });
93
+ res.end("<html><body><h2>Authenticated! You can close this tab.</h2></body></html>");
94
+ resolve(data.access_token);
95
+ } catch (err) {
96
+ res.writeHead(500);
97
+ res.end("Authentication failed");
98
+ reject(err);
99
+ } finally {
100
+ server.close();
101
+ }
102
+ }
103
+ });
104
+ server.listen(port, () => {
105
+ const authUrl = `${baseUrl}/api/auth/authorize?redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`;
106
+ open(authUrl);
107
+ });
108
+ setTimeout(() => {
109
+ server.close();
110
+ reject(new Error("Login timed out after 120 seconds"));
111
+ }, 12e4);
112
+ });
113
+ await tokenPromise;
114
+ console.log("Logged in successfully.");
115
+ }
116
+
117
+ // src/commands/generate.ts
118
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
119
+ import { join as join2 } from "path";
120
+
121
+ // src/types.ts
122
+ var FOXGLOVE_SCHEMAS = [
123
+ "foxglove.PointCloud",
124
+ "foxglove.GeoJSON",
125
+ "foxglove.Grid",
126
+ "foxglove.LocationFix"
127
+ ];
128
+
129
+ // src/commands/generate.ts
130
+ async function generateCommand(options) {
131
+ if (!FOXGLOVE_SCHEMAS.includes(options.schema)) {
132
+ console.error(`Invalid schema: ${options.schema}`);
133
+ console.error(`Valid schemas: ${FOXGLOVE_SCHEMAS.join(", ")}`);
134
+ process.exit(1);
135
+ }
136
+ const headers = getAuthHeaders();
137
+ if (!headers.authorization) {
138
+ console.error("Not authenticated. Run `chaveta login` or set CHAVETA_CLIENT_ID and CHAVETA_CLIENT_SECRET.");
139
+ process.exit(1);
140
+ }
141
+ const baseUrl = getBaseUrl();
142
+ const outputMode = options.view ? "view" : "stream";
143
+ console.log(`Generating ${options.count} ${options.schema} fixture(s)...`);
144
+ console.log(`Scenario: "${options.scenario}"`);
145
+ const res = await fetch(`${baseUrl}/api/generate`, {
146
+ method: "POST",
147
+ headers: {
148
+ ...headers,
149
+ "content-type": "application/json"
150
+ },
151
+ body: JSON.stringify({
152
+ schema: options.schema,
153
+ scenario: options.scenario,
154
+ count: options.count,
155
+ seed: options.seed,
156
+ outputMode
157
+ })
158
+ });
159
+ if (!res.ok) {
160
+ const err = await res.json().catch(() => ({ error: res.statusText }));
161
+ console.error(`Generation failed: ${err.error ?? res.statusText}`);
162
+ process.exit(1);
163
+ }
164
+ const data = await res.json();
165
+ console.log(`Queued: ${data.id}`);
166
+ const result = await pollForCompletion(baseUrl, data.id, headers);
167
+ if (result.status === "failed") {
168
+ console.error("Generation failed.");
169
+ process.exit(1);
170
+ }
171
+ if (result.view_url) {
172
+ console.log(`
173
+ Preview: ${result.view_url}`);
174
+ console.log(` Expires in 24h
175
+ `);
176
+ }
177
+ if (options.output && result.view_url) {
178
+ const outputDir = options.output;
179
+ if (!existsSync2(outputDir)) {
180
+ mkdirSync2(outputDir, { recursive: true });
181
+ }
182
+ const mcapRes = await fetch(result.view_url, {
183
+ headers: { accept: "application/octet-stream" }
184
+ });
185
+ if (mcapRes.ok) {
186
+ const buffer = Buffer.from(await mcapRes.arrayBuffer());
187
+ const filename = `${data.id}.mcap`;
188
+ writeFileSync2(join2(outputDir, filename), buffer);
189
+ console.log(` Saved: ${join2(outputDir, filename)}`);
190
+ }
191
+ }
192
+ }
193
+ async function pollForCompletion(baseUrl, id, headers, maxAttempts = 60) {
194
+ for (let i = 0; i < maxAttempts; i++) {
195
+ await new Promise((r) => setTimeout(r, 2e3));
196
+ const res = await fetch(`${baseUrl}/api/generate/${id}`, { headers });
197
+ if (!res.ok) continue;
198
+ const data = await res.json();
199
+ if (data.status === "complete" || data.status === "failed") {
200
+ return data;
201
+ }
202
+ if (i % 5 === 0) {
203
+ process.stdout.write(".");
204
+ }
205
+ }
206
+ return { status: "timeout" };
207
+ }
208
+
209
+ // src/commands/clients.ts
210
+ async function createClient(name) {
211
+ const headers = getAuthHeaders();
212
+ if (!headers.authorization) {
213
+ console.error("Not authenticated. Run `chaveta login` first.");
214
+ process.exit(1);
215
+ }
216
+ const baseUrl = getBaseUrl();
217
+ const res = await fetch(`${baseUrl}/api/auth/oauth2/create-client`, {
218
+ method: "POST",
219
+ headers: { ...headers, "content-type": "application/json" },
220
+ body: JSON.stringify({
221
+ client_name: name,
222
+ grant_types: ["client_credentials"],
223
+ token_endpoint_auth_method: "client_secret_post"
224
+ })
225
+ });
226
+ if (!res.ok) {
227
+ console.error(`Failed to create client: ${res.statusText}`);
228
+ process.exit(1);
229
+ }
230
+ const data = await res.json();
231
+ console.log("\nClient created successfully.\n");
232
+ console.log(` CHAVETA_CLIENT_ID=${data.client_id}`);
233
+ console.log(` CHAVETA_CLIENT_SECRET=${data.client_secret}`);
234
+ console.log("\n Save the secret now \u2014 it will not be shown again.\n");
235
+ }
236
+ async function listClients() {
237
+ const headers = getAuthHeaders();
238
+ if (!headers.authorization) {
239
+ console.error("Not authenticated. Run `chaveta login` first.");
240
+ process.exit(1);
241
+ }
242
+ const baseUrl = getBaseUrl();
243
+ const res = await fetch(`${baseUrl}/api/auth/oauth2/get-clients`, { headers });
244
+ if (!res.ok) {
245
+ console.error(`Failed to list clients: ${res.statusText}`);
246
+ process.exit(1);
247
+ }
248
+ const data = await res.json();
249
+ if (data.length === 0) {
250
+ console.log("No clients found.");
251
+ return;
252
+ }
253
+ console.log("\nNAME CLIENT_ID CREATED");
254
+ console.log("\u2500".repeat(65));
255
+ for (const client of data) {
256
+ const name = (client.client_name ?? "").padEnd(20);
257
+ const id = client.client_id.padEnd(22);
258
+ const created = new Date(client.createdAt).toISOString().split("T")[0];
259
+ console.log(`${name} ${id} ${created}`);
260
+ }
261
+ console.log();
262
+ }
263
+ async function revokeClient(clientId) {
264
+ const headers = getAuthHeaders();
265
+ if (!headers.authorization) {
266
+ console.error("Not authenticated. Run `chaveta login` first.");
267
+ process.exit(1);
268
+ }
269
+ const baseUrl = getBaseUrl();
270
+ const res = await fetch(`${baseUrl}/api/auth/oauth2/delete-client`, {
271
+ method: "POST",
272
+ headers: { ...headers, "content-type": "application/json" },
273
+ body: JSON.stringify({ client_id: clientId })
274
+ });
275
+ if (!res.ok) {
276
+ console.error(`Failed to revoke client: ${res.statusText}`);
277
+ process.exit(1);
278
+ }
279
+ console.log(`Revoked client: ${clientId}`);
280
+ }
281
+
282
+ // src/index.ts
283
+ program.name("chaveta").description("Generative test fixtures for robotics CI").version("0.0.1");
284
+ program.command("login").description("Authenticate via GitHub OAuth").action(login);
285
+ program.command("logout").description("Clear stored credentials").action(() => {
286
+ clearCredentials();
287
+ console.log("Logged out.");
288
+ });
289
+ program.command("generate").description("Generate Foxglove-compatible test fixtures").requiredOption("--schema <schema>", "Foxglove schema (foxglove.PointCloud, foxglove.GeoJSON, foxglove.Grid, foxglove.LocationFix)").requiredOption("--scenario <scenario>", "Natural language scenario description").option("--count <count>", "Number of fixtures to generate", "1").option("--seed <seed>", "Deterministic seed for reproducibility").option("--output <dir>", "Output directory for MCAP files").option("--view", "Upload and return an ephemeral viewer link").action((opts) => {
290
+ generateCommand({
291
+ schema: opts.schema,
292
+ scenario: opts.scenario,
293
+ count: parseInt(opts.count, 10),
294
+ seed: opts.seed,
295
+ output: opts.output,
296
+ view: opts.view ?? !opts.output
297
+ });
298
+ });
299
+ var clients = program.command("clients").description("Manage M2M client credentials for CI");
300
+ clients.command("create").description("Create a new M2M client").requiredOption("--name <name>", "Client name").action((opts) => createClient(opts.name));
301
+ clients.command("list").description("List all clients").action(listClients);
302
+ clients.command("revoke").description("Revoke a client").argument("<client_id>", "Client ID to revoke").action(revokeClient);
303
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@beaglabs/chaveta",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "chaveta": "./dist/index.js"
7
+ },
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
11
+ "dependencies": {
12
+ "commander": "^13.1.0",
13
+ "open": "^10.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "tsup": "^8.5.0"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "keywords": [
22
+ "robotics",
23
+ "foxglove",
24
+ "mcap",
25
+ "pointcloud",
26
+ "synthetic-data",
27
+ "ci",
28
+ "test-fixtures",
29
+ "beaglabs"
30
+ ],
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/beaglabs/chaveta"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup src/index.ts --format esm",
38
+ "typecheck": "tsc --noEmit -p tsconfig.json"
39
+ }
40
+ }