@tenyokjs/devdevian-cli 0.0.0-stage → 0.1.0-alpha.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/README.md CHANGED
@@ -1,3 +1,52 @@
1
- # Temporary Holding Version
1
+ # Devdevian CLI · Alpha
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An official terminal client for [Devdevian](https://devdevian.vercel.app), the open-source developer forum. Use the same account, discussions and community permissions as the web app. Requires **Node.js 24 or newer**.
4
+
5
+ ```sh
6
+ npm install -g @tenyokjs/devdevian-cli@alpha
7
+ devdevian login
8
+ devdevian
9
+ ```
10
+
11
+ Login opens a browser. Compare the code shown in the terminal, sign in to Devdevian, then authorize the terminal. Google, GitHub and email login happen on the website; no provider credentials are collected by this package. Use `--no-open` to open the printed address yourself, including when connecting over SSH.
12
+
13
+ ## Everyday commands
14
+
15
+ ```sh
16
+ devdevian whoami
17
+ devdevian inbox --page 1
18
+ devdevian feed --topic web
19
+ devdevian search "bounded retries"
20
+ devdevian conversation open 142
21
+ devdevian ask --title "How should I retry requests?" --body-file question.md --topic web
22
+ devdevian reply 142 --editor
23
+ devdevian review create --file src/retry.ts --lines 20:65 --title "Review my retry strategy" --focus "Timeouts, retries and idempotency" --editor
24
+ devdevian review list 142 --status open
25
+ devdevian review comment 142 --start-line 2 --end-line 4 --body "Handle the timeout here."
26
+ devdevian review thread 142 12
27
+ devdevian review reply 142 12 --body "Would a deadline address this?"
28
+ devdevian review resolve 142 12
29
+ devdevian review reopen 142 12
30
+ devdevian communities
31
+ devdevian logout
32
+ ```
33
+
34
+ Commands are examples; replace IDs, file paths and topic IDs with your own. Run `devdevian --help` for flags. Selected file ranges are published as standalone snippets; subsequent review line numbers refer to that published snippet, beginning at 1.
35
+
36
+ A selected file is read only when explicitly named. Before publication, the client previews the payload and audience and asks for confirmation. Use `--community ID` for an approved private community, `--json` for machine-readable API output and `--yes` to explicitly publish without a prompt. Noninteractive publishing requires the relevant text flags and `--yes`. No repository discovery or background file collection runs.
37
+
38
+ `--editor` uses `$VISUAL`, then `$EDITOR`, then `vi` (`notepad` on Windows). The editor is launched without a shell. File, body and API limits also apply in the terminal. This client uses ordinary HTTPS API calls, not a direct database connection, and sends no background polling traffic when idle. Device login polls every five seconds for at most ten minutes.
39
+
40
+ ## Connected devices and credentials
41
+
42
+ Access expires after 30 days. Revoke a terminal from **Settings → Connected terminals** or run `devdevian logout`. Password reset revokes CLI sessions alongside browser sessions. Terminal authorization requests approved before a password change cannot mint a new session afterwards.
43
+
44
+ The credential file is `~/.config/devdevian/credentials.json`, with mode `0600` and a new directory with mode `0700` on Unix. This alpha uses a local token file rather than an OS keychain. On Windows, protect your user profile with appropriate account permissions; Unix permission bits are not equivalent to Windows ACLs. Do not share or commit the credential file. Tokens are never printed by login, including with `--json`.
45
+
46
+ For local development use `devdevian login --host http://127.0.0.1:4173`. Remote origins require HTTPS. Credentials stay bound to the selected origin; a different `--host` cannot reuse them. HTTP redirects are rejected instead of forwarding credentials. `DEVDEVIAN_CONFIG_DIR` overrides the local credentials directory for isolated testing. `NO_COLOR=1` disables terminal accents.
47
+
48
+ ## Alpha scope
49
+
50
+ This first release covers login/logout, inbox, paginated feeds and search, discussions and replies, questions, code review creation, line feedback and feedback threads/status, and community discovery/private publication. Profile/media editing, events administration and moderation remain available in the browser. There is no offline mode or automatic GitHub repository synchronization.
51
+
52
+ Source, contributions and issue reporting: [Tenyokj/devdevian](https://github.com/Tenyokj/devdevian). Report security issues through the repository's [security policy](https://github.com/Tenyokj/devdevian/blob/main/SECURITY.md), not a public issue containing private data. Licensed GPL-3.0-only.
@@ -0,0 +1,525 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises";
4
+ import { hostname, tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { createInterface } from "node:readline/promises";
8
+ import { setTimeout as delay } from "node:timers/promises";
9
+ import {
10
+ client,
11
+ originOf,
12
+ loadConfig,
13
+ saveConfig,
14
+ removeConfig,
15
+ clean,
16
+ } from "../lib/client.mjs";
17
+ const version = "0.1.0-alpha.1";
18
+ const options = {
19
+ host: { type: "string" },
20
+ page: { type: "string" },
21
+ topic: { type: "string" },
22
+ community: { type: "string" },
23
+ title: { type: "string" },
24
+ body: { type: "string" },
25
+ "body-file": { type: "string" },
26
+ file: { type: "string" },
27
+ lines: { type: "string" },
28
+ focus: { type: "string" },
29
+ "start-line": { type: "string" },
30
+ "end-line": { type: "string" },
31
+ status: { type: "string" },
32
+ revision: { type: "string" },
33
+ "reply-id": { type: "string" },
34
+ name: { type: "string" },
35
+ yes: { type: "boolean" },
36
+ json: { type: "boolean" },
37
+ editor: { type: "boolean" },
38
+ "no-open": { type: "boolean" },
39
+ help: { type: "boolean", short: "h" },
40
+ version: { type: "boolean", short: "v" },
41
+ };
42
+ let terminal;
43
+ const prompt = async (question, fallback = "") => {
44
+ if (!process.stdin.isTTY)
45
+ throw Error(
46
+ "Interactive input needs a terminal. Provide command flags instead.",
47
+ );
48
+ terminal ??= createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ });
52
+ const answer = await terminal.question(
53
+ question + (fallback ? ` [${fallback}]` : "") + ": ",
54
+ );
55
+ return answer.trim() || fallback;
56
+ };
57
+ const out = (value) => {
58
+ const text = clean(value);
59
+ process.stdout.write(
60
+ (process.stdout.isTTY &&
61
+ !process.env.NO_COLOR &&
62
+ /^(DEVDEVIAN|Devdevian ·|Publication preview)/.test(text.trimStart())
63
+ ? "\x1b[38;5;141m" + text + "\x1b[0m"
64
+ : text) + "\n",
65
+ );
66
+ };
67
+ const positive = (value, label = "ID") => {
68
+ if (!/^[1-9]\d*$/.test(String(value)) || !Number.isSafeInteger(Number(value)))
69
+ throw Error(label + " must be a positive integer.");
70
+ return Number(value);
71
+ };
72
+ const help = `Devdevian CLI ${version} · alpha
73
+
74
+ devdevian Interactive menu
75
+ devdevian login Authorize in your browser
76
+ devdevian logout Revoke this terminal
77
+ devdevian whoami
78
+ devdevian inbox [--page 2]
79
+ devdevian feed [--topic web] [--page 2]
80
+ devdevian search "retry strategies"
81
+ devdevian conversation open POST_ID
82
+ devdevian ask --title "..." --body-file question.md --topic web
83
+ devdevian reply POST_ID --body "..." [--editor]
84
+ devdevian review create --file src/retry.ts --lines 20:65 --title "..." --focus "..."
85
+ devdevian review list POST_ID [--status open]
86
+ devdevian review comment POST_ID --start-line 2 --end-line 4 --body "..."
87
+ devdevian review thread POST_ID REVIEW_ID [--page 2]
88
+ devdevian review reply POST_ID REVIEW_ID --body "..."
89
+ devdevian review resolve POST_ID REVIEW_ID
90
+ devdevian review reopen POST_ID REVIEW_ID
91
+ devdevian communities [--page 2]
92
+
93
+ --community ID Publish to an approved private community
94
+ --editor Write text in $VISUAL / $EDITOR
95
+ --json Machine-readable output
96
+ --yes Explicitly publish without an interactive confirmation
97
+ --host ORIGIN Select server when logging in (HTTPS; loopback HTTP for development)
98
+
99
+ Only files you explicitly select are read. Code/text is previewed before publishing.
100
+ Credentials are stored locally with restricted file permissions; revoke access in Settings.
101
+ `;
102
+ async function editor(initial = "") {
103
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
104
+ throw Error("--editor needs an interactive terminal.");
105
+ terminal?.pause();
106
+ const directory = await mkdtemp(join(tmpdir(), "devdevian-editor-")),
107
+ file = join(directory, "message.md");
108
+ try {
109
+ await writeFile(file, initial, { mode: 0o600 });
110
+ const command =
111
+ process.env.VISUAL ||
112
+ process.env.EDITOR ||
113
+ (process.platform === "win32" ? "notepad" : "vi");
114
+ // No shell: metacharacters in content or filenames cannot become commands.
115
+ const parts =
116
+ command
117
+ .match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)
118
+ ?.map((p) => p.replace(/^(['"])(.*)\1$/, "$2")) || [];
119
+ if (!parts.length) throw Error("Set EDITOR to your text editor.");
120
+ const result = spawnSync(parts[0], [...parts.slice(1), file], {
121
+ stdio: "inherit",
122
+ shell: false,
123
+ });
124
+ if (result.error || result.status !== 0)
125
+ throw Error("The editor did not finish successfully.");
126
+ return await readFile(file, "utf8");
127
+ } finally {
128
+ terminal?.resume();
129
+ await rm(directory, { recursive: true, force: true });
130
+ }
131
+ }
132
+ async function boundedFile(file, max) {
133
+ const data = await readFile(file);
134
+ if (data.length > max * 4)
135
+ throw Error(`File is too large (limit ${max} characters).`);
136
+ const text = data.toString("utf8");
137
+ if (text.includes("\u0000") || text.includes("\ufffd") || text.length > max)
138
+ throw Error(`Choose a UTF-8 text file with at most ${max} characters.`);
139
+ return text;
140
+ }
141
+ async function message(flags, initial = "") {
142
+ if (flags["body-file"]) return boundedFile(flags["body-file"], 15000);
143
+ if (flags.editor || (!flags.body && process.stdin.isTTY))
144
+ return editor(flags.body || initial);
145
+ if (!flags.body) throw Error("Provide --body, --body-file, or --editor.");
146
+ return flags.body;
147
+ }
148
+ function show(data, json, kind = "posts") {
149
+ if (json) {
150
+ process.stdout.write(JSON.stringify(data) + "\n");
151
+ return;
152
+ }
153
+ const items = data.items || data;
154
+ if (Array.isArray(items)) {
155
+ if (!items.length) out("Nothing here yet.");
156
+ for (const item of items) {
157
+ if (kind === "inbox")
158
+ out(
159
+ `${item.read_at ? " " : "●"} #${item.id} ${item.actor_name} · ${item.type}\n ${item.post_title || ""}\n ${item.post_id ? "Post " + item.post_id : ""}${item.code_review_id ? " · review " + item.code_review_id : ""}${item.feedback_reply_id ? " · reply " + item.feedback_reply_id : ""}`,
160
+ );
161
+ else if (kind === "reviews")
162
+ out(
163
+ `#${item.id} · ${item.resolved ? "resolved" : "open"} · lines ${item.start_line}–${item.end_line}${item.outdated ? " · earlier snippet" : ""}\n${item.deleted_at ? "[deleted]" : item.body}\n${item.quote || ""}`,
164
+ );
165
+ else if (kind === "replies")
166
+ out(
167
+ `#${item.id} · ${item.name || ""}\n${item.deleted_at ? "[deleted]" : item.body}`,
168
+ );
169
+ else
170
+ out(
171
+ `#${item.id} ${item.title || item.name || ""}${item.kind ? " · " + item.kind : ""}${item.topic ? " · #" + item.topic : ""}`,
172
+ );
173
+ }
174
+ if (data.pagination)
175
+ out(
176
+ `Page ${data.pagination.page}/${data.pagination.pages || 1} · ${data.pagination.total} results`,
177
+ );
178
+ } else if (data.title) {
179
+ out(`#${data.id} · ${data.kind}\n${data.title}\n\n${data.body}`);
180
+ if (data.code) out("\nCode:\n" + data.code);
181
+ } else out(JSON.stringify(data, null, 2));
182
+ }
183
+ async function confirm(payload, flags) {
184
+ if (flags.yes) return;
185
+ out("\nPublication preview");
186
+ out(JSON.stringify(payload, null, 2));
187
+ out(
188
+ "Audience: " +
189
+ (payload.community_id
190
+ ? "private community " + payload.community_id
191
+ : "PUBLIC feed"),
192
+ );
193
+ if (
194
+ (await prompt("Publish this content? (yes/no)", "no")).toLowerCase() !==
195
+ "yes"
196
+ )
197
+ throw Error("Cancelled. Nothing was published.");
198
+ }
199
+ function openBrowser(url) {
200
+ const args =
201
+ process.platform === "darwin"
202
+ ? ["open", [url]]
203
+ : process.platform === "win32"
204
+ ? ["rundll32", ["url.dll,FileProtocolHandler", url]]
205
+ : ["xdg-open", [url]];
206
+ const child = spawn(args[0], args[1], {
207
+ stdio: "ignore",
208
+ detached: true,
209
+ shell: false,
210
+ });
211
+ child.on("error", () => {});
212
+ child.unref();
213
+ }
214
+ async function login(flags) {
215
+ const origin = originOf(flags.host || "https://devdevian.vercel.app"),
216
+ api = client(origin);
217
+ const device = await api("/cli/device", {
218
+ method: "POST",
219
+ body: { name: flags.name || `Devdevian CLI · ${hostname()}` },
220
+ });
221
+ if (new URL(device.verification_uri).origin !== origin)
222
+ throw Error("The server returned an unexpected authorization address.");
223
+ out(
224
+ `Open ${device.verification_uri}\nConfirm code: ${device.user_code}\nWaiting for browser approval…`,
225
+ );
226
+ if (!flags["no-open"]) openBrowser(device.verification_uri);
227
+ const until = Date.now() + Math.min(device.expires_in, 600) * 1000;
228
+ while (Date.now() < until) {
229
+ await delay(Math.max(5, Number(device.interval) || 5) * 1000);
230
+ const result = await api("/cli/token", {
231
+ method: "POST",
232
+ body: { device_code: device.device_code },
233
+ });
234
+ if (result.pending) continue;
235
+ await saveConfig({
236
+ origin,
237
+ token: result.access_token,
238
+ expires_at: result.expires_at,
239
+ });
240
+ out("Connected. Run devdevian to open your workspace.");
241
+ return;
242
+ }
243
+ throw Error("Login expired. Run devdevian login again.");
244
+ }
245
+ async function execute(args, flags) {
246
+ if (flags.help) {
247
+ out(help);
248
+ return;
249
+ }
250
+ if (flags.version) {
251
+ out(version);
252
+ return;
253
+ }
254
+ const command = args[0];
255
+ if (command === "login") return login(flags);
256
+ const config = await loadConfig();
257
+ if (!config) throw Error("Sign in first: devdevian login");
258
+ if (flags.host && originOf(flags.host) !== config.origin)
259
+ throw Error(
260
+ "Saved credentials belong to a different server. Run login --host for that server.",
261
+ );
262
+ const api = client(config.origin, config.token),
263
+ page = positive(flags.page || "1", "Page");
264
+ if (command === "logout") {
265
+ try {
266
+ await api("/cli/logout", { method: "POST", body: {} });
267
+ } catch (e) {
268
+ if (e.status !== 401) throw e;
269
+ }
270
+ await removeConfig();
271
+ out("This terminal has been disconnected.");
272
+ return;
273
+ }
274
+ if (command === "whoami") {
275
+ const data = await api("/me");
276
+ show(
277
+ flags.json
278
+ ? data
279
+ : {
280
+ account: "@" + data.user.username,
281
+ name: data.user.name,
282
+ server: config.origin,
283
+ },
284
+ flags.json,
285
+ );
286
+ return;
287
+ }
288
+ if (command === "inbox") {
289
+ show(await api("/notifications?page=" + page), flags.json, "inbox");
290
+ return;
291
+ }
292
+ if (command === "feed") {
293
+ const query = new URLSearchParams({
294
+ page: String(page),
295
+ limit: "12",
296
+ ...(flags.topic ? { topic: flags.topic } : {}),
297
+ });
298
+ show(await api("/posts?" + query), flags.json);
299
+ return;
300
+ }
301
+ if (command === "communities") {
302
+ show(await api("/communities?page=" + page + "&limit=12"), flags.json);
303
+ return;
304
+ }
305
+ if (command === "search") {
306
+ const query = args.slice(1).join(" ") || (await prompt("Search"));
307
+ show(
308
+ await api("/search?q=" + encodeURIComponent(query) + "&page=" + page),
309
+ flags.json,
310
+ );
311
+ return;
312
+ }
313
+ if (command === "conversation" && args[1] === "open") {
314
+ const id = positive(args[2]);
315
+ show(await api("/posts/" + id), flags.json);
316
+ if (!flags.json) {
317
+ out("\nReplies:");
318
+ show(await api(`/posts/${id}/replies?page=${page}`), false, "replies");
319
+ }
320
+ return;
321
+ }
322
+ if (command === "ask" || (command === "review" && args[1] === "create")) {
323
+ const review = command === "review";
324
+ let code = "";
325
+ if (review) {
326
+ const file = flags.file || (await prompt("Code file"));
327
+ code = await boundedFile(file, 20000);
328
+ if (flags.lines) {
329
+ const m = flags.lines.match(/^(\d+):(\d+)$/);
330
+ if (!m) throw Error("Use --lines START:END.");
331
+ const lines = code.split("\n"),
332
+ start = positive(m[1]),
333
+ end = positive(m[2]);
334
+ if (end < start || end > lines.length)
335
+ throw Error("The selected range is outside the file.");
336
+ code = lines.slice(start - 1, end).join("\n");
337
+ }
338
+ }
339
+ const payload = {
340
+ kind: review ? "review" : "question",
341
+ topic: flags.topic || (await prompt("Topic ID", "web")),
342
+ title: flags.title || (await prompt("Title")),
343
+ body: await message(
344
+ flags,
345
+ review
346
+ ? "Explain what the code does and the help you need."
347
+ : "Describe the problem, expected behavior, and what you tried.",
348
+ ),
349
+ ...(flags.community ? { community_id: flags.community } : {}),
350
+ };
351
+ if (review) {
352
+ payload.code = code;
353
+ payload.review_focus =
354
+ flags.focus || (await prompt("What should reviewers focus on?"));
355
+ }
356
+ await confirm(payload, flags);
357
+ const result = await api("/posts", { method: "POST", body: payload });
358
+ show(result, flags.json);
359
+ if (!flags.json) out(config.origin + "/app#post/" + result.id);
360
+ return;
361
+ }
362
+ if (command === "reply") {
363
+ const id = positive(args[1]),
364
+ body = await message(flags);
365
+ const parent = await api("/posts/" + id);
366
+ await confirm(
367
+ { post_id: id, community_id: parent.community_id || null, body },
368
+ flags,
369
+ );
370
+ show(
371
+ await api(`/posts/${id}/replies`, { method: "POST", body: { body } }),
372
+ flags.json,
373
+ );
374
+ return;
375
+ }
376
+ if (command === "review") {
377
+ const action = args[1],
378
+ post = positive(args[2]);
379
+ if (action === "list") {
380
+ show(
381
+ await api(
382
+ `/posts/${post}/code-reviews?page=${page}${flags.status ? "&status=" + encodeURIComponent(flags.status) : ""}`,
383
+ ),
384
+ flags.json,
385
+ "reviews",
386
+ );
387
+ return;
388
+ }
389
+ if (action === "comment") {
390
+ const current = await api(`/posts/${post}/code-reviews`),
391
+ payload = {
392
+ body: await message(flags),
393
+ start_line: positive(flags["start-line"], "Start line"),
394
+ end_line: positive(
395
+ flags["end-line"] || flags["start-line"],
396
+ "End line",
397
+ ),
398
+ snippet_hash: current.snippet_hash,
399
+ };
400
+ const parent = await api("/posts/" + post);
401
+ await confirm(
402
+ {
403
+ post_id: post,
404
+ community_id: parent.community_id || null,
405
+ ...payload,
406
+ },
407
+ flags,
408
+ );
409
+ show(
410
+ await api(`/posts/${post}/code-reviews`, {
411
+ method: "POST",
412
+ body: payload,
413
+ }),
414
+ flags.json,
415
+ );
416
+ return;
417
+ }
418
+ const id = positive(args[3]),
419
+ url = `/posts/${post}/code-reviews/${id}`;
420
+ if (action === "thread") {
421
+ show(await api(url + "/replies?page=" + page), flags.json, "replies");
422
+ return;
423
+ }
424
+ if (action === "reply") {
425
+ const body = await message(flags);
426
+ const parent = await api("/posts/" + post);
427
+ await confirm(
428
+ {
429
+ post_id: post,
430
+ community_id: parent.community_id || null,
431
+ review_id: id,
432
+ body,
433
+ },
434
+ flags,
435
+ );
436
+ show(
437
+ await api(url + "/replies", { method: "POST", body: { body } }),
438
+ flags.json,
439
+ );
440
+ return;
441
+ }
442
+ if (["resolve", "reopen"].includes(action)) {
443
+ const data = await api(`/posts/${post}/code-reviews?target_id=${id}`),
444
+ item = data.items.find((c) => c.id === id);
445
+ if (!item) throw Error("Feedback not found.");
446
+ show(
447
+ await api(url, {
448
+ method: "PATCH",
449
+ body: { revision: item.revision, resolved: action === "resolve" },
450
+ }),
451
+ flags.json,
452
+ );
453
+ return;
454
+ }
455
+ }
456
+ throw Error("Unknown command. Run devdevian --help.");
457
+ }
458
+ async function menu(flags) {
459
+ const config = await loadConfig();
460
+ if (!config) {
461
+ out("Devdevian · developer commons");
462
+ if (
463
+ (await prompt("Connect your account? (yes/no)", "yes")).toLowerCase() !==
464
+ "yes"
465
+ )
466
+ return;
467
+ await login(flags);
468
+ }
469
+ const identityConfig = await loadConfig(),
470
+ identity = await client(identityConfig.origin, identityConfig.token)("/me");
471
+ while (true) {
472
+ out(
473
+ `\nDEVDEVIAN · @${identity.user.username}\n 1 Inbox\n 2 Public feed\n 3 Open conversation\n 4 Ask a question\n 5 Request code review\n 6 Communities\n 7 Review feedback\n 8 Search\n 9 Reply to a conversation\n 0 Exit`,
474
+ );
475
+ const choice = await prompt("Choose", "0");
476
+ if (choice === "0") return;
477
+ try {
478
+ if (choice === "1") await execute(["inbox"], flags);
479
+ else if (choice === "2") await execute(["feed"], flags);
480
+ else if (choice === "3")
481
+ await execute(["conversation", "open", await prompt("Post ID")], flags);
482
+ else if (choice === "4") await execute(["ask"], flags);
483
+ else if (choice === "5") await execute(["review", "create"], flags);
484
+ else if (choice === "6") await execute(["communities"], flags);
485
+ else if (choice === "7") {
486
+ const post = await prompt("Review post ID");
487
+ await execute(["review", "list", post], flags);
488
+ const action = await prompt(
489
+ "Action: thread / comment / reply / resolve / reopen / back",
490
+ "back",
491
+ );
492
+ if (action !== "back") {
493
+ const id = action === "comment" ? null : await prompt("Feedback ID");
494
+ const next = { ...flags };
495
+ if (action === "comment") {
496
+ next["start-line"] = await prompt("Start line");
497
+ next["end-line"] = await prompt("End line", next["start-line"]);
498
+ }
499
+ await execute(["review", action, post, ...(id ? [id] : [])], next);
500
+ }
501
+ } else if (choice === "9")
502
+ await execute(["reply", await prompt("Post ID")], flags);
503
+ else if (choice === "8")
504
+ await execute(["search", await prompt("Search")], flags);
505
+ else out("Choose one of the listed actions.");
506
+ } catch (e) {
507
+ out("Error: " + e.message);
508
+ }
509
+ }
510
+ }
511
+ try {
512
+ const { values, positionals } = parseArgs({
513
+ options,
514
+ allowPositionals: true,
515
+ strict: true,
516
+ });
517
+ if (!positionals.length && !values.help && !values.version)
518
+ await menu(values);
519
+ else await execute(positionals, values);
520
+ } catch (e) {
521
+ process.stderr.write("Devdevian: " + clean(e.message) + "\n");
522
+ process.exitCode = 1;
523
+ } finally {
524
+ terminal?.close();
525
+ }