@tinycloud/cli 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.
- package/bin/tc +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1350 -0
- package/dist/index.js.map +1 -0
- package/package.json +37 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1350 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
// src/config/constants.ts
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
var CONFIG_DIR = join(homedir(), ".tinycloud");
|
|
8
|
+
var PROFILES_DIR = join(CONFIG_DIR, "profiles");
|
|
9
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
10
|
+
var DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
11
|
+
var DEFAULT_PROFILE = "default";
|
|
12
|
+
var DEFAULT_CHAIN_ID = 1;
|
|
13
|
+
var ExitCode = {
|
|
14
|
+
SUCCESS: 0,
|
|
15
|
+
ERROR: 1,
|
|
16
|
+
USAGE_ERROR: 2,
|
|
17
|
+
AUTH_REQUIRED: 3,
|
|
18
|
+
NOT_FOUND: 4,
|
|
19
|
+
PERMISSION_DENIED: 5,
|
|
20
|
+
NETWORK_ERROR: 6,
|
|
21
|
+
NODE_ERROR: 7
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/output/formatter.ts
|
|
25
|
+
import ora from "ora";
|
|
26
|
+
function outputJson(data) {
|
|
27
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
28
|
+
}
|
|
29
|
+
function outputError(code, message) {
|
|
30
|
+
process.stderr.write(
|
|
31
|
+
JSON.stringify({ error: { code, message } }, null, 2) + "\n"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function isInteractive() {
|
|
35
|
+
return Boolean(process.stdout.isTTY);
|
|
36
|
+
}
|
|
37
|
+
async function withSpinner(label, fn) {
|
|
38
|
+
if (!isInteractive()) {
|
|
39
|
+
return fn();
|
|
40
|
+
}
|
|
41
|
+
const spinner = ora(label).start();
|
|
42
|
+
try {
|
|
43
|
+
const result = await fn();
|
|
44
|
+
spinner.succeed();
|
|
45
|
+
return result;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
spinner.fail();
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/output/errors.ts
|
|
53
|
+
var CLIError = class extends Error {
|
|
54
|
+
constructor(code, message, exitCode = ExitCode.ERROR) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.code = code;
|
|
57
|
+
this.exitCode = exitCode;
|
|
58
|
+
this.name = "CLIError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function wrapError(error) {
|
|
62
|
+
if (error instanceof CLIError) return error;
|
|
63
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
64
|
+
if (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired")) {
|
|
65
|
+
return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
|
|
66
|
+
}
|
|
67
|
+
if (message.includes("NOT_FOUND") || message.includes("KV_NOT_FOUND")) {
|
|
68
|
+
return new CLIError("NOT_FOUND", message, ExitCode.NOT_FOUND);
|
|
69
|
+
}
|
|
70
|
+
if (message.includes("PERMISSION_DENIED")) {
|
|
71
|
+
return new CLIError("PERMISSION_DENIED", message, ExitCode.PERMISSION_DENIED);
|
|
72
|
+
}
|
|
73
|
+
if (message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("fetch failed")) {
|
|
74
|
+
return new CLIError("NETWORK_ERROR", message, ExitCode.NETWORK_ERROR);
|
|
75
|
+
}
|
|
76
|
+
return new CLIError("ERROR", message, ExitCode.ERROR);
|
|
77
|
+
}
|
|
78
|
+
function handleError(error) {
|
|
79
|
+
const cliError = wrapError(error);
|
|
80
|
+
outputError(cliError.code, cliError.message);
|
|
81
|
+
process.exit(cliError.exitCode);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/config/profiles.ts
|
|
85
|
+
import { join as join2 } from "path";
|
|
86
|
+
import { rm as rm2 } from "fs/promises";
|
|
87
|
+
|
|
88
|
+
// src/config/storage.ts
|
|
89
|
+
import { readFile, writeFile, stat, mkdir, rm, readdir } from "fs/promises";
|
|
90
|
+
import { dirname } from "path";
|
|
91
|
+
async function readJson(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
const data = await readFile(filePath, "utf-8");
|
|
94
|
+
return JSON.parse(data);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err.code === "ENOENT") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function writeJson(filePath, data) {
|
|
103
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
104
|
+
await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
105
|
+
}
|
|
106
|
+
async function fileExists(filePath) {
|
|
107
|
+
try {
|
|
108
|
+
await stat(filePath);
|
|
109
|
+
return true;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err.code === "ENOENT") {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function ensureDir(dirPath) {
|
|
118
|
+
await mkdir(dirPath, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
async function removeDir(dirPath) {
|
|
121
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
async function listDirs(dirPath) {
|
|
124
|
+
try {
|
|
125
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
126
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (err.code === "ENOENT") {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/config/profiles.ts
|
|
136
|
+
var ProfileManager = class _ProfileManager {
|
|
137
|
+
// ── Initialization ──────────────────────────────────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* Creates ~/.tinycloud/ and ~/.tinycloud/profiles/ if they don't exist.
|
|
140
|
+
*/
|
|
141
|
+
static async ensureConfigDir() {
|
|
142
|
+
await ensureDir(CONFIG_DIR);
|
|
143
|
+
await ensureDir(PROFILES_DIR);
|
|
144
|
+
}
|
|
145
|
+
// ── Global config ───────────────────────────────────────────────────
|
|
146
|
+
/**
|
|
147
|
+
* Reads config.json. Returns a default config if the file is missing.
|
|
148
|
+
*/
|
|
149
|
+
static async getConfig() {
|
|
150
|
+
const config = await readJson(CONFIG_FILE);
|
|
151
|
+
if (!config) {
|
|
152
|
+
return { defaultProfile: DEFAULT_PROFILE, version: 1 };
|
|
153
|
+
}
|
|
154
|
+
return config;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Writes the global config to config.json.
|
|
158
|
+
*/
|
|
159
|
+
static async setConfig(config) {
|
|
160
|
+
await _ProfileManager.ensureConfigDir();
|
|
161
|
+
await writeJson(CONFIG_FILE, config);
|
|
162
|
+
}
|
|
163
|
+
// ── Profile CRUD ────────────────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Returns the profile config for the given name.
|
|
166
|
+
* Throws CLIError if the profile doesn't exist.
|
|
167
|
+
*/
|
|
168
|
+
static async getProfile(name) {
|
|
169
|
+
const profilePath = join2(PROFILES_DIR, name, "profile.json");
|
|
170
|
+
const profile = await readJson(profilePath);
|
|
171
|
+
if (!profile) {
|
|
172
|
+
throw new CLIError(
|
|
173
|
+
"PROFILE_NOT_FOUND",
|
|
174
|
+
`Profile "${name}" does not exist. Run \`tc init\` or \`tc profile create ${name}\` first.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return profile;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Saves a profile config, creating the profile directory if needed.
|
|
181
|
+
*/
|
|
182
|
+
static async setProfile(name, data) {
|
|
183
|
+
const profileDir = join2(PROFILES_DIR, name);
|
|
184
|
+
await ensureDir(profileDir);
|
|
185
|
+
await writeJson(join2(profileDir, "profile.json"), data);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Returns true if a profile directory exists.
|
|
189
|
+
*/
|
|
190
|
+
static async profileExists(name) {
|
|
191
|
+
return fileExists(join2(PROFILES_DIR, name, "profile.json"));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Returns an array of profile directory names.
|
|
195
|
+
*/
|
|
196
|
+
static async listProfiles() {
|
|
197
|
+
return listDirs(PROFILES_DIR);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Deletes a profile directory.
|
|
201
|
+
* Throws if trying to delete the current default profile.
|
|
202
|
+
*/
|
|
203
|
+
static async deleteProfile(name) {
|
|
204
|
+
const config = await _ProfileManager.getConfig();
|
|
205
|
+
if (config.defaultProfile === name) {
|
|
206
|
+
throw new CLIError(
|
|
207
|
+
"PROFILE_DELETE_DEFAULT",
|
|
208
|
+
`Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const profileDir = join2(PROFILES_DIR, name);
|
|
212
|
+
await removeDir(profileDir);
|
|
213
|
+
}
|
|
214
|
+
// ── Key management ──────────────────────────────────────────────────
|
|
215
|
+
/**
|
|
216
|
+
* Returns the parsed JWK for a profile, or null if no key exists.
|
|
217
|
+
*/
|
|
218
|
+
static async getKey(name) {
|
|
219
|
+
return readJson(join2(PROFILES_DIR, name, "key.json"));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Saves a JWK key for a profile.
|
|
223
|
+
*/
|
|
224
|
+
static async setKey(name, jwk) {
|
|
225
|
+
const profileDir = join2(PROFILES_DIR, name);
|
|
226
|
+
await ensureDir(profileDir);
|
|
227
|
+
await writeJson(join2(profileDir, "key.json"), jwk);
|
|
228
|
+
}
|
|
229
|
+
// ── Session management ──────────────────────────────────────────────
|
|
230
|
+
/**
|
|
231
|
+
* Returns the parsed session for a profile, or null if none exists.
|
|
232
|
+
*/
|
|
233
|
+
static async getSession(name) {
|
|
234
|
+
return readJson(join2(PROFILES_DIR, name, "session.json"));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Saves session data for a profile.
|
|
238
|
+
*/
|
|
239
|
+
static async setSession(name, session) {
|
|
240
|
+
const profileDir = join2(PROFILES_DIR, name);
|
|
241
|
+
await ensureDir(profileDir);
|
|
242
|
+
await writeJson(join2(profileDir, "session.json"), session);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Removes the session file for a profile.
|
|
246
|
+
*/
|
|
247
|
+
static async clearSession(name) {
|
|
248
|
+
const sessionPath = join2(PROFILES_DIR, name, "session.json");
|
|
249
|
+
try {
|
|
250
|
+
await rm2(sessionPath);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (err.code !== "ENOENT") {
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// ── Cache management ────────────────────────────────────────────────
|
|
258
|
+
/**
|
|
259
|
+
* Returns the path to the profile's cache directory, creating it if needed.
|
|
260
|
+
*/
|
|
261
|
+
static async getCacheDir(name) {
|
|
262
|
+
const cacheDir = join2(PROFILES_DIR, name, "cache");
|
|
263
|
+
await ensureDir(cacheDir);
|
|
264
|
+
return cacheDir;
|
|
265
|
+
}
|
|
266
|
+
// ── Resolution helpers ──────────────────────────────────────────────
|
|
267
|
+
/**
|
|
268
|
+
* Resolves the full CLI context from flags, env vars, and config.
|
|
269
|
+
*
|
|
270
|
+
* Profile resolution: options.profile > TC_PROFILE env > config.defaultProfile > "default"
|
|
271
|
+
* Host resolution: options.host > TC_HOST env > profile.host > DEFAULT_HOST
|
|
272
|
+
*/
|
|
273
|
+
static async resolveContext(options) {
|
|
274
|
+
const config = await _ProfileManager.getConfig();
|
|
275
|
+
const profile = options.profile ?? process.env.TC_PROFILE ?? config.defaultProfile ?? DEFAULT_PROFILE;
|
|
276
|
+
let profileHost;
|
|
277
|
+
try {
|
|
278
|
+
const profileConfig = await _ProfileManager.getProfile(profile);
|
|
279
|
+
profileHost = profileConfig.host;
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
const host = options.host ?? process.env.TC_HOST ?? profileHost ?? DEFAULT_HOST;
|
|
283
|
+
return {
|
|
284
|
+
profile,
|
|
285
|
+
host,
|
|
286
|
+
verbose: options.verbose ?? false,
|
|
287
|
+
noCache: options.noCache ?? false,
|
|
288
|
+
quiet: options.quiet ?? false
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// src/auth/local-key.ts
|
|
294
|
+
import { TCWSessionManager, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
295
|
+
var wasmInitialized = false;
|
|
296
|
+
function ensureWasm() {
|
|
297
|
+
if (!wasmInitialized) {
|
|
298
|
+
initPanicHook();
|
|
299
|
+
wasmInitialized = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function generateKey() {
|
|
303
|
+
ensureWasm();
|
|
304
|
+
const mgr = new TCWSessionManager();
|
|
305
|
+
const keyId = mgr.createSessionKey("cli");
|
|
306
|
+
const jwkStr = mgr.jwk(keyId);
|
|
307
|
+
if (!jwkStr) throw new Error("Failed to generate key");
|
|
308
|
+
const jwk = JSON.parse(jwkStr);
|
|
309
|
+
const did = mgr.getDID(keyId);
|
|
310
|
+
return { jwk, did };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/auth/browser-auth.ts
|
|
314
|
+
import { createServer } from "http";
|
|
315
|
+
import { createInterface } from "readline";
|
|
316
|
+
var OPENKEY_BASE = "https://openkey.cloud";
|
|
317
|
+
async function startAuthFlow(did, options = {}) {
|
|
318
|
+
if (options.paste) {
|
|
319
|
+
return pasteFlow(did);
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
return await callbackFlow(did);
|
|
323
|
+
} catch {
|
|
324
|
+
if (isInteractive()) {
|
|
325
|
+
console.error("Could not open browser. Falling back to manual paste mode.");
|
|
326
|
+
return pasteFlow(did);
|
|
327
|
+
}
|
|
328
|
+
throw new Error("Cannot open browser in non-interactive mode. Use --paste flag.");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function callbackFlow(did) {
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
const server = createServer((req, res) => {
|
|
334
|
+
if (req.method === "POST" && req.url === "/callback") {
|
|
335
|
+
let body = "";
|
|
336
|
+
req.on("data", (chunk) => {
|
|
337
|
+
body += chunk.toString();
|
|
338
|
+
});
|
|
339
|
+
req.on("end", () => {
|
|
340
|
+
try {
|
|
341
|
+
const data = JSON.parse(body);
|
|
342
|
+
res.writeHead(200, {
|
|
343
|
+
"Content-Type": "application/json",
|
|
344
|
+
"Access-Control-Allow-Origin": "*"
|
|
345
|
+
});
|
|
346
|
+
res.end(JSON.stringify({ success: true }));
|
|
347
|
+
server.close();
|
|
348
|
+
resolve(data);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
351
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
352
|
+
reject(new Error("Invalid delegation data received"));
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
} else if (req.method === "OPTIONS") {
|
|
356
|
+
res.writeHead(204, {
|
|
357
|
+
"Access-Control-Allow-Origin": "*",
|
|
358
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
359
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
360
|
+
});
|
|
361
|
+
res.end();
|
|
362
|
+
} else {
|
|
363
|
+
res.writeHead(404);
|
|
364
|
+
res.end();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
server.listen(0, "127.0.0.1", async () => {
|
|
368
|
+
const addr = server.address();
|
|
369
|
+
if (!addr || typeof addr === "string") {
|
|
370
|
+
reject(new Error("Failed to start callback server"));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const port = addr.port;
|
|
374
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
375
|
+
const authUrl = `${OPENKEY_BASE}/delegate?did=${encodeURIComponent(did)}&callback=${encodeURIComponent(callbackUrl)}`;
|
|
376
|
+
if (isInteractive()) {
|
|
377
|
+
console.error(`Opening browser for authentication...`);
|
|
378
|
+
console.error(`If the browser doesn't open, visit: ${authUrl}`);
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
const open = (await import("open")).default;
|
|
382
|
+
await open(authUrl);
|
|
383
|
+
} catch {
|
|
384
|
+
server.close();
|
|
385
|
+
throw new Error("Failed to open browser");
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
setTimeout(() => {
|
|
389
|
+
server.close();
|
|
390
|
+
reject(new Error("Authentication timed out after 5 minutes"));
|
|
391
|
+
}, 5 * 60 * 1e3);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
async function pasteFlow(did) {
|
|
395
|
+
const authUrl = `${OPENKEY_BASE}/delegate?did=${encodeURIComponent(did)}`;
|
|
396
|
+
console.error(`
|
|
397
|
+
Open this URL in a browser to authenticate:
|
|
398
|
+
`);
|
|
399
|
+
console.error(` ${authUrl}
|
|
400
|
+
`);
|
|
401
|
+
const rl = createInterface({
|
|
402
|
+
input: process.stdin,
|
|
403
|
+
output: process.stderr
|
|
404
|
+
});
|
|
405
|
+
return new Promise((resolve, reject) => {
|
|
406
|
+
rl.question("Paste delegation code: ", (input) => {
|
|
407
|
+
rl.close();
|
|
408
|
+
try {
|
|
409
|
+
const data = JSON.parse(input.trim());
|
|
410
|
+
resolve(data);
|
|
411
|
+
} catch {
|
|
412
|
+
try {
|
|
413
|
+
const decoded = Buffer.from(input.trim(), "base64").toString("utf-8");
|
|
414
|
+
const data = JSON.parse(decoded);
|
|
415
|
+
resolve(data);
|
|
416
|
+
} catch {
|
|
417
|
+
reject(new Error("Invalid delegation code. Expected JSON or base64-encoded JSON."));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/commands/init.ts
|
|
425
|
+
function registerInitCommand(program2) {
|
|
426
|
+
program2.command("init").description("Initialize a new TinyCloud profile").option("--name <profile>", "Profile name", "default").option("--key-only", "Only generate key, skip authentication").option("--host <url>", "TinyCloud node URL").option("--paste", "Use manual paste mode for authentication").action(async (options, cmd) => {
|
|
427
|
+
try {
|
|
428
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
429
|
+
const profileName = options.name;
|
|
430
|
+
const host = options.host ?? globalOpts.host ?? DEFAULT_HOST;
|
|
431
|
+
if (await ProfileManager.profileExists(profileName)) {
|
|
432
|
+
throw new CLIError(
|
|
433
|
+
"PROFILE_EXISTS",
|
|
434
|
+
`Profile "${profileName}" already exists. Use \`tc profile delete ${profileName}\` first or choose a different name.`,
|
|
435
|
+
ExitCode.ERROR
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
await ProfileManager.ensureConfigDir();
|
|
439
|
+
const { jwk, did } = await withSpinner("Generating key...", async () => {
|
|
440
|
+
return generateKey();
|
|
441
|
+
});
|
|
442
|
+
await ProfileManager.setKey(profileName, jwk);
|
|
443
|
+
const profileConfig = {
|
|
444
|
+
name: profileName,
|
|
445
|
+
host,
|
|
446
|
+
chainId: DEFAULT_CHAIN_ID,
|
|
447
|
+
spaceName: "default",
|
|
448
|
+
did,
|
|
449
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
450
|
+
};
|
|
451
|
+
await ProfileManager.setProfile(profileName, profileConfig);
|
|
452
|
+
const config = await ProfileManager.getConfig();
|
|
453
|
+
if (profileName === "default" || !await ProfileManager.profileExists(config.defaultProfile)) {
|
|
454
|
+
await ProfileManager.setConfig({ ...config, defaultProfile: profileName });
|
|
455
|
+
}
|
|
456
|
+
if (options.keyOnly) {
|
|
457
|
+
outputJson({
|
|
458
|
+
profile: profileName,
|
|
459
|
+
did,
|
|
460
|
+
host,
|
|
461
|
+
authenticated: false
|
|
462
|
+
});
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const delegationData = await startAuthFlow(did, { paste: options.paste });
|
|
466
|
+
await ProfileManager.setSession(profileName, delegationData);
|
|
467
|
+
await ProfileManager.setProfile(profileName, {
|
|
468
|
+
...profileConfig,
|
|
469
|
+
spaceId: delegationData.spaceId,
|
|
470
|
+
primaryDid: delegationData.primaryDid
|
|
471
|
+
});
|
|
472
|
+
outputJson({
|
|
473
|
+
profile: profileName,
|
|
474
|
+
did,
|
|
475
|
+
host,
|
|
476
|
+
spaceId: delegationData.spaceId,
|
|
477
|
+
authenticated: true
|
|
478
|
+
});
|
|
479
|
+
} catch (error) {
|
|
480
|
+
handleError(error);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/commands/auth.ts
|
|
486
|
+
function registerAuthCommand(program2) {
|
|
487
|
+
const auth = program2.command("auth").description("Authentication management");
|
|
488
|
+
auth.command("login").description("Authenticate with OpenKey").option("--paste", "Use manual paste mode instead of browser callback").action(async (options, cmd) => {
|
|
489
|
+
try {
|
|
490
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
491
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
492
|
+
const key = await ProfileManager.getKey(ctx.profile);
|
|
493
|
+
if (!key) {
|
|
494
|
+
throw new CLIError(
|
|
495
|
+
"NO_KEY",
|
|
496
|
+
`No key found for profile "${ctx.profile}". Run \`tc init\` first.`,
|
|
497
|
+
ExitCode.AUTH_REQUIRED
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
501
|
+
const delegationData = await startAuthFlow(profile.did, {
|
|
502
|
+
paste: options.paste
|
|
503
|
+
});
|
|
504
|
+
await ProfileManager.setSession(ctx.profile, delegationData);
|
|
505
|
+
if (delegationData.spaceId) {
|
|
506
|
+
await ProfileManager.setProfile(ctx.profile, {
|
|
507
|
+
...profile,
|
|
508
|
+
spaceId: delegationData.spaceId,
|
|
509
|
+
primaryDid: delegationData.primaryDid
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
outputJson({
|
|
513
|
+
authenticated: true,
|
|
514
|
+
profile: ctx.profile,
|
|
515
|
+
did: profile.did,
|
|
516
|
+
spaceId: delegationData.spaceId
|
|
517
|
+
});
|
|
518
|
+
} catch (error) {
|
|
519
|
+
handleError(error);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
auth.command("logout").description("Clear session (keep key)").action(async (_options, cmd) => {
|
|
523
|
+
try {
|
|
524
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
525
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
526
|
+
await ProfileManager.clearSession(ctx.profile);
|
|
527
|
+
outputJson({ profile: ctx.profile, authenticated: false });
|
|
528
|
+
} catch (error) {
|
|
529
|
+
handleError(error);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
auth.command("status").description("Show current authentication state").action(async (_options, cmd) => {
|
|
533
|
+
try {
|
|
534
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
535
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
536
|
+
const hasKey = await ProfileManager.getKey(ctx.profile);
|
|
537
|
+
const session = await ProfileManager.getSession(ctx.profile);
|
|
538
|
+
let profile;
|
|
539
|
+
try {
|
|
540
|
+
profile = await ProfileManager.getProfile(ctx.profile);
|
|
541
|
+
} catch {
|
|
542
|
+
profile = null;
|
|
543
|
+
}
|
|
544
|
+
outputJson({
|
|
545
|
+
authenticated: session !== null,
|
|
546
|
+
did: profile?.did ?? null,
|
|
547
|
+
primaryDid: profile?.primaryDid ?? null,
|
|
548
|
+
spaceId: profile?.spaceId ?? null,
|
|
549
|
+
host: ctx.host,
|
|
550
|
+
profile: ctx.profile,
|
|
551
|
+
hasKey: hasKey !== null
|
|
552
|
+
});
|
|
553
|
+
} catch (error) {
|
|
554
|
+
handleError(error);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
auth.command("whoami").description("Show identity information").action(async (_options, cmd) => {
|
|
558
|
+
try {
|
|
559
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
560
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
561
|
+
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
562
|
+
const session = await ProfileManager.getSession(ctx.profile);
|
|
563
|
+
outputJson({
|
|
564
|
+
profile: ctx.profile,
|
|
565
|
+
did: profile.did,
|
|
566
|
+
primaryDid: profile.primaryDid ?? null,
|
|
567
|
+
spaceId: profile.spaceId ?? null,
|
|
568
|
+
host: profile.host,
|
|
569
|
+
authenticated: session !== null
|
|
570
|
+
});
|
|
571
|
+
} catch (error) {
|
|
572
|
+
handleError(error);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/commands/kv.ts
|
|
578
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
579
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
580
|
+
|
|
581
|
+
// src/lib/sdk.ts
|
|
582
|
+
import { TinyCloudNode } from "@tinycloud/node-sdk";
|
|
583
|
+
async function createSDKInstance(ctx) {
|
|
584
|
+
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
585
|
+
const session = await ProfileManager.getSession(ctx.profile);
|
|
586
|
+
const key = await ProfileManager.getKey(ctx.profile);
|
|
587
|
+
if (!key) {
|
|
588
|
+
throw new CLIError(
|
|
589
|
+
"AUTH_REQUIRED",
|
|
590
|
+
`No key found for profile "${ctx.profile}". Run \`tc init\` first.`,
|
|
591
|
+
ExitCode.AUTH_REQUIRED
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const node = new TinyCloudNode({
|
|
595
|
+
host: ctx.host
|
|
596
|
+
});
|
|
597
|
+
return node;
|
|
598
|
+
}
|
|
599
|
+
async function ensureAuthenticated(ctx) {
|
|
600
|
+
const session = await ProfileManager.getSession(ctx.profile);
|
|
601
|
+
if (!session) {
|
|
602
|
+
throw new CLIError(
|
|
603
|
+
"AUTH_REQUIRED",
|
|
604
|
+
`Not authenticated. Run \`tc auth login\` or \`tc init\` first.`,
|
|
605
|
+
ExitCode.AUTH_REQUIRED
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return createSDKInstance(ctx);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/commands/kv.ts
|
|
612
|
+
async function readStdin() {
|
|
613
|
+
const chunks = [];
|
|
614
|
+
for await (const chunk of process.stdin) {
|
|
615
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
616
|
+
}
|
|
617
|
+
return Buffer.concat(chunks);
|
|
618
|
+
}
|
|
619
|
+
function registerKvCommand(program2) {
|
|
620
|
+
const kv = program2.command("kv").description("Key-value store operations");
|
|
621
|
+
kv.command("get <key>").description("Get a value by key").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").action(async (key, options, cmd) => {
|
|
622
|
+
try {
|
|
623
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
624
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
625
|
+
const node = await ensureAuthenticated(ctx);
|
|
626
|
+
const result = await withSpinner(`Getting ${key}...`, () => node.kv.get(key));
|
|
627
|
+
if (!result.ok) {
|
|
628
|
+
if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
|
|
629
|
+
throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
|
|
630
|
+
}
|
|
631
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
632
|
+
}
|
|
633
|
+
const data = result.data.data;
|
|
634
|
+
const metadata = result.data.headers ?? {};
|
|
635
|
+
if (options.output) {
|
|
636
|
+
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
637
|
+
await writeFile2(options.output, content);
|
|
638
|
+
outputJson({ key, written: options.output });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (options.raw) {
|
|
642
|
+
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
643
|
+
process.stdout.write(content);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
outputJson({
|
|
647
|
+
key,
|
|
648
|
+
data,
|
|
649
|
+
metadata
|
|
650
|
+
});
|
|
651
|
+
} catch (error) {
|
|
652
|
+
handleError(error);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
kv.command("put <key> [value]").description("Set a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").action(async (key, value, options, cmd) => {
|
|
656
|
+
try {
|
|
657
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
658
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
659
|
+
const node = await ensureAuthenticated(ctx);
|
|
660
|
+
let putValue;
|
|
661
|
+
const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
|
|
662
|
+
if (sources.length === 0) {
|
|
663
|
+
throw new CLIError("USAGE_ERROR", "Must provide a value, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
664
|
+
}
|
|
665
|
+
if (sources.length > 1) {
|
|
666
|
+
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
667
|
+
}
|
|
668
|
+
if (options.file) {
|
|
669
|
+
putValue = await readFile2(options.file);
|
|
670
|
+
} else if (options.stdin) {
|
|
671
|
+
putValue = await readStdin();
|
|
672
|
+
} else {
|
|
673
|
+
try {
|
|
674
|
+
putValue = JSON.parse(value);
|
|
675
|
+
} catch {
|
|
676
|
+
putValue = value;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const result = await withSpinner(`Writing ${key}...`, () => node.kv.put(key, putValue));
|
|
680
|
+
if (!result.ok) {
|
|
681
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
682
|
+
}
|
|
683
|
+
outputJson({ key, written: true });
|
|
684
|
+
} catch (error) {
|
|
685
|
+
handleError(error);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
kv.command("delete <key>").description("Delete a key").action(async (key, _options, cmd) => {
|
|
689
|
+
try {
|
|
690
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
691
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
692
|
+
const node = await ensureAuthenticated(ctx);
|
|
693
|
+
const result = await withSpinner(`Deleting ${key}...`, () => node.kv.delete(key));
|
|
694
|
+
if (!result.ok) {
|
|
695
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
696
|
+
}
|
|
697
|
+
outputJson({ key, deleted: true });
|
|
698
|
+
} catch (error) {
|
|
699
|
+
handleError(error);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").action(async (options, cmd) => {
|
|
703
|
+
try {
|
|
704
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
705
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
706
|
+
const node = await ensureAuthenticated(ctx);
|
|
707
|
+
const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
|
|
708
|
+
const result = await withSpinner("Listing keys...", () => node.kv.list(listOptions));
|
|
709
|
+
if (!result.ok) {
|
|
710
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
711
|
+
}
|
|
712
|
+
const keys = result.data.data ?? result.data;
|
|
713
|
+
const keyList = Array.isArray(keys) ? keys : [];
|
|
714
|
+
outputJson({
|
|
715
|
+
keys: keyList,
|
|
716
|
+
count: keyList.length,
|
|
717
|
+
prefix: options.prefix ?? null
|
|
718
|
+
});
|
|
719
|
+
} catch (error) {
|
|
720
|
+
handleError(error);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
kv.command("head <key>").description("Get metadata for a key (no body)").action(async (key, _options, cmd) => {
|
|
724
|
+
try {
|
|
725
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
726
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
727
|
+
const node = await ensureAuthenticated(ctx);
|
|
728
|
+
const result = await withSpinner(`Checking ${key}...`, () => node.kv.head(key));
|
|
729
|
+
if (!result.ok) {
|
|
730
|
+
if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
|
|
731
|
+
outputJson({ key, exists: false, metadata: {} });
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
735
|
+
}
|
|
736
|
+
outputJson({
|
|
737
|
+
key,
|
|
738
|
+
exists: true,
|
|
739
|
+
metadata: result.data.headers ?? {}
|
|
740
|
+
});
|
|
741
|
+
} catch (error) {
|
|
742
|
+
handleError(error);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/commands/space.ts
|
|
748
|
+
function registerSpaceCommand(program2) {
|
|
749
|
+
const space = program2.command("space").description("Space management");
|
|
750
|
+
space.command("list").description("List spaces").action(async (_options, cmd) => {
|
|
751
|
+
try {
|
|
752
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
753
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
754
|
+
const node = await ensureAuthenticated(ctx);
|
|
755
|
+
const result = await node.spaces.list();
|
|
756
|
+
if (!result.ok) {
|
|
757
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
758
|
+
}
|
|
759
|
+
outputJson({ spaces: result.data, count: result.data.length });
|
|
760
|
+
} catch (error) {
|
|
761
|
+
handleError(error);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
space.command("create <name>").description("Create a new space").action(async (name, _options, cmd) => {
|
|
765
|
+
try {
|
|
766
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
767
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
768
|
+
const node = await ensureAuthenticated(ctx);
|
|
769
|
+
const result = await node.spaces.create(name);
|
|
770
|
+
if (!result.ok) {
|
|
771
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
772
|
+
}
|
|
773
|
+
outputJson({ spaceId: result.data.id, name });
|
|
774
|
+
} catch (error) {
|
|
775
|
+
handleError(error);
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
space.command("info [space-id]").description("Get space info").action(async (spaceId, _options, cmd) => {
|
|
779
|
+
try {
|
|
780
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
781
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
782
|
+
const node = await ensureAuthenticated(ctx);
|
|
783
|
+
const targetId = spaceId ?? node.spaceId;
|
|
784
|
+
if (!targetId) {
|
|
785
|
+
throw new CLIError("NO_SPACE", "No space ID specified and no active space", ExitCode.ERROR);
|
|
786
|
+
}
|
|
787
|
+
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
788
|
+
outputJson({
|
|
789
|
+
spaceId: targetId,
|
|
790
|
+
name: profile.spaceName,
|
|
791
|
+
owner: node.did,
|
|
792
|
+
host: ctx.host
|
|
793
|
+
});
|
|
794
|
+
} catch (error) {
|
|
795
|
+
handleError(error);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
space.command("switch <name>").description("Switch active space").action(async (name, _options, cmd) => {
|
|
799
|
+
try {
|
|
800
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
801
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
802
|
+
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
803
|
+
await ProfileManager.setProfile(ctx.profile, { ...profile, spaceName: name });
|
|
804
|
+
outputJson({ profile: ctx.profile, spaceName: name, switched: true });
|
|
805
|
+
} catch (error) {
|
|
806
|
+
handleError(error);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/lib/duration.ts
|
|
812
|
+
function parseDuration(input) {
|
|
813
|
+
const match = input.match(/^(\d+)(m|h|d|w)$/);
|
|
814
|
+
if (match) {
|
|
815
|
+
const value = parseInt(match[1], 10);
|
|
816
|
+
const unit = match[2];
|
|
817
|
+
const multipliers = {
|
|
818
|
+
m: 60 * 1e3,
|
|
819
|
+
h: 60 * 60 * 1e3,
|
|
820
|
+
d: 24 * 60 * 60 * 1e3,
|
|
821
|
+
w: 7 * 24 * 60 * 60 * 1e3
|
|
822
|
+
};
|
|
823
|
+
return value * multipliers[unit];
|
|
824
|
+
}
|
|
825
|
+
const date = new Date(input);
|
|
826
|
+
if (!isNaN(date.getTime())) {
|
|
827
|
+
const ms = date.getTime() - Date.now();
|
|
828
|
+
if (ms <= 0) {
|
|
829
|
+
throw new Error(`Expiry date "${input}" is in the past`);
|
|
830
|
+
}
|
|
831
|
+
return ms;
|
|
832
|
+
}
|
|
833
|
+
throw new Error(`Invalid duration: "${input}". Use format like "1h", "7d", or an ISO date.`);
|
|
834
|
+
}
|
|
835
|
+
function parseExpiry(input) {
|
|
836
|
+
return new Date(Date.now() + parseDuration(input));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/commands/delegation.ts
|
|
840
|
+
function registerDelegationCommand(program2) {
|
|
841
|
+
const delegation = program2.command("delegation").description("Manage delegations");
|
|
842
|
+
delegation.command("create").description("Create a delegation").requiredOption("--to <did>", "Recipient DID").requiredOption("--path <path>", "KV path scope").requiredOption("--actions <actions>", "Comma-separated actions (e.g., kv/get,kv/list)").option("--expiry <duration>", "Expiry duration (e.g., 1h, 7d, ISO date)", "1h").action(async (options, cmd) => {
|
|
843
|
+
try {
|
|
844
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
845
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
846
|
+
const node = await ensureAuthenticated(ctx);
|
|
847
|
+
const actions = options.actions.split(",").map((a) => {
|
|
848
|
+
const trimmed = a.trim();
|
|
849
|
+
return trimmed.startsWith("tinycloud.") ? trimmed : `tinycloud.${trimmed}`;
|
|
850
|
+
});
|
|
851
|
+
const expiry = parseExpiry(options.expiry);
|
|
852
|
+
const result = await node.delegationManager.create({
|
|
853
|
+
delegateDID: options.to,
|
|
854
|
+
path: options.path,
|
|
855
|
+
actions,
|
|
856
|
+
expiry
|
|
857
|
+
});
|
|
858
|
+
if (!result.ok) {
|
|
859
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
860
|
+
}
|
|
861
|
+
outputJson({
|
|
862
|
+
cid: result.data.cid,
|
|
863
|
+
delegateDid: options.to,
|
|
864
|
+
path: options.path,
|
|
865
|
+
actions,
|
|
866
|
+
expiry: expiry.toISOString()
|
|
867
|
+
});
|
|
868
|
+
} catch (error) {
|
|
869
|
+
handleError(error);
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
delegation.command("list").description("List delegations").option("--granted", "Show only delegations I've granted").option("--received", "Show only delegations I've received").action(async (options, cmd) => {
|
|
873
|
+
try {
|
|
874
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
875
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
876
|
+
const node = await ensureAuthenticated(ctx);
|
|
877
|
+
const result = await node.delegationManager.list();
|
|
878
|
+
if (!result.ok) {
|
|
879
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
880
|
+
}
|
|
881
|
+
let delegations = result.data;
|
|
882
|
+
if (options.granted) {
|
|
883
|
+
const myDid = node.did;
|
|
884
|
+
delegations = delegations.filter((d) => d.delegatorDID === myDid);
|
|
885
|
+
} else if (options.received) {
|
|
886
|
+
const myDid = node.did;
|
|
887
|
+
delegations = delegations.filter((d) => d.delegateDID === myDid || d.delegateDID?.includes(myDid));
|
|
888
|
+
}
|
|
889
|
+
outputJson({
|
|
890
|
+
delegations: delegations.map((d) => ({
|
|
891
|
+
cid: d.cid,
|
|
892
|
+
delegatee: d.delegateDID,
|
|
893
|
+
delegator: d.delegatorDID,
|
|
894
|
+
path: d.path,
|
|
895
|
+
actions: d.actions,
|
|
896
|
+
expiry: d.expiry instanceof Date ? d.expiry.toISOString() : d.expiry
|
|
897
|
+
})),
|
|
898
|
+
count: delegations.length
|
|
899
|
+
});
|
|
900
|
+
} catch (error) {
|
|
901
|
+
handleError(error);
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
delegation.command("info <cid>").description("Get delegation details").action(async (cid, _options, cmd) => {
|
|
905
|
+
try {
|
|
906
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
907
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
908
|
+
const node = await ensureAuthenticated(ctx);
|
|
909
|
+
const result = await node.delegationManager.get(cid);
|
|
910
|
+
if (!result.ok) {
|
|
911
|
+
throw new CLIError("NOT_FOUND", `Delegation "${cid}" not found`, ExitCode.NOT_FOUND);
|
|
912
|
+
}
|
|
913
|
+
outputJson(result.data);
|
|
914
|
+
} catch (error) {
|
|
915
|
+
handleError(error);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
delegation.command("revoke <cid>").description("Revoke a delegation").action(async (cid, _options, cmd) => {
|
|
919
|
+
try {
|
|
920
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
921
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
922
|
+
const node = await ensureAuthenticated(ctx);
|
|
923
|
+
const result = await node.delegationManager.revoke(cid);
|
|
924
|
+
if (!result.ok) {
|
|
925
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
926
|
+
}
|
|
927
|
+
outputJson({ cid, revoked: true });
|
|
928
|
+
} catch (error) {
|
|
929
|
+
handleError(error);
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/commands/share.ts
|
|
935
|
+
function registerShareCommand(program2) {
|
|
936
|
+
const share = program2.command("share").description("Share data with others");
|
|
937
|
+
share.command("create").description("Create a share link").requiredOption("--path <path>", "KV path scope").option("--actions <actions>", "Comma-separated actions", "kv/get").option("--expiry <duration>", "Expiry duration", "7d").option("--web-link", "Generate a web UI link for non-technical recipients").action(async (options, cmd) => {
|
|
938
|
+
try {
|
|
939
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
940
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
941
|
+
const node = await ensureAuthenticated(ctx);
|
|
942
|
+
const actions = options.actions.split(",").map((a) => {
|
|
943
|
+
const trimmed = a.trim();
|
|
944
|
+
return trimmed.startsWith("tinycloud.") ? trimmed : `tinycloud.${trimmed}`;
|
|
945
|
+
});
|
|
946
|
+
const expiry = parseExpiry(options.expiry);
|
|
947
|
+
const result = await node.sharing.generate({
|
|
948
|
+
path: options.path,
|
|
949
|
+
actions,
|
|
950
|
+
expiry
|
|
951
|
+
});
|
|
952
|
+
if (!result.ok) {
|
|
953
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
954
|
+
}
|
|
955
|
+
const output = {
|
|
956
|
+
token: result.data.token ?? result.data.cid,
|
|
957
|
+
shareData: result.data.encodedData ?? result.data.url,
|
|
958
|
+
path: options.path,
|
|
959
|
+
actions,
|
|
960
|
+
expiry: expiry.toISOString()
|
|
961
|
+
};
|
|
962
|
+
if (options.webLink) {
|
|
963
|
+
const shareData = result.data.encodedData ?? result.data.url ?? "";
|
|
964
|
+
output.webLink = `https://openkey.cloud/share?data=${encodeURIComponent(shareData)}`;
|
|
965
|
+
}
|
|
966
|
+
outputJson(output);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
handleError(error);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
share.command("receive [data]").description("Receive a share").option("--stdin", "Read share data from stdin").action(async (data, options, cmd) => {
|
|
972
|
+
try {
|
|
973
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
974
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
975
|
+
const node = await ensureAuthenticated(ctx);
|
|
976
|
+
let shareData;
|
|
977
|
+
if (options.stdin) {
|
|
978
|
+
const chunks = [];
|
|
979
|
+
for await (const chunk of process.stdin) {
|
|
980
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
981
|
+
}
|
|
982
|
+
shareData = Buffer.concat(chunks).toString("utf-8").trim();
|
|
983
|
+
} else if (data) {
|
|
984
|
+
shareData = data;
|
|
985
|
+
} else {
|
|
986
|
+
throw new CLIError("USAGE_ERROR", "Must provide share data or use --stdin", ExitCode.USAGE_ERROR);
|
|
987
|
+
}
|
|
988
|
+
const result = await node.sharing.receive(shareData);
|
|
989
|
+
if (!result.ok) {
|
|
990
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
991
|
+
}
|
|
992
|
+
outputJson({
|
|
993
|
+
received: true,
|
|
994
|
+
spaceId: result.data.spaceId,
|
|
995
|
+
path: result.data.path,
|
|
996
|
+
actions: result.data.actions
|
|
997
|
+
});
|
|
998
|
+
} catch (error) {
|
|
999
|
+
handleError(error);
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
share.command("list").description("List active shares").action(async (_options, cmd) => {
|
|
1003
|
+
try {
|
|
1004
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1005
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1006
|
+
const node = await ensureAuthenticated(ctx);
|
|
1007
|
+
const result = await node.sharing.list();
|
|
1008
|
+
if (!result.ok) {
|
|
1009
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
1010
|
+
}
|
|
1011
|
+
outputJson({ shares: result.data, count: result.data.length });
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
handleError(error);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
share.command("revoke <token>").description("Revoke a share").action(async (token, _options, cmd) => {
|
|
1017
|
+
try {
|
|
1018
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1019
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1020
|
+
const node = await ensureAuthenticated(ctx);
|
|
1021
|
+
const result = await node.sharing.revoke(token);
|
|
1022
|
+
if (!result.ok) {
|
|
1023
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
1024
|
+
}
|
|
1025
|
+
outputJson({ token, revoked: true });
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
handleError(error);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/commands/node.ts
|
|
1033
|
+
function registerNodeCommand(program2) {
|
|
1034
|
+
const node = program2.command("node").description("Node health and info");
|
|
1035
|
+
node.command("health").description("Check node health").action(async (_options, cmd) => {
|
|
1036
|
+
try {
|
|
1037
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1038
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1039
|
+
const start = Date.now();
|
|
1040
|
+
const response = await fetch(`${ctx.host}/healthz`);
|
|
1041
|
+
const latencyMs = Date.now() - start;
|
|
1042
|
+
outputJson({
|
|
1043
|
+
healthy: response.ok,
|
|
1044
|
+
host: ctx.host,
|
|
1045
|
+
latencyMs
|
|
1046
|
+
});
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
1049
|
+
outputJson({ healthy: false, host: (await ProfileManager.resolveContext(cmd.optsWithGlobals())).host, error: "Connection refused" });
|
|
1050
|
+
} else {
|
|
1051
|
+
handleError(error);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
node.command("version").description("Get node version").action(async (_options, cmd) => {
|
|
1056
|
+
try {
|
|
1057
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1058
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1059
|
+
const response = await fetch(`${ctx.host}/version`);
|
|
1060
|
+
if (!response.ok) {
|
|
1061
|
+
throw new CLIError("NODE_ERROR", `Node returned ${response.status}`, ExitCode.NODE_ERROR);
|
|
1062
|
+
}
|
|
1063
|
+
const data = await response.json();
|
|
1064
|
+
outputJson({ ...data, host: ctx.host });
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
handleError(error);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
node.command("status").description("Combined health and version info").action(async (_options, cmd) => {
|
|
1070
|
+
try {
|
|
1071
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1072
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1073
|
+
const start = Date.now();
|
|
1074
|
+
const [healthRes, versionRes] = await Promise.allSettled([
|
|
1075
|
+
fetch(`${ctx.host}/healthz`),
|
|
1076
|
+
fetch(`${ctx.host}/version`)
|
|
1077
|
+
]);
|
|
1078
|
+
const latencyMs = Date.now() - start;
|
|
1079
|
+
const healthy = healthRes.status === "fulfilled" && healthRes.value.ok;
|
|
1080
|
+
let versionData = {};
|
|
1081
|
+
if (versionRes.status === "fulfilled" && versionRes.value.ok) {
|
|
1082
|
+
versionData = await versionRes.value.json();
|
|
1083
|
+
}
|
|
1084
|
+
outputJson({
|
|
1085
|
+
healthy,
|
|
1086
|
+
host: ctx.host,
|
|
1087
|
+
latencyMs,
|
|
1088
|
+
...versionData
|
|
1089
|
+
});
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
handleError(error);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/commands/profile.ts
|
|
1097
|
+
import { createInterface as createInterface2 } from "readline";
|
|
1098
|
+
function registerProfileCommand(program2) {
|
|
1099
|
+
const profile = program2.command("profile").description("Profile management");
|
|
1100
|
+
profile.command("list").description("List all profiles").action(async (_options, cmd) => {
|
|
1101
|
+
try {
|
|
1102
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1103
|
+
const config = await ProfileManager.getConfig();
|
|
1104
|
+
const names = await ProfileManager.listProfiles();
|
|
1105
|
+
const profiles = await Promise.all(
|
|
1106
|
+
names.map(async (name) => {
|
|
1107
|
+
try {
|
|
1108
|
+
const p = await ProfileManager.getProfile(name);
|
|
1109
|
+
return {
|
|
1110
|
+
name: p.name,
|
|
1111
|
+
host: p.host,
|
|
1112
|
+
did: p.did,
|
|
1113
|
+
active: name === config.defaultProfile
|
|
1114
|
+
};
|
|
1115
|
+
} catch {
|
|
1116
|
+
return { name, host: null, did: null, active: name === config.defaultProfile };
|
|
1117
|
+
}
|
|
1118
|
+
})
|
|
1119
|
+
);
|
|
1120
|
+
outputJson({
|
|
1121
|
+
profiles,
|
|
1122
|
+
defaultProfile: config.defaultProfile
|
|
1123
|
+
});
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
handleError(error);
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
profile.command("create <name>").description("Create a new profile").option("--host <url>", "TinyCloud node URL").action(async (name, options, cmd) => {
|
|
1129
|
+
try {
|
|
1130
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1131
|
+
const host = options.host ?? globalOpts.host ?? "https://node.tinycloud.xyz";
|
|
1132
|
+
if (await ProfileManager.profileExists(name)) {
|
|
1133
|
+
throw new CLIError("PROFILE_EXISTS", `Profile "${name}" already exists`, ExitCode.ERROR);
|
|
1134
|
+
}
|
|
1135
|
+
await ProfileManager.ensureConfigDir();
|
|
1136
|
+
const { jwk, did } = generateKey();
|
|
1137
|
+
await ProfileManager.setKey(name, jwk);
|
|
1138
|
+
await ProfileManager.setProfile(name, {
|
|
1139
|
+
name,
|
|
1140
|
+
host,
|
|
1141
|
+
chainId: 1,
|
|
1142
|
+
spaceName: "default",
|
|
1143
|
+
did,
|
|
1144
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1145
|
+
});
|
|
1146
|
+
outputJson({ profile: name, did, host, created: true });
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
handleError(error);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
profile.command("show [name]").description("Show profile details").action(async (name, _options, cmd) => {
|
|
1152
|
+
try {
|
|
1153
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1154
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1155
|
+
const profileName = name ?? ctx.profile;
|
|
1156
|
+
const p = await ProfileManager.getProfile(profileName);
|
|
1157
|
+
const hasKey = await ProfileManager.getKey(profileName) !== null;
|
|
1158
|
+
const hasSession = await ProfileManager.getSession(profileName) !== null;
|
|
1159
|
+
const config = await ProfileManager.getConfig();
|
|
1160
|
+
outputJson({
|
|
1161
|
+
...p,
|
|
1162
|
+
hasKey,
|
|
1163
|
+
hasSession,
|
|
1164
|
+
isDefault: profileName === config.defaultProfile
|
|
1165
|
+
});
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
handleError(error);
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
profile.command("switch <name>").description("Set default profile").action(async (name, _options, cmd) => {
|
|
1171
|
+
try {
|
|
1172
|
+
if (!await ProfileManager.profileExists(name)) {
|
|
1173
|
+
throw new CLIError("PROFILE_NOT_FOUND", `Profile "${name}" does not exist`, ExitCode.NOT_FOUND);
|
|
1174
|
+
}
|
|
1175
|
+
const config = await ProfileManager.getConfig();
|
|
1176
|
+
await ProfileManager.setConfig({ ...config, defaultProfile: name });
|
|
1177
|
+
outputJson({ defaultProfile: name, switched: true });
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
handleError(error);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
profile.command("delete <name>").description("Delete a profile").action(async (name, _options, cmd) => {
|
|
1183
|
+
try {
|
|
1184
|
+
if (isInteractive()) {
|
|
1185
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
1186
|
+
const answer = await new Promise((resolve) => {
|
|
1187
|
+
rl.question(`Delete profile "${name}"? This cannot be undone. [y/N] `, resolve);
|
|
1188
|
+
});
|
|
1189
|
+
rl.close();
|
|
1190
|
+
if (answer.toLowerCase() !== "y") {
|
|
1191
|
+
outputJson({ profile: name, deleted: false, reason: "Cancelled by user" });
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
await ProfileManager.deleteProfile(name);
|
|
1196
|
+
outputJson({ profile: name, deleted: true });
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
handleError(error);
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/commands/completion.ts
|
|
1204
|
+
function registerCompletionCommand(program2) {
|
|
1205
|
+
const completion = program2.command("completion").description("Generate shell completions");
|
|
1206
|
+
completion.command("bash").description("Output bash completions").action(() => {
|
|
1207
|
+
const script = generateBashCompletion();
|
|
1208
|
+
process.stdout.write(script);
|
|
1209
|
+
});
|
|
1210
|
+
completion.command("zsh").description("Output zsh completions").action(() => {
|
|
1211
|
+
const script = generateZshCompletion();
|
|
1212
|
+
process.stdout.write(script);
|
|
1213
|
+
});
|
|
1214
|
+
completion.command("fish").description("Output fish completions").action(() => {
|
|
1215
|
+
const script = generateFishCompletion();
|
|
1216
|
+
process.stdout.write(script);
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
function generateBashCompletion() {
|
|
1220
|
+
return `# tc bash completion
|
|
1221
|
+
_tc_completions() {
|
|
1222
|
+
local cur prev commands subcommands
|
|
1223
|
+
COMPREPLY=()
|
|
1224
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1225
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1226
|
+
|
|
1227
|
+
commands="init auth kv space delegation share node profile completion"
|
|
1228
|
+
|
|
1229
|
+
case "\${COMP_WORDS[1]}" in
|
|
1230
|
+
auth) subcommands="login logout status whoami" ;;
|
|
1231
|
+
kv) subcommands="get put delete list head" ;;
|
|
1232
|
+
space) subcommands="list create info switch" ;;
|
|
1233
|
+
delegation) subcommands="create list info revoke" ;;
|
|
1234
|
+
share) subcommands="create receive list revoke" ;;
|
|
1235
|
+
node) subcommands="health version status" ;;
|
|
1236
|
+
profile) subcommands="list create show switch delete" ;;
|
|
1237
|
+
completion) subcommands="bash zsh fish" ;;
|
|
1238
|
+
*) COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") ); return ;;
|
|
1239
|
+
esac
|
|
1240
|
+
|
|
1241
|
+
if [ \${COMP_CWORD} -eq 2 ]; then
|
|
1242
|
+
COMPREPLY=( $(compgen -W "\${subcommands}" -- "\${cur}") )
|
|
1243
|
+
fi
|
|
1244
|
+
}
|
|
1245
|
+
complete -F _tc_completions tc
|
|
1246
|
+
`;
|
|
1247
|
+
}
|
|
1248
|
+
function generateZshCompletion() {
|
|
1249
|
+
return `#compdef tc
|
|
1250
|
+
|
|
1251
|
+
_tc() {
|
|
1252
|
+
local -a commands
|
|
1253
|
+
commands=(
|
|
1254
|
+
'init:Initialize a new TinyCloud profile'
|
|
1255
|
+
'auth:Authentication management'
|
|
1256
|
+
'kv:Key-value store operations'
|
|
1257
|
+
'space:Space management'
|
|
1258
|
+
'delegation:Manage delegations'
|
|
1259
|
+
'share:Share data with others'
|
|
1260
|
+
'node:Node health and info'
|
|
1261
|
+
'profile:Profile management'
|
|
1262
|
+
'completion:Generate shell completions'
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
_arguments -C \\
|
|
1266
|
+
'(-p --profile)'{-p,--profile}'[Profile to use]:profile:' \\
|
|
1267
|
+
'(-H --host)'{-H,--host}'[TinyCloud node URL]:url:' \\
|
|
1268
|
+
'(-v --verbose)'{-v,--verbose}'[Enable verbose output]' \\
|
|
1269
|
+
'--no-cache[Disable caching]' \\
|
|
1270
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]' \\
|
|
1271
|
+
'1:command:->cmd' \\
|
|
1272
|
+
'*::arg:->args'
|
|
1273
|
+
|
|
1274
|
+
case $state in
|
|
1275
|
+
cmd)
|
|
1276
|
+
_describe 'command' commands
|
|
1277
|
+
;;
|
|
1278
|
+
args)
|
|
1279
|
+
case $words[1] in
|
|
1280
|
+
auth) _values 'subcommand' login logout status whoami ;;
|
|
1281
|
+
kv) _values 'subcommand' get put delete list head ;;
|
|
1282
|
+
space) _values 'subcommand' list create info switch ;;
|
|
1283
|
+
delegation) _values 'subcommand' create list info revoke ;;
|
|
1284
|
+
share) _values 'subcommand' create receive list revoke ;;
|
|
1285
|
+
node) _values 'subcommand' health version status ;;
|
|
1286
|
+
profile) _values 'subcommand' list create show switch delete ;;
|
|
1287
|
+
completion) _values 'subcommand' bash zsh fish ;;
|
|
1288
|
+
esac
|
|
1289
|
+
;;
|
|
1290
|
+
esac
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
_tc
|
|
1294
|
+
`;
|
|
1295
|
+
}
|
|
1296
|
+
function generateFishCompletion() {
|
|
1297
|
+
return `# tc fish completion
|
|
1298
|
+
set -l commands init auth kv space delegation share node profile completion
|
|
1299
|
+
|
|
1300
|
+
# Disable file completion by default
|
|
1301
|
+
complete -c tc -f
|
|
1302
|
+
|
|
1303
|
+
# Top-level commands
|
|
1304
|
+
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a init -d "Initialize a new TinyCloud profile"
|
|
1305
|
+
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a auth -d "Authentication management"
|
|
1306
|
+
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a kv -d "Key-value store operations"
|
|
1307
|
+
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a space -d "Space management"
|
|
1308
|
+
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a delegation -d "Manage delegations"
|
|
1309
|
+
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a share -d "Share data with others"
|
|
1310
|
+
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a node -d "Node health and info"
|
|
1311
|
+
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a profile -d "Profile management"
|
|
1312
|
+
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a completion -d "Generate shell completions"
|
|
1313
|
+
|
|
1314
|
+
# Subcommands
|
|
1315
|
+
complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout status whoami"
|
|
1316
|
+
complete -c tc -n "__fish_seen_subcommand_from kv" -a "get put delete list head"
|
|
1317
|
+
complete -c tc -n "__fish_seen_subcommand_from space" -a "list create info switch"
|
|
1318
|
+
complete -c tc -n "__fish_seen_subcommand_from delegation" -a "create list info revoke"
|
|
1319
|
+
complete -c tc -n "__fish_seen_subcommand_from share" -a "create receive list revoke"
|
|
1320
|
+
complete -c tc -n "__fish_seen_subcommand_from node" -a "health version status"
|
|
1321
|
+
complete -c tc -n "__fish_seen_subcommand_from profile" -a "list create show switch delete"
|
|
1322
|
+
complete -c tc -n "__fish_seen_subcommand_from completion" -a "bash zsh fish"
|
|
1323
|
+
|
|
1324
|
+
# Global options
|
|
1325
|
+
complete -c tc -l profile -s p -d "Profile to use"
|
|
1326
|
+
complete -c tc -l host -s H -d "TinyCloud node URL"
|
|
1327
|
+
complete -c tc -l verbose -s v -d "Enable verbose output"
|
|
1328
|
+
complete -c tc -l no-cache -d "Disable caching"
|
|
1329
|
+
complete -c tc -l quiet -s q -d "Suppress non-essential output"
|
|
1330
|
+
`;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// src/index.ts
|
|
1334
|
+
var program = new Command();
|
|
1335
|
+
program.name("tc").description("TinyCloud CLI").version("0.1.0").option("-p, --profile <name>", "Profile to use").option("-H, --host <url>", "TinyCloud node URL").option("-v, --verbose", "Enable verbose output").option("--no-cache", "Disable caching").option("-q, --quiet", "Suppress non-essential output");
|
|
1336
|
+
registerInitCommand(program);
|
|
1337
|
+
registerAuthCommand(program);
|
|
1338
|
+
registerKvCommand(program);
|
|
1339
|
+
registerSpaceCommand(program);
|
|
1340
|
+
registerDelegationCommand(program);
|
|
1341
|
+
registerShareCommand(program);
|
|
1342
|
+
registerNodeCommand(program);
|
|
1343
|
+
registerProfileCommand(program);
|
|
1344
|
+
registerCompletionCommand(program);
|
|
1345
|
+
try {
|
|
1346
|
+
await program.parseAsync(process.argv);
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
handleError(error);
|
|
1349
|
+
}
|
|
1350
|
+
//# sourceMappingURL=index.js.map
|