@thingd/cli 0.76.0 → 0.77.2
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/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.d.ts.map +1 -0
- package/dist/commands/cloud.js +669 -0
- package/dist/commands/mcp-connect.d.ts +3 -0
- package/dist/commands/mcp-connect.d.ts.map +1 -0
- package/dist/commands/mcp-connect.js +154 -0
- package/dist/commands/sync.d.ts +3 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +197 -0
- package/dist/dashboard/public/assets/favicon-CgGFvG_0.svg +4 -0
- package/dist/dashboard/public/assets/index-C6PkDB7y.css +1 -0
- package/dist/dashboard/public/assets/index-DrpfyClj.js +4 -0
- package/dist/dashboard/public/index.html +19 -0
- package/dist/dashboard/server.d.ts +6 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +684 -0
- package/dist/data-movement.d.ts +6 -0
- package/dist/data-movement.d.ts.map +1 -0
- package/dist/data-movement.js +475 -0
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +108 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1446 -0
- package/dist/install.d.ts +3 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +229 -0
- package/dist/interactive.d.ts +2 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/interactive.js +3039 -0
- package/dist/lib/cloud-api.d.ts +143 -0
- package/dist/lib/cloud-api.d.ts.map +1 -0
- package/dist/lib/cloud-api.js +207 -0
- package/dist/lib/cloud-config.d.ts +33 -0
- package/dist/lib/cloud-config.d.ts.map +1 -0
- package/dist/lib/cloud-config.js +42 -0
- package/dist/lib/mcp-config-writer.d.ts +26 -0
- package/dist/lib/mcp-config-writer.d.ts.map +1 -0
- package/dist/lib/mcp-config-writer.js +86 -0
- package/dist/lib/sync-config.d.ts +22 -0
- package/dist/lib/sync-config.d.ts.map +1 -0
- package/dist/lib/sync-config.js +25 -0
- package/dist/logo.d.ts +3 -0
- package/dist/logo.d.ts.map +1 -0
- package/dist/logo.js +8 -0
- package/dist/mcp/cluster.d.ts +69 -0
- package/dist/mcp/cluster.d.ts.map +1 -0
- package/dist/mcp/cluster.js +304 -0
- package/dist/mcp/config.d.ts +14 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +67 -0
- package/dist/mcp/http.d.ts +35 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +957 -0
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp-http.d.ts +3 -0
- package/dist/mcp-http.d.ts.map +1 -0
- package/dist/mcp-http.js +42 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +28 -0
- package/dist/paths.d.ts +4 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +14 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/commands/cloud.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAA6B,MAAM,aAAa,CAAC;AA2IzE,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCjE"}
|
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
import { requiredToken, stringFlag } from "../index.js";
|
|
7
|
+
import { addOrganizationMember, CloudApiError, cleanupSessions, createApiKey, createInstance, createOrganization, createProject, createUserToken, getMe, getOrganization, listInstances, listOrganizationMembers, listOrganizations, listProjects, listUserTokens, parseUserTokenId, pollCliAuth, removeOrganizationMember, resolveAllInstances, revokeUserToken, startCliAuth, updateUserToken, } from "../lib/cloud-api.js";
|
|
8
|
+
import { readCloudConfig, removeCloudConfig, writeCloudConfig, } from "../lib/cloud-config.js";
|
|
9
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
10
|
+
const POLL_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
11
|
+
function cliApiUrl(context) {
|
|
12
|
+
return stringFlag(context.parsed, "url") ?? process.env.THINGD_URL ?? "https://api.thingd.cloud";
|
|
13
|
+
}
|
|
14
|
+
function buildWebUrl(apiUrl) {
|
|
15
|
+
if (apiUrl.includes("//api.") || apiUrl.startsWith("api.")) {
|
|
16
|
+
return apiUrl.replace("api.", "");
|
|
17
|
+
}
|
|
18
|
+
if (apiUrl.includes("localhost:8787") || apiUrl.includes("127.0.0.1:8787")) {
|
|
19
|
+
return "http://localhost:5173";
|
|
20
|
+
}
|
|
21
|
+
return apiUrl;
|
|
22
|
+
}
|
|
23
|
+
function sanitizeBrowserUrl(url) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = new URL(url);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
// Disallow characters that can be interpreted by cmd.exe
|
|
35
|
+
if (/[%"!\r\n]/.test(url)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return parsed.toString();
|
|
39
|
+
}
|
|
40
|
+
function openBrowser(url) {
|
|
41
|
+
const safeUrl = sanitizeBrowserUrl(url);
|
|
42
|
+
if (!safeUrl) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const platform = process.platform;
|
|
47
|
+
if (platform === "darwin") {
|
|
48
|
+
execFileSync("open", [safeUrl], { timeout: 5_000 });
|
|
49
|
+
}
|
|
50
|
+
else if (platform === "win32") {
|
|
51
|
+
execFileSync("cmd", ["/c", "start", "", safeUrl], { timeout: 5_000 });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
execFileSync("xdg-open", [safeUrl], { timeout: 5_000 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// browser open is best-effort
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function makeBaseConfig(context) {
|
|
62
|
+
return { url: cliApiUrl(context) };
|
|
63
|
+
}
|
|
64
|
+
async function askQuestion(rl, query) {
|
|
65
|
+
return rl.question(query);
|
|
66
|
+
}
|
|
67
|
+
async function pickAndSaveInstance(context, cloudConfig) {
|
|
68
|
+
const instances = await resolveAllInstances(cloudConfig);
|
|
69
|
+
if (instances.length === 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const rl = createInterface({
|
|
73
|
+
input: context.stdin,
|
|
74
|
+
output: context.stderr,
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
let selected;
|
|
78
|
+
if (instances.length === 1) {
|
|
79
|
+
selected = instances[0];
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
context.stderr.write(`${pc.bold("Select an instance")}\n`);
|
|
83
|
+
for (let i = 0; i < instances.length; i++) {
|
|
84
|
+
const inst = instances[i];
|
|
85
|
+
context.stderr.write(` [${i + 1}] ${pc.cyan(inst.projectSlug)}/${pc.cyan(inst.instanceSlug)}\n`);
|
|
86
|
+
}
|
|
87
|
+
const choice = await askQuestion(rl, `Select instance [1-${instances.length}] (default 1): `);
|
|
88
|
+
const index = Math.max(0, Math.min(instances.length - 1, (Number(choice.trim()) || 1) - 1));
|
|
89
|
+
selected = instances[index];
|
|
90
|
+
}
|
|
91
|
+
cloudConfig.instanceUrl = selected.mcpUrl;
|
|
92
|
+
cloudConfig.projectId = selected.projectId;
|
|
93
|
+
cloudConfig.projectSlug = selected.projectSlug;
|
|
94
|
+
cloudConfig.instanceSlug = selected.instanceSlug;
|
|
95
|
+
writeCloudConfig(cloudConfig);
|
|
96
|
+
context.stdout.write(` Instance: ${pc.cyan(selected.projectSlug)}/${pc.cyan(selected.instanceSlug)}\n`);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
rl.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function runCloud(context) {
|
|
103
|
+
const sub = requiredToken(context.parsed, 1, "subcommand");
|
|
104
|
+
switch (sub) {
|
|
105
|
+
case "login":
|
|
106
|
+
await runLogin(context);
|
|
107
|
+
return;
|
|
108
|
+
case "logout":
|
|
109
|
+
await runLogout(context);
|
|
110
|
+
return;
|
|
111
|
+
case "status":
|
|
112
|
+
await runCloudStatus(context);
|
|
113
|
+
return;
|
|
114
|
+
case "token":
|
|
115
|
+
await runToken(context);
|
|
116
|
+
return;
|
|
117
|
+
case "project":
|
|
118
|
+
await runProject(context);
|
|
119
|
+
return;
|
|
120
|
+
case "instance":
|
|
121
|
+
await runInstance(context);
|
|
122
|
+
return;
|
|
123
|
+
case "api-key":
|
|
124
|
+
context.stderr.write(pc.yellow("Deprecated. Use `thingd cloud token create` for CLI tokens. Project API keys are managed in the dashboard.\n"));
|
|
125
|
+
await runApiKey(context);
|
|
126
|
+
return;
|
|
127
|
+
case "org":
|
|
128
|
+
await runOrg(context);
|
|
129
|
+
return;
|
|
130
|
+
default:
|
|
131
|
+
context.stderr.write(`Unknown cloud subcommand: ${sub}\n` +
|
|
132
|
+
"Available: login, logout, status, token, org, project, instance, api-key\n");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function runLogin(context) {
|
|
136
|
+
const code = context.parsed.tokens[2] ?? stringFlag(context.parsed, "code");
|
|
137
|
+
const token = context.parsed.tokens[3] ?? stringFlag(context.parsed, "token");
|
|
138
|
+
// ── Manual --code --token flow (fallback) ─────────────────────────
|
|
139
|
+
if (code && token) {
|
|
140
|
+
const config = { url: cliApiUrl(context) };
|
|
141
|
+
try {
|
|
142
|
+
// Verify JWT and get user info
|
|
143
|
+
const { user } = await getMe({ ...config, token });
|
|
144
|
+
// Create a permanent user token
|
|
145
|
+
const hostname = os.hostname();
|
|
146
|
+
const { token: userToken } = await createUserToken({ ...config, token }, `cli-${hostname}`);
|
|
147
|
+
// Save config with user token (not JWT)
|
|
148
|
+
const cloudConfig = { userToken, email: user.email, ...config };
|
|
149
|
+
writeCloudConfig(cloudConfig);
|
|
150
|
+
context.stdout.write(pc.green(`✓ Logged in as ${user.email}\n`));
|
|
151
|
+
// Discover and select an instance (interactive if multiple)
|
|
152
|
+
await pickAndSaveInstance(context, cloudConfig);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
if (err instanceof CloudApiError && err.status === 401) {
|
|
156
|
+
context.stderr.write(pc.red("Invalid token. Please try again.\n"));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
context.stderr.write(pc.red(`Failed to verify token: ${err}\n`));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// ── Auto device-code flow ─────────────────────────────────────────
|
|
165
|
+
const baseConfig = makeBaseConfig(context);
|
|
166
|
+
let deviceCode;
|
|
167
|
+
try {
|
|
168
|
+
const result = await startCliAuth(baseConfig);
|
|
169
|
+
deviceCode = result.code;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
context.stderr.write(pc.red(`Failed to start CLI auth: ${err}\n`));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const webUrl = buildWebUrl(cliApiUrl(context));
|
|
176
|
+
const authUrl = `${webUrl}/cli/auth?code=${deviceCode}`;
|
|
177
|
+
context.stdout.write(`\n ${pc.cyan("Opening browser...")}\n` +
|
|
178
|
+
` ${pc.dim("If the browser doesn't open, visit:")}\n` +
|
|
179
|
+
` ${pc.dim(authUrl)}\n\n`);
|
|
180
|
+
openBrowser(authUrl);
|
|
181
|
+
// Poll for token
|
|
182
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
183
|
+
let dots = 0;
|
|
184
|
+
while (Date.now() < deadline) {
|
|
185
|
+
await sleep(POLL_INTERVAL_MS);
|
|
186
|
+
try {
|
|
187
|
+
const result = await pollCliAuth(baseConfig, deviceCode);
|
|
188
|
+
if ("token" in result) {
|
|
189
|
+
const jwt = result.token;
|
|
190
|
+
const tokenConfig = { url: cliApiUrl(context) };
|
|
191
|
+
try {
|
|
192
|
+
// Verify temporary JWT
|
|
193
|
+
const { user } = await getMe({ ...tokenConfig, token: jwt });
|
|
194
|
+
// Create permanent user token
|
|
195
|
+
const hostname = os.hostname();
|
|
196
|
+
const { token: userToken } = await createUserToken({ ...tokenConfig, token: jwt }, `cli-${hostname}`);
|
|
197
|
+
// Save config with user token (not JWT)
|
|
198
|
+
const cloudConfig = { userToken, email: user.email, ...tokenConfig };
|
|
199
|
+
writeCloudConfig(cloudConfig);
|
|
200
|
+
context.stdout.write(pc.green(`\r✓ Logged in as ${user.email}\n`));
|
|
201
|
+
// Discover and select an instance (interactive if multiple)
|
|
202
|
+
await pickAndSaveInstance(context, cloudConfig);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
context.stderr.write(pc.red("\rToken received but verification failed. Try again.\n"));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Show spinner
|
|
211
|
+
dots = (dots + 1) % 4;
|
|
212
|
+
context.stdout.write(`\r ${pc.dim(`Waiting for browser login${".".repeat(dots)}${" ".repeat(3 - dots)}`)}`);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
if (err instanceof CloudApiError && err.status === 410) {
|
|
216
|
+
context.stderr.write(pc.red("\nCode expired. Run `thingd cloud login` again.\n"));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
context.stderr.write(pc.red("\nTimed out. Run `thingd cloud login` again.\n"));
|
|
222
|
+
}
|
|
223
|
+
async function runLogout(context) {
|
|
224
|
+
const config = readCloudConfig();
|
|
225
|
+
if (config?.userToken) {
|
|
226
|
+
try {
|
|
227
|
+
const tokenId = parseUserTokenId(config.userToken);
|
|
228
|
+
if (tokenId) {
|
|
229
|
+
await revokeUserToken(config, tokenId);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Best-effort — token may already be revoked or API unreachable
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
removeCloudConfig();
|
|
237
|
+
context.stdout.write(pc.green("✓ Logged out\n"));
|
|
238
|
+
}
|
|
239
|
+
async function runCloudStatus(context) {
|
|
240
|
+
const config = readCloudConfig();
|
|
241
|
+
if (!config) {
|
|
242
|
+
context.stdout.write(`Not logged in. Run ${pc.cyan("thingd cloud login")}\n`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Warn about old config format
|
|
246
|
+
if (config.token && !config.userToken) {
|
|
247
|
+
context.stdout.write(`${pc.yellow("Your credentials use an older format.")} Run ${pc.cyan("thingd cloud login")} to upgrade to a persistent CLI token.\n\n`);
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const { user } = await getMe(config);
|
|
251
|
+
context.stdout.write(`Logged in as ${pc.green(user.email)} (${user.role})\n`);
|
|
252
|
+
// Show token info
|
|
253
|
+
if (config.userToken) {
|
|
254
|
+
try {
|
|
255
|
+
const { userTokens } = await listUserTokens(config);
|
|
256
|
+
const active = userTokens.find((t) => !t.revokedAt);
|
|
257
|
+
if (active) {
|
|
258
|
+
context.stdout.write(`CLI Token: ${pc.cyan(active.name)}\n` +
|
|
259
|
+
` Prefix: ${pc.dim(active.prefix)}\n` +
|
|
260
|
+
` Created: ${pc.dim(formatTimeAgo(active.createdAt))}\n` +
|
|
261
|
+
` Last used:${pc.dim(active.lastUsedAt ? formatTimeAgo(active.lastUsedAt) : "never")}\n` +
|
|
262
|
+
` Access: ${pc.dim(active.projectAccess === "all" ? "All projects" : active.projectAccess)}\n`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
context.stdout.write(` ${pc.dim("(token info unavailable)")}\n`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
context.stdout.write(`API: ${config.url ?? "https://api.thingd.cloud"}\n`);
|
|
270
|
+
if (config.projectSlug && config.instanceSlug && config.instanceUrl) {
|
|
271
|
+
context.stdout.write(`Instance: ${pc.cyan(config.projectSlug)}/${pc.cyan(config.instanceSlug)}\n` +
|
|
272
|
+
` ${pc.dim(config.instanceUrl)}\n`);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
context.stdout.write(`Instance: ${pc.dim("none — set with thingd cloud instance use <project> <instance>")}\n`);
|
|
276
|
+
}
|
|
277
|
+
if (config.organizationId) {
|
|
278
|
+
try {
|
|
279
|
+
const { organization, role } = await getOrganization(config, config.organizationId);
|
|
280
|
+
context.stdout.write(`Org: ${pc.cyan(organization.name)} (${pc.dim(organization.slug)}) — ${role}\n`);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
context.stdout.write(`Org: ${pc.dim("unreachable")}\n`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
context.stdout.write(`Org: ${pc.dim("none — set with thingd cloud org use <slug>")}\n`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
context.stdout.write(`Token expired. Run ${pc.cyan("thingd cloud login")}\n`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function formatTimeAgo(iso) {
|
|
295
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
296
|
+
const s = Math.floor(ms / 1000);
|
|
297
|
+
if (s < 5) {
|
|
298
|
+
return "just now";
|
|
299
|
+
}
|
|
300
|
+
if (s < 60) {
|
|
301
|
+
return `${s}s ago`;
|
|
302
|
+
}
|
|
303
|
+
const m = Math.floor(s / 60);
|
|
304
|
+
if (m < 60) {
|
|
305
|
+
return `${m}m ago`;
|
|
306
|
+
}
|
|
307
|
+
const h = Math.floor(m / 60);
|
|
308
|
+
if (h < 24) {
|
|
309
|
+
return `${h}h ago`;
|
|
310
|
+
}
|
|
311
|
+
const d = Math.floor(h / 24);
|
|
312
|
+
return `${d}d ago`;
|
|
313
|
+
}
|
|
314
|
+
// ── Token subcommands ────────────────────────────────────────────────
|
|
315
|
+
function requireLoggedInConfig(context) {
|
|
316
|
+
const config = readCloudConfig();
|
|
317
|
+
if (!config?.userToken && !config?.token) {
|
|
318
|
+
context.stderr.write(pc.yellow("Not logged in. Run thingd cloud login first.\n"));
|
|
319
|
+
throw new Error("not_logged_in");
|
|
320
|
+
}
|
|
321
|
+
return config;
|
|
322
|
+
}
|
|
323
|
+
async function runToken(context) {
|
|
324
|
+
const sub = context.parsed.tokens[2];
|
|
325
|
+
if (!sub) {
|
|
326
|
+
context.stderr.write("Usage: thingd cloud token <list|create|revoke|restrict|unrestricted>\n");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
switch (sub) {
|
|
330
|
+
case "list":
|
|
331
|
+
await runTokenList(context);
|
|
332
|
+
return;
|
|
333
|
+
case "create":
|
|
334
|
+
await runTokenCreate(context);
|
|
335
|
+
return;
|
|
336
|
+
case "revoke":
|
|
337
|
+
await runTokenRevoke(context);
|
|
338
|
+
return;
|
|
339
|
+
case "restrict":
|
|
340
|
+
await runTokenRestrict(context);
|
|
341
|
+
return;
|
|
342
|
+
case "unrestricted":
|
|
343
|
+
await runTokenUnrestricted(context);
|
|
344
|
+
return;
|
|
345
|
+
case "cleanup":
|
|
346
|
+
await runTokenCleanup(context);
|
|
347
|
+
return;
|
|
348
|
+
default:
|
|
349
|
+
context.stderr.write(`Unknown token action: ${sub}. Available: list, create, revoke, restrict, unrestricted, cleanup\n`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function runTokenCleanup(context) {
|
|
353
|
+
const config = requireLoggedInConfig(context);
|
|
354
|
+
try {
|
|
355
|
+
// Clean up expired sessions on the user's account
|
|
356
|
+
const { removed } = await cleanupSessions(config);
|
|
357
|
+
context.stdout.write(pc.green(`✓ Cleaned up ${removed} expired sessions\n`));
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
context.stderr.write(pc.red(`Failed to clean up sessions: ${err}\n`));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function runTokenList(context) {
|
|
364
|
+
const config = requireLoggedInConfig(context);
|
|
365
|
+
try {
|
|
366
|
+
const { userTokens } = await listUserTokens(config);
|
|
367
|
+
if (userTokens.length === 0) {
|
|
368
|
+
context.stdout.write("No CLI tokens found. Create one with `thingd cloud token create <name>`\n");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
// Header
|
|
372
|
+
const header = `${pc.bold("Name".padEnd(18))} ${pc.bold("Prefix".padEnd(22))} ${pc.bold("Created".padEnd(14))} ${pc.bold("Last Used".padEnd(14))} ${pc.bold("Access")}`;
|
|
373
|
+
context.stdout.write(`${header}\n`);
|
|
374
|
+
context.stdout.write(`${pc.dim("─".repeat(header.length))}\n`);
|
|
375
|
+
for (const t of userTokens) {
|
|
376
|
+
if (t.revokedAt) {
|
|
377
|
+
continue; // Skip revoked tokens
|
|
378
|
+
}
|
|
379
|
+
const access = t.projectAccess === "all" ? "All" : t.projectAccess;
|
|
380
|
+
context.stdout.write(`${t.name.padEnd(18)} ${pc.dim(t.prefix.padEnd(22))} ${formatTimeAgo(t.createdAt).padEnd(14)} ${(t.lastUsedAt ? formatTimeAgo(t.lastUsedAt) : "never").padEnd(14)} ${pc.cyan(access)}\n`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
context.stderr.write(pc.red(`Failed to list tokens: ${err}\n`));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function runTokenCreate(context) {
|
|
388
|
+
const config = requireLoggedInConfig(context);
|
|
389
|
+
const name = context.parsed.tokens[3] ?? "cli-token";
|
|
390
|
+
try {
|
|
391
|
+
const { token: userToken } = await createUserToken(config, name);
|
|
392
|
+
context.stdout.write(`\n${pc.green("✓ Token created")}\n\n` +
|
|
393
|
+
`${pc.bold(userToken)}\n\n` +
|
|
394
|
+
`${pc.yellow("⚠ This token will only be shown once. Copy it now.\n")}` +
|
|
395
|
+
`${pc.dim("Press Enter to continue...")}\n`);
|
|
396
|
+
// Wait for user to acknowledge
|
|
397
|
+
const rl = createInterface({
|
|
398
|
+
input: context.stdin,
|
|
399
|
+
output: context.stderr,
|
|
400
|
+
});
|
|
401
|
+
await rl.question("");
|
|
402
|
+
rl.close();
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
context.stderr.write(pc.red(`Failed to create token: ${err}\n`));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async function runTokenRevoke(context) {
|
|
409
|
+
const config = requireLoggedInConfig(context);
|
|
410
|
+
const tokenId = requiredToken(context.parsed, 3, "token-id");
|
|
411
|
+
try {
|
|
412
|
+
await revokeUserToken(config, tokenId);
|
|
413
|
+
context.stdout.write(pc.green(`✓ Token ${tokenId} revoked\n`));
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
if (err instanceof CloudApiError && err.status === 404) {
|
|
417
|
+
context.stderr.write(pc.red("Token not found.\n"));
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
context.stderr.write(pc.red(`Failed to revoke token: ${err}\n`));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function runTokenRestrict(context) {
|
|
425
|
+
const config = requireLoggedInConfig(context);
|
|
426
|
+
const tokenId = requiredToken(context.parsed, 3, "token-id");
|
|
427
|
+
const projectSlug = requiredToken(context.parsed, 4, "project");
|
|
428
|
+
try {
|
|
429
|
+
const { userToken } = await updateUserToken(config, tokenId, {
|
|
430
|
+
projectAccess: projectSlug,
|
|
431
|
+
});
|
|
432
|
+
context.stdout.write(pc.green(`✓ Token "${userToken.name}" restricted to: ${projectSlug}\n`));
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
if (err instanceof CloudApiError && err.status === 404) {
|
|
436
|
+
context.stderr.write(pc.red("Token not found.\n"));
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
context.stderr.write(pc.red(`Failed to update token: ${err}\n`));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function runTokenUnrestricted(context) {
|
|
444
|
+
const config = requireLoggedInConfig(context);
|
|
445
|
+
const tokenId = requiredToken(context.parsed, 3, "token-id");
|
|
446
|
+
try {
|
|
447
|
+
const { userToken } = await updateUserToken(config, tokenId, {
|
|
448
|
+
projectAccess: "all",
|
|
449
|
+
});
|
|
450
|
+
context.stdout.write(pc.green(`✓ Token "${userToken.name}" now has access to all projects\n`));
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
if (err instanceof CloudApiError && err.status === 404) {
|
|
454
|
+
context.stderr.write(pc.red("Token not found.\n"));
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
context.stderr.write(pc.red(`Failed to update token: ${err}\n`));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// ── Org helpers ──────────────────────────────────────────────────────
|
|
462
|
+
async function resolveOrg(config, slugOrId) {
|
|
463
|
+
// Try direct ID lookup first, then assume it's a slug and list
|
|
464
|
+
try {
|
|
465
|
+
const { organization } = await getOrganization(config, slugOrId);
|
|
466
|
+
return organization;
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Fall through to slug resolution
|
|
470
|
+
}
|
|
471
|
+
const { organizations } = await listOrganizations(config);
|
|
472
|
+
const found = organizations.find((o) => o.slug === slugOrId);
|
|
473
|
+
if (!found) {
|
|
474
|
+
throw new Error(`Organization not found: ${slugOrId}`);
|
|
475
|
+
}
|
|
476
|
+
return found;
|
|
477
|
+
}
|
|
478
|
+
async function runOrg(context) {
|
|
479
|
+
const config = await requireConfig(context);
|
|
480
|
+
const action = context.parsed.tokens[2];
|
|
481
|
+
if (!action || action === "list") {
|
|
482
|
+
const { organizations } = await listOrganizations(config);
|
|
483
|
+
if (organizations.length === 0) {
|
|
484
|
+
context.stdout.write("No organizations found.\n");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
for (const org of organizations) {
|
|
488
|
+
const active = org.id === config.organizationId ? pc.green(" ●") : "";
|
|
489
|
+
context.stdout.write(`${pc.cyan(org.slug)} ${org.name}${active}\n`);
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (action === "create") {
|
|
494
|
+
const name = requiredToken(context.parsed, 3, "name");
|
|
495
|
+
const { organization } = await createOrganization(config, name);
|
|
496
|
+
context.stdout.write(pc.green(`✓ Created organization: ${organization.slug}\n`));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (action === "use") {
|
|
500
|
+
const slugOrId = requiredToken(context.parsed, 3, "organization");
|
|
501
|
+
const org = await resolveOrg(config, slugOrId);
|
|
502
|
+
config.organizationId = org.id;
|
|
503
|
+
writeCloudConfig(config);
|
|
504
|
+
context.stdout.write(pc.green(`✓ Active organization: ${org.slug}\n`));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (action === "members") {
|
|
508
|
+
await runOrgMembers(context, config);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
context.stderr.write(`Unknown org action: ${action}. Available: list, create, use, members\n`);
|
|
512
|
+
}
|
|
513
|
+
async function runOrgMembers(context, config) {
|
|
514
|
+
const sub = context.parsed.tokens[3];
|
|
515
|
+
if (!sub || sub === "list") {
|
|
516
|
+
const orgSlug = requiredToken(context.parsed, 4, "organization");
|
|
517
|
+
const org = await resolveOrg(config, orgSlug);
|
|
518
|
+
const { members } = await listOrganizationMembers(config, org.id);
|
|
519
|
+
if (members.length === 0) {
|
|
520
|
+
context.stdout.write("No members found.\n");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
for (const m of members) {
|
|
524
|
+
context.stdout.write(`${pc.cyan(m.userId)} ${m.role}\n`);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (sub === "add") {
|
|
529
|
+
const orgSlug = requiredToken(context.parsed, 4, "organization");
|
|
530
|
+
const userId = requiredToken(context.parsed, 5, "user-id");
|
|
531
|
+
const role = stringFlag(context.parsed, "role") ?? "member";
|
|
532
|
+
const org = await resolveOrg(config, orgSlug);
|
|
533
|
+
const { member } = await addOrganizationMember(config, org.id, userId, role);
|
|
534
|
+
context.stdout.write(pc.green(`✓ Added ${member.userId} as ${member.role}\n`));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (sub === "remove") {
|
|
538
|
+
const orgSlug = requiredToken(context.parsed, 4, "organization");
|
|
539
|
+
const userId = requiredToken(context.parsed, 5, "user-id");
|
|
540
|
+
const org = await resolveOrg(config, orgSlug);
|
|
541
|
+
await removeOrganizationMember(config, org.id, userId);
|
|
542
|
+
context.stdout.write(pc.green(`✓ Removed ${userId}\n`));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
context.stderr.write(`Unknown members action: ${sub}. Available: list, add, remove\n`);
|
|
546
|
+
}
|
|
547
|
+
async function requireConfig(context) {
|
|
548
|
+
const config = readCloudConfig();
|
|
549
|
+
if (!config) {
|
|
550
|
+
context.stderr.write(`Not logged in. Run ${pc.cyan("thingd cloud login")} first.\n`);
|
|
551
|
+
throw new Error("not_logged_in");
|
|
552
|
+
}
|
|
553
|
+
return config;
|
|
554
|
+
}
|
|
555
|
+
async function runProject(context) {
|
|
556
|
+
const config = await requireConfig(context);
|
|
557
|
+
const action = requiredToken(context.parsed, 2, "action");
|
|
558
|
+
if (action === "list") {
|
|
559
|
+
const { projects } = await listProjects(config);
|
|
560
|
+
if (projects.length === 0) {
|
|
561
|
+
context.stdout.write("No projects found.\n");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
for (const p of projects) {
|
|
565
|
+
context.stdout.write(`${pc.cyan(p.slug)} ${p.name} ${pc.dim(p.createdAt.slice(0, 10))}\n`);
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (action === "create") {
|
|
570
|
+
const name = requiredToken(context.parsed, 3, "name");
|
|
571
|
+
// Optional --org flag for team projects
|
|
572
|
+
let organizationId;
|
|
573
|
+
const orgSlug = stringFlag(context.parsed, "org");
|
|
574
|
+
if (orgSlug) {
|
|
575
|
+
const org = await resolveOrg(config, orgSlug);
|
|
576
|
+
organizationId = org.id;
|
|
577
|
+
}
|
|
578
|
+
const { project } = await createProject(config, name, organizationId);
|
|
579
|
+
const ctxNote = organizationId ? ` (under org ${orgSlug})` : "";
|
|
580
|
+
context.stdout.write(pc.green(`✓ Created project: ${project.slug}${ctxNote}\n`));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
context.stderr.write(`Unknown project action: ${action}. Available: list, create\n`);
|
|
584
|
+
}
|
|
585
|
+
async function runInstance(context) {
|
|
586
|
+
const config = await requireConfig(context);
|
|
587
|
+
const action = requiredToken(context.parsed, 2, "action");
|
|
588
|
+
if (action === "list") {
|
|
589
|
+
const projectSlug = requiredToken(context.parsed, 3, "project");
|
|
590
|
+
const { projects } = await listProjects(config);
|
|
591
|
+
const project = projects.find((p) => p.slug === projectSlug || p.id === projectSlug);
|
|
592
|
+
if (!project) {
|
|
593
|
+
context.stderr.write(pc.red(`Project not found: ${projectSlug}\n`));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const { instances } = await listInstances(config, project.id);
|
|
597
|
+
if (instances.length === 0) {
|
|
598
|
+
context.stdout.write("No instances found.\n");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
for (const inst of instances) {
|
|
602
|
+
const active = inst.mcpUrl && inst.mcpUrl === config.instanceUrl ? pc.green(" ●") : "";
|
|
603
|
+
context.stdout.write(`${pc.cyan(inst.slug)} ${inst.name} ${pc.dim(inst.mcpUrl || "no URL")}${active}\n`);
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (action === "use") {
|
|
608
|
+
const projectSlug = requiredToken(context.parsed, 3, "project");
|
|
609
|
+
const instanceSlug = requiredToken(context.parsed, 4, "instance");
|
|
610
|
+
const { projects } = await listProjects(config);
|
|
611
|
+
const project = projects.find((p) => p.slug === projectSlug || p.id === projectSlug);
|
|
612
|
+
if (!project) {
|
|
613
|
+
context.stderr.write(pc.red(`Project not found: ${projectSlug}\n`));
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const { instances } = await listInstances(config, project.id);
|
|
617
|
+
const instance = instances.find((i) => i.slug === instanceSlug || i.id === instanceSlug);
|
|
618
|
+
if (!instance) {
|
|
619
|
+
context.stderr.write(pc.red(`Instance not found: ${instanceSlug}\n`));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (!instance.mcpUrl) {
|
|
623
|
+
context.stderr.write(pc.red("Instance has no MCP URL. Is it running?\n"));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
config.instanceUrl = instance.mcpUrl;
|
|
627
|
+
config.projectId = project.id;
|
|
628
|
+
config.projectSlug = project.slug;
|
|
629
|
+
config.instanceSlug = instance.slug;
|
|
630
|
+
writeCloudConfig(config);
|
|
631
|
+
context.stdout.write(pc.green(`✓ Active instance: ${pc.cyan(project.slug)}/${pc.cyan(instance.slug)}\n`) +
|
|
632
|
+
` ${pc.dim(instance.mcpUrl)}\n`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (action === "create") {
|
|
636
|
+
const projectSlug = requiredToken(context.parsed, 3, "project");
|
|
637
|
+
const name = requiredToken(context.parsed, 4, "name");
|
|
638
|
+
const { projects } = await listProjects(config);
|
|
639
|
+
const project = projects.find((p) => p.slug === projectSlug || p.id === projectSlug);
|
|
640
|
+
if (!project) {
|
|
641
|
+
context.stderr.write(pc.red(`Project not found: ${projectSlug}\n`));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const { instance } = await createInstance(config, project.id, name);
|
|
645
|
+
context.stdout.write(pc.green(`✓ Created instance: ${instance.slug}\n`));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
context.stderr.write(`Unknown instance action: ${action}. Available: list, use, create\n`);
|
|
649
|
+
}
|
|
650
|
+
async function runApiKey(context) {
|
|
651
|
+
const config = await requireConfig(context);
|
|
652
|
+
const action = requiredToken(context.parsed, 2, "action");
|
|
653
|
+
if (action === "create") {
|
|
654
|
+
const projectSlug = requiredToken(context.parsed, 3, "project");
|
|
655
|
+
const name = context.parsed.tokens[4];
|
|
656
|
+
const { projects } = await listProjects(config);
|
|
657
|
+
const project = projects.find((p) => p.slug === projectSlug || p.id === projectSlug);
|
|
658
|
+
if (!project) {
|
|
659
|
+
context.stderr.write(pc.red(`Project not found: ${projectSlug}\n`));
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const { key } = await createApiKey(config, project.id, name);
|
|
663
|
+
context.stdout.write(pc.green(`✓ Created API key: ${key.prefix}\n`) +
|
|
664
|
+
`${pc.yellow("Save this token — it won't be shown again:")}\n` +
|
|
665
|
+
`${pc.bold(key.token ?? "(token not returned)")}\n`);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
context.stderr.write(`Unknown api-key action: ${action}. Available: create\n`);
|
|
669
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-connect.d.ts","sourceRoot":"","sources":["../../src/commands/mcp-connect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAsB9C,wBAAsB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAwKtE"}
|