@tenyokjs/devdevian-cli 0.0.0-stage → 0.2.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,68 @@
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 open # a separate terminal window
8
+ # or: devdevian tui (the current terminal)
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
+ ## Live workspace
14
+
15
+ The default `devdevian` command and `devdevian tui` open an alternate terminal screen. Navigation, conversations, inline forms, previews and search update in that screen; exiting restores your normal terminal. The blue and purple layout adapts to the terminal size, with compact navigation below 90 columns. Minimum size: 60 columns × 18 rows.
16
+
17
+ - **Tab** switches between navigation, content and the command bar. **↑/↓** select; **Enter** opens; **Esc** goes back.
18
+ - **/** focuses search and commands: `/feed`, `/inbox`, `/search query`, `/open 142`, `/ask`, `/review`, `/communities`, `/devices`, `/help`, `/logout`, `/quit`.
19
+ - **←/→** switch conversation tabs; **Space** reads the selected reply/remark in full. **[ / ]** paginate; **Page Up/Down** scroll.
20
+ - **N** creates a conversation (a community on the Communities screen); **A** asks a question; **R** replies inside a conversation; **Shift+R** refreshes.
21
+ - **C** comments on snippet lines; **E/D** edit/delete your own feedback; **S** resolves/reopens a code remark as the publication author; **!** reports selected code feedback.
22
+ - In forms, **Tab/Shift+Tab** change fields, arrows choose options or move the text cursor, **Enter** adds a line, **Ctrl+S** opens the preview. Publication requires a separate confirmation; Escape asks before discarding a draft. Drafts are kept in memory only.
23
+ - **Ctrl+C** exits. With an unsent draft, confirm leaving or press Ctrl+C again.
24
+
25
+ `devdevian open` launches Terminal on macOS, Windows Terminal on Windows, or GNOME Terminal/Konsole/xterm on a Linux desktop. A graphical desktop and an installed supported terminal are required; over SSH and inside an editor, use `devdevian tui`. Window launchers still need platform-specific user testing; terminal rendering and input are covered by macOS/Linux pseudo-terminal checks.
26
+
27
+ Pages have a bounded 30-second in-memory cache. Refresh explicitly with Shift+R. Network errors stay inside the workspace; failed writes are never automatically retried. Existing web permissions and server validation apply to every operation.
28
+
29
+ ## Everyday commands
30
+
31
+ ```sh
32
+ devdevian whoami
33
+ devdevian inbox --page 1
34
+ devdevian feed --topic web
35
+ devdevian search "bounded retries"
36
+ devdevian conversation open 142
37
+ devdevian ask --title "How should I retry requests?" --body-file question.md --topic web
38
+ devdevian reply 142 --editor
39
+ devdevian review create --file src/retry.ts --lines 20:65 --title "Review my retry strategy" --focus "Timeouts, retries and idempotency" --editor
40
+ devdevian review list 142 --status open
41
+ devdevian review comment 142 --start-line 2 --end-line 4 --body "Handle the timeout here."
42
+ devdevian review thread 142 12
43
+ devdevian review reply 142 12 --body "Would a deadline address this?"
44
+ devdevian review resolve 142 12
45
+ devdevian review reopen 142 12
46
+ devdevian communities
47
+ devdevian logout
48
+ ```
49
+
50
+ 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.
51
+
52
+ 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.
53
+
54
+ `--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.
55
+
56
+ ## Connected devices and credentials
57
+
58
+ 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.
59
+
60
+ 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`.
61
+
62
+ 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.
63
+
64
+ ## Alpha scope
65
+
66
+ This alpha covers login/logout, inbox, paginated feeds and search, discussions and replies, questions, code review creation, line feedback and feedback threads/status, and community discovery, creation and private publication. Profile/media editing, events administration and moderation remain available in the browser. There is no offline mode or automatic GitHub repository synchronization.
67
+
68
+ 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,488 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
3
+ import packageInfo from "../package.json" with { type: "json" };
4
+ import { parseArgs } from "node:util";
5
+ import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises";
6
+ import { hostname, tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { spawn, spawnSync } from "node:child_process";
9
+ import { createInterface } from "node:readline/promises";
10
+ import { setTimeout as delay } from "node:timers/promises";
11
+ import {
12
+ client,
13
+ originOf,
14
+ loadConfig,
15
+ saveConfig,
16
+ removeConfig,
17
+ clean,
18
+ } from "../lib/client.mjs";
19
+ const version = packageInfo.version;
20
+ const options = {
21
+ host: { type: "string" },
22
+ page: { type: "string" },
23
+ topic: { type: "string" },
24
+ community: { type: "string" },
25
+ title: { type: "string" },
26
+ body: { type: "string" },
27
+ "body-file": { type: "string" },
28
+ file: { type: "string" },
29
+ lines: { type: "string" },
30
+ focus: { type: "string" },
31
+ "start-line": { type: "string" },
32
+ "end-line": { type: "string" },
33
+ status: { type: "string" },
34
+ revision: { type: "string" },
35
+ "reply-id": { type: "string" },
36
+ name: { type: "string" },
37
+ yes: { type: "boolean" },
38
+ json: { type: "boolean" },
39
+ editor: { type: "boolean" },
40
+ "no-open": { type: "boolean" },
41
+ help: { type: "boolean", short: "h" },
42
+ version: { type: "boolean", short: "v" },
43
+ };
44
+ let terminal;
45
+ const prompt = async (question, fallback = "") => {
46
+ if (!process.stdin.isTTY)
47
+ throw Error(
48
+ "Interactive input needs a terminal. Provide command flags instead.",
49
+ );
50
+ terminal ??= createInterface({
51
+ input: process.stdin,
52
+ output: process.stdout,
53
+ });
54
+ const answer = await terminal.question(
55
+ question + (fallback ? ` [${fallback}]` : "") + ": ",
56
+ );
57
+ return answer.trim() || fallback;
58
+ };
59
+ const out = (value) => {
60
+ const text = clean(value);
61
+ process.stdout.write(
62
+ (process.stdout.isTTY &&
63
+ !process.env.NO_COLOR &&
64
+ /^(DEVDEVIAN|Devdevian ·|Publication preview)/.test(text.trimStart())
65
+ ? "\x1b[38;5;141m" + text + "\x1b[0m"
66
+ : text) + "\n",
67
+ );
68
+ };
69
+ const positive = (value, label = "ID") => {
70
+ if (!/^[1-9]\d*$/.test(String(value)) || !Number.isSafeInteger(Number(value)))
71
+ throw Error(label + " must be a positive integer.");
72
+ return Number(value);
73
+ };
74
+ const help = `Devdevian CLI ${version} · alpha
75
+
76
+ devdevian Live terminal workspace
77
+ devdevian tui Live workspace in this terminal
78
+ devdevian open Open the workspace in a new terminal window
79
+ devdevian login Authorize in your browser
80
+ devdevian logout Revoke this terminal
81
+ devdevian whoami
82
+ devdevian inbox [--page 2]
83
+ devdevian feed [--topic web] [--page 2]
84
+ devdevian search "retry strategies"
85
+ devdevian conversation open POST_ID
86
+ devdevian ask --title "..." --body-file question.md --topic web
87
+ devdevian reply POST_ID --body "..." [--editor]
88
+ devdevian review create --file src/retry.ts --lines 20:65 --title "..." --focus "..."
89
+ devdevian review list POST_ID [--status open]
90
+ devdevian review comment POST_ID --start-line 2 --end-line 4 --body "..."
91
+ devdevian review thread POST_ID REVIEW_ID [--page 2]
92
+ devdevian review reply POST_ID REVIEW_ID --body "..."
93
+ devdevian review resolve POST_ID REVIEW_ID
94
+ devdevian review reopen POST_ID REVIEW_ID
95
+ devdevian communities [--page 2]
96
+
97
+ --community ID Publish to an approved private community
98
+ --editor Write text in $VISUAL / $EDITOR
99
+ --json Machine-readable output
100
+ --yes Explicitly publish without an interactive confirmation
101
+ --host ORIGIN Select server when logging in (HTTPS; loopback HTTP for development)
102
+
103
+ Only files you explicitly select are read. Code/text is previewed before publishing.
104
+ Credentials are stored locally with restricted file permissions; revoke access in Settings.
105
+ `;
106
+ async function editor(initial = "") {
107
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
108
+ throw Error("--editor needs an interactive terminal.");
109
+ terminal?.pause();
110
+ const directory = await mkdtemp(join(tmpdir(), "devdevian-editor-")),
111
+ file = join(directory, "message.md");
112
+ try {
113
+ await writeFile(file, initial, { mode: 0o600 });
114
+ const command =
115
+ process.env.VISUAL ||
116
+ process.env.EDITOR ||
117
+ (process.platform === "win32" ? "notepad" : "vi");
118
+ // No shell: metacharacters in content or filenames cannot become commands.
119
+ const parts =
120
+ command
121
+ .match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)
122
+ ?.map((p) => p.replace(/^(['"])(.*)\1$/, "$2")) || [];
123
+ if (!parts.length) throw Error("Set EDITOR to your text editor.");
124
+ const result = spawnSync(parts[0], [...parts.slice(1), file], {
125
+ stdio: "inherit",
126
+ shell: false,
127
+ });
128
+ if (result.error || result.status !== 0)
129
+ throw Error("The editor did not finish successfully.");
130
+ return await readFile(file, "utf8");
131
+ } finally {
132
+ terminal?.resume();
133
+ await rm(directory, { recursive: true, force: true });
134
+ }
135
+ }
136
+ async function boundedFile(file, max) {
137
+ const data = await readFile(file);
138
+ if (data.length > max * 4)
139
+ throw Error(`File is too large (limit ${max} characters).`);
140
+ const text = data.toString("utf8");
141
+ if (text.includes("\u0000") || text.includes("\ufffd") || text.length > max)
142
+ throw Error(`Choose a UTF-8 text file with at most ${max} characters.`);
143
+ return text;
144
+ }
145
+ async function message(flags, initial = "") {
146
+ if (flags["body-file"]) return boundedFile(flags["body-file"], 15000);
147
+ if (flags.editor || (!flags.body && process.stdin.isTTY))
148
+ return editor(flags.body || initial);
149
+ if (!flags.body) throw Error("Provide --body, --body-file, or --editor.");
150
+ return flags.body;
151
+ }
152
+ function show(data, json, kind = "posts") {
153
+ if (json) {
154
+ process.stdout.write(JSON.stringify(data) + "\n");
155
+ return;
156
+ }
157
+ const items = data.items || data;
158
+ if (Array.isArray(items)) {
159
+ if (!items.length) out("Nothing here yet.");
160
+ for (const item of items) {
161
+ if (kind === "inbox")
162
+ out(
163
+ `${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 : ""}`,
164
+ );
165
+ else if (kind === "reviews")
166
+ out(
167
+ `#${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 || ""}`,
168
+ );
169
+ else if (kind === "replies")
170
+ out(
171
+ `#${item.id} · ${item.name || ""}\n${item.deleted_at ? "[deleted]" : item.body}`,
172
+ );
173
+ else
174
+ out(
175
+ `#${item.id} ${item.title || item.name || ""}${item.kind ? " · " + item.kind : ""}${item.topic ? " · #" + item.topic : ""}`,
176
+ );
177
+ }
178
+ if (data.pagination)
179
+ out(
180
+ `Page ${data.pagination.page}/${data.pagination.pages || 1} · ${data.pagination.total} results`,
181
+ );
182
+ } else if (data.title) {
183
+ out(`#${data.id} · ${data.kind}\n${data.title}\n\n${data.body}`);
184
+ if (data.code) out("\nCode:\n" + data.code);
185
+ } else out(JSON.stringify(data, null, 2));
186
+ }
187
+ async function confirm(payload, flags) {
188
+ if (flags.yes) return;
189
+ out("\nPublication preview");
190
+ out(JSON.stringify(payload, null, 2));
191
+ out(
192
+ "Audience: " +
193
+ (payload.community_id
194
+ ? "private community " + payload.community_id
195
+ : "PUBLIC feed"),
196
+ );
197
+ if (
198
+ (await prompt("Publish this content? (yes/no)", "no")).toLowerCase() !==
199
+ "yes"
200
+ )
201
+ throw Error("Cancelled. Nothing was published.");
202
+ }
203
+ function openBrowser(url) {
204
+ const args =
205
+ process.platform === "darwin"
206
+ ? ["open", [url]]
207
+ : process.platform === "win32"
208
+ ? ["rundll32", ["url.dll,FileProtocolHandler", url]]
209
+ : ["xdg-open", [url]];
210
+ const child = spawn(args[0], args[1], {
211
+ stdio: "ignore",
212
+ detached: true,
213
+ shell: false,
214
+ });
215
+ child.on("error", () => {});
216
+ child.unref();
217
+ }
218
+ async function login(flags) {
219
+ const origin = originOf(flags.host || "https://devdevian.vercel.app"),
220
+ api = client(origin);
221
+ const device = await api("/cli/device", {
222
+ method: "POST",
223
+ body: { name: flags.name || `Devdevian CLI · ${hostname()}` },
224
+ });
225
+ if (new URL(device.verification_uri).origin !== origin)
226
+ throw Error("The server returned an unexpected authorization address.");
227
+ out(
228
+ `Open ${device.verification_uri}\nConfirm code: ${device.user_code}\nWaiting for browser approval…`,
229
+ );
230
+ if (!flags["no-open"]) openBrowser(device.verification_uri);
231
+ const until = Date.now() + Math.min(device.expires_in, 600) * 1000;
232
+ while (Date.now() < until) {
233
+ await delay(Math.max(5, Number(device.interval) || 5) * 1000);
234
+ const result = await api("/cli/token", {
235
+ method: "POST",
236
+ body: { device_code: device.device_code },
237
+ });
238
+ if (result.pending) continue;
239
+ await saveConfig({
240
+ origin,
241
+ token: result.access_token,
242
+ expires_at: result.expires_at,
243
+ });
244
+ out("Connected. Run devdevian to open your workspace.");
245
+ return;
246
+ }
247
+ throw Error("Login expired. Run devdevian login again.");
248
+ }
249
+ async function execute(args, flags) {
250
+ if (flags.help) {
251
+ out(help);
252
+ return;
253
+ }
254
+ if (flags.version) {
255
+ out(version);
256
+ return;
257
+ }
258
+ const command = args[0];
259
+ if (command === "login") return login(flags);
260
+ const config = await loadConfig();
261
+ if (!config) throw Error("Sign in first: devdevian login");
262
+ if (flags.host && originOf(flags.host) !== config.origin)
263
+ throw Error(
264
+ "Saved credentials belong to a different server. Run login --host for that server.",
265
+ );
266
+ const api = client(config.origin, config.token),
267
+ page = positive(flags.page || "1", "Page");
268
+ if (command === "logout") {
269
+ try {
270
+ await api("/cli/logout", { method: "POST", body: {} });
271
+ } catch (e) {
272
+ if (e.status !== 401) throw e;
273
+ }
274
+ await removeConfig();
275
+ out("This terminal has been disconnected.");
276
+ return;
277
+ }
278
+ if (command === "whoami") {
279
+ const data = await api("/me");
280
+ show(
281
+ flags.json
282
+ ? data
283
+ : {
284
+ account: "@" + data.user.username,
285
+ name: data.user.name,
286
+ server: config.origin,
287
+ },
288
+ flags.json,
289
+ );
290
+ return;
291
+ }
292
+ if (command === "inbox") {
293
+ show(await api("/notifications?page=" + page), flags.json, "inbox");
294
+ return;
295
+ }
296
+ if (command === "feed") {
297
+ const query = new URLSearchParams({
298
+ page: String(page),
299
+ limit: "12",
300
+ ...(flags.topic ? { topic: flags.topic } : {}),
301
+ });
302
+ show(await api("/posts?" + query), flags.json);
303
+ return;
304
+ }
305
+ if (command === "communities") {
306
+ show(await api("/communities?page=" + page + "&limit=12"), flags.json);
307
+ return;
308
+ }
309
+ if (command === "search") {
310
+ const query = args.slice(1).join(" ") || (await prompt("Search"));
311
+ show(
312
+ await api("/search?q=" + encodeURIComponent(query) + "&page=" + page),
313
+ flags.json,
314
+ );
315
+ return;
316
+ }
317
+ if (command === "conversation" && args[1] === "open") {
318
+ const id = positive(args[2]);
319
+ show(await api("/posts/" + id), flags.json);
320
+ if (!flags.json) {
321
+ out("\nReplies:");
322
+ show(await api(`/posts/${id}/replies?page=${page}`), false, "replies");
323
+ }
324
+ return;
325
+ }
326
+ if (command === "ask" || (command === "review" && args[1] === "create")) {
327
+ const review = command === "review";
328
+ let code = "";
329
+ if (review) {
330
+ const file = flags.file || (await prompt("Code file"));
331
+ code = await boundedFile(file, 20000);
332
+ if (flags.lines) {
333
+ const m = flags.lines.match(/^(\d+):(\d+)$/);
334
+ if (!m) throw Error("Use --lines START:END.");
335
+ const lines = code.split("\n"),
336
+ start = positive(m[1]),
337
+ end = positive(m[2]);
338
+ if (end < start || end > lines.length)
339
+ throw Error("The selected range is outside the file.");
340
+ code = lines.slice(start - 1, end).join("\n");
341
+ }
342
+ }
343
+ const payload = {
344
+ kind: review ? "review" : "question",
345
+ topic: flags.topic || (await prompt("Topic ID", "web")),
346
+ title: flags.title || (await prompt("Title")),
347
+ body: await message(
348
+ flags,
349
+ review
350
+ ? "Explain what the code does and the help you need."
351
+ : "Describe the problem, expected behavior, and what you tried.",
352
+ ),
353
+ ...(flags.community ? { community_id: flags.community } : {}),
354
+ };
355
+ if (review) {
356
+ payload.code = code;
357
+ payload.review_focus =
358
+ flags.focus || (await prompt("What should reviewers focus on?"));
359
+ }
360
+ await confirm(payload, flags);
361
+ const result = await api("/posts", { method: "POST", body: payload });
362
+ show(result, flags.json);
363
+ if (!flags.json) out(config.origin + "/app#post/" + result.id);
364
+ return;
365
+ }
366
+ if (command === "reply") {
367
+ const id = positive(args[1]),
368
+ body = await message(flags);
369
+ const parent = await api("/posts/" + id);
370
+ await confirm(
371
+ { post_id: id, community_id: parent.community_id || null, body },
372
+ flags,
373
+ );
374
+ show(
375
+ await api(`/posts/${id}/replies`, { method: "POST", body: { body } }),
376
+ flags.json,
377
+ );
378
+ return;
379
+ }
380
+ if (command === "review") {
381
+ const action = args[1],
382
+ post = positive(args[2]);
383
+ if (action === "list") {
384
+ show(
385
+ await api(
386
+ `/posts/${post}/code-reviews?page=${page}${flags.status ? "&status=" + encodeURIComponent(flags.status) : ""}`,
387
+ ),
388
+ flags.json,
389
+ "reviews",
390
+ );
391
+ return;
392
+ }
393
+ if (action === "comment") {
394
+ const current = await api(`/posts/${post}/code-reviews`),
395
+ payload = {
396
+ body: await message(flags),
397
+ start_line: positive(flags["start-line"], "Start line"),
398
+ end_line: positive(
399
+ flags["end-line"] || flags["start-line"],
400
+ "End line",
401
+ ),
402
+ snippet_hash: current.snippet_hash,
403
+ };
404
+ const parent = await api("/posts/" + post);
405
+ await confirm(
406
+ {
407
+ post_id: post,
408
+ community_id: parent.community_id || null,
409
+ ...payload,
410
+ },
411
+ flags,
412
+ );
413
+ show(
414
+ await api(`/posts/${post}/code-reviews`, {
415
+ method: "POST",
416
+ body: payload,
417
+ }),
418
+ flags.json,
419
+ );
420
+ return;
421
+ }
422
+ const id = positive(args[3]),
423
+ url = `/posts/${post}/code-reviews/${id}`;
424
+ if (action === "thread") {
425
+ show(await api(url + "/replies?page=" + page), flags.json, "replies");
426
+ return;
427
+ }
428
+ if (action === "reply") {
429
+ const body = await message(flags);
430
+ const parent = await api("/posts/" + post);
431
+ await confirm(
432
+ {
433
+ post_id: post,
434
+ community_id: parent.community_id || null,
435
+ review_id: id,
436
+ body,
437
+ },
438
+ flags,
439
+ );
440
+ show(
441
+ await api(url + "/replies", { method: "POST", body: { body } }),
442
+ flags.json,
443
+ );
444
+ return;
445
+ }
446
+ if (["resolve", "reopen"].includes(action)) {
447
+ const data = await api(`/posts/${post}/code-reviews?target_id=${id}`),
448
+ item = data.items.find((c) => c.id === id);
449
+ if (!item) throw Error("Feedback not found.");
450
+ show(
451
+ await api(url, {
452
+ method: "PATCH",
453
+ body: { revision: item.revision, resolved: action === "resolve" },
454
+ }),
455
+ flags.json,
456
+ );
457
+ return;
458
+ }
459
+ }
460
+ throw Error("Unknown command. Run devdevian --help.");
461
+ }
462
+
463
+ try {
464
+ const { values, positionals } = parseArgs({
465
+ options,
466
+ allowPositionals: true,
467
+ strict: true,
468
+ });
469
+ if (!positionals.length && !values.help && !values.version)
470
+ await (await import("../lib/tui.mjs")).startTui(values);
471
+ else if (positionals[0] === "tui")
472
+ await (await import("../lib/tui.mjs")).startTui(values);
473
+ else if (positionals[0] === "open") {
474
+ const args = [];
475
+ for (const key of ["host", "name"])
476
+ if (values[key]) args.push("--" + key, values[key]);
477
+ if (values["no-open"]) args.push("--no-open");
478
+ await (
479
+ await import("../lib/window.mjs")
480
+ ).openWindow({ entry: fileURLToPath(import.meta.url), args });
481
+ out("Devdevian opened in a separate terminal window.");
482
+ } else await execute(positionals, values);
483
+ } catch (e) {
484
+ process.stderr.write("Devdevian: " + clean(e.message) + "\n");
485
+ process.exitCode = 1;
486
+ } finally {
487
+ terminal?.close();
488
+ }