@kernelius/forge-cli 0.1.3 → 0.2.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,74 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0] - 2026-02-01
9
+
10
+ ### Added
11
+ **Major Feature Expansion: ~60+ new commands for UI parity**
12
+
13
+ #### Repository Commands
14
+ - `repos fork` - Fork repositories
15
+ - `repos star` / `repos unstar` - Star/unstar repositories
16
+ - `repos stars` - List starred repositories
17
+ - `repos edit` - Edit repository details (name, description, visibility)
18
+ - `repos delete` - Delete repositories
19
+
20
+ #### Issue Management
21
+ - `issues edit` - Edit issue title, body, or state
22
+ - `issues reopen` - Reopen closed issues
23
+ - `issues comments` - List issue comments
24
+ - `issues labels` - List repository labels
25
+ - `issues label-add` / `issues label-remove` - Manage issue labels
26
+ - `issues assign` / `issues unassign` - Manage issue assignees
27
+
28
+ #### Pull Request Management
29
+ - `prs edit` - Edit PR title or body
30
+ - `prs reopen` - Reopen closed PRs
31
+ - `prs draft` / `prs ready` - Manage draft status
32
+ - `prs review` - Submit reviews (approve/request changes/comment)
33
+ - `prs reviews` - List PR reviews
34
+ - `prs commits` - List commits in PR
35
+ - `prs diff` - View PR diff statistics
36
+
37
+ #### Organization Management (New)
38
+ - `orgs list` - List organizations
39
+ - `orgs view` - View organization details
40
+ - `orgs create` - Create new organization
41
+ - `orgs members` - List organization members
42
+ - `orgs member-add` / `orgs member-remove` - Manage members
43
+ - `orgs teams` - List organization teams
44
+ - `orgs team-create` - Create teams
45
+ - `orgs team-members` - List team members
46
+ - `orgs team-add-member` / `orgs team-remove-member` - Manage team membership
47
+
48
+ #### User Management (New)
49
+ - `user profile` - View user profiles (self or others)
50
+ - `user edit` - Edit profile (name, bio, location, website, pronouns, company, git-email)
51
+ - `user search` - Search for users
52
+ - `user repos` - List user repositories
53
+
54
+ ### Changed
55
+ - Significantly improved CLI coverage from ~13% to ~40% of UI capabilities
56
+ - All commands now support proper error handling and formatted output
57
+ - Added color-coded output with emojis for better UX
58
+
59
+ ### Documentation
60
+ - Created comprehensive gap analysis document
61
+ - Identified all UI features for complete parity roadmap
62
+
63
+ ## [0.1.4] - 2026-02-01
64
+
65
+ ### Added
66
+ - **auth signup**: New command to create user account with agent in one step
67
+ - Creates both human user account and agent account
68
+ - Automatically generates and saves API key
69
+ - No manual authentication needed after signup
70
+ - Supports custom agent username, name, and emoji
71
+ - Example: `forge auth signup --email user@example.com --agent-username myagent --agent-name "My Agent"`
72
+
73
+ ### Changed
74
+ - Agent signup now provides immediate CLI access with automatic config saving
75
+
8
76
  ## [0.1.3] - 2026-02-01
9
77
 
10
78
  ### Fixed
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command5 } from "commander";
4
+ import { Command as Command7 } from "commander";
5
5
  import { readFileSync } from "fs";
6
6
  import { fileURLToPath } from "url";
7
7
  import { dirname, join as join2 } from "path";
@@ -112,6 +112,9 @@ async function apiPatch(endpoint, data) {
112
112
  body: data ? JSON.stringify(data) : void 0
113
113
  });
114
114
  }
115
+ async function apiDelete(endpoint) {
116
+ return apiRequest(endpoint, { method: "DELETE" });
117
+ }
115
118
 
116
119
  // src/commands/auth.ts
117
120
  function createAuthCommand() {
@@ -213,6 +216,87 @@ function createAuthCommand() {
213
216
  process.exit(1);
214
217
  }
215
218
  });
219
+ auth.command("signup").description("Create a new user account with an agent").requiredOption("--email <email>", "User email address").requiredOption("--user-name <name>", "User display name").requiredOption("--password <password>", "User password").requiredOption("--agent-username <username>", "Agent username (e.g., myagent)").requiredOption("--agent-name <name>", "Agent display name").option("--agent-emoji <emoji>", "Agent emoji (e.g., \u{1F916})").option("--api-url <url>", "Forge API URL", "http://localhost:3001").action(async (options) => {
220
+ try {
221
+ const {
222
+ email,
223
+ userName,
224
+ password,
225
+ agentUsername,
226
+ agentName,
227
+ agentEmoji,
228
+ apiUrl
229
+ } = options;
230
+ if (!/^[a-zA-Z0-9_-]+$/.test(agentUsername)) {
231
+ console.error(
232
+ chalk.red(
233
+ "Error: Agent username can only contain letters, numbers, underscores, and hyphens"
234
+ )
235
+ );
236
+ process.exit(1);
237
+ }
238
+ console.log(chalk.dim("Creating user account and agent..."));
239
+ const response = await fetch(`${apiUrl}/api/agents/signup`, {
240
+ method: "POST",
241
+ headers: {
242
+ "Content-Type": "application/json"
243
+ },
244
+ body: JSON.stringify({
245
+ userEmail: email,
246
+ userName,
247
+ userPassword: password,
248
+ agentUsername,
249
+ agentName,
250
+ agentEmoji
251
+ })
252
+ });
253
+ if (!response.ok) {
254
+ const errorData = await response.json().catch(() => ({}));
255
+ const errorMessage = errorData.error || `HTTP ${response.status}: ${response.statusText}`;
256
+ console.error(chalk.red(`Error: ${errorMessage}`));
257
+ process.exit(1);
258
+ }
259
+ const data = await response.json();
260
+ console.log(chalk.green("\n\u2713 Successfully created accounts!\n"));
261
+ console.log(chalk.bold("User Account:"));
262
+ console.log(chalk.dim(` Username: ${data.user.username}`));
263
+ console.log(chalk.dim(` Email: ${data.user.email}`));
264
+ console.log(
265
+ chalk.dim(
266
+ ` Status: ${data.user.humanVerified ? "Verified" : "Pending verification"}`
267
+ )
268
+ );
269
+ console.log(chalk.bold("\nAgent Account:"));
270
+ console.log(chalk.dim(` Username: @${data.agent.username}`));
271
+ console.log(chalk.dim(` Name: ${data.agent.name}`));
272
+ if (data.agent.emoji) {
273
+ console.log(chalk.dim(` Emoji: ${data.agent.emoji}`));
274
+ }
275
+ if (data.agent.apiKey) {
276
+ console.log(chalk.bold("\n\u{1F511} API Key (save this - it won't be shown again!):"));
277
+ console.log(chalk.yellow(` ${data.agent.apiKey}`));
278
+ await saveConfig({
279
+ apiUrl,
280
+ apiKey: data.agent.apiKey,
281
+ agentId: data.agent.id,
282
+ agentName: data.agent.username
283
+ });
284
+ console.log(chalk.green("\n\u2713 API key saved to config"));
285
+ console.log(
286
+ chalk.dim(" You can now use 'forge' commands with this agent")
287
+ );
288
+ } else {
289
+ console.log(
290
+ chalk.yellow(
291
+ "\nNote: Use 'forge auth login --token <key>' to authenticate with this agent"
292
+ )
293
+ );
294
+ }
295
+ } catch (error) {
296
+ console.error(chalk.red(`Error: ${error.message}`));
297
+ process.exit(1);
298
+ }
299
+ });
216
300
  return auth;
217
301
  }
218
302
 
@@ -323,6 +407,121 @@ function createReposCommand() {
323
407
  process.exit(1);
324
408
  }
325
409
  });
410
+ repos.command("fork").description("Fork a repository").argument("<repo>", "Repository to fork (@owner/name)").option("--name <name>", "Custom name for the fork").option("--org <identifier>", "Fork into organization (defaults to your personal org)").action(async (repoArg, options) => {
411
+ try {
412
+ const [ownerIdentifier, name] = parseRepoArg(repoArg);
413
+ const user = await apiGet("/api/users/me");
414
+ const targetOrgIdentifier = options.org || user.username;
415
+ const fork = await apiPost(
416
+ `/api/repositories/${ownerIdentifier}/${name}/fork`,
417
+ {
418
+ name: options.name || name,
419
+ orgIdentifier: targetOrgIdentifier
420
+ }
421
+ );
422
+ console.log(chalk2.green("\u2713 Repository forked successfully"));
423
+ console.log(chalk2.dim(` @${fork.ownerIdentifier}/${fork.name}`));
424
+ console.log(chalk2.dim(` Forked from @${ownerIdentifier}/${name}`));
425
+ } catch (error) {
426
+ console.error(chalk2.red(`Error: ${error.message}`));
427
+ process.exit(1);
428
+ }
429
+ });
430
+ repos.command("star").description("Star a repository").argument("<repo>", "Repository (@owner/name)").action(async (repoArg) => {
431
+ try {
432
+ const [ownerIdentifier, name] = parseRepoArg(repoArg);
433
+ await apiPost(
434
+ `/api/repositories/${ownerIdentifier}/${name}/star`,
435
+ {}
436
+ );
437
+ console.log(chalk2.green(`\u2713 Starred @${ownerIdentifier}/${name}`));
438
+ } catch (error) {
439
+ console.error(chalk2.red(`Error: ${error.message}`));
440
+ process.exit(1);
441
+ }
442
+ });
443
+ repos.command("unstar").description("Unstar a repository").argument("<repo>", "Repository (@owner/name)").action(async (repoArg) => {
444
+ try {
445
+ const [ownerIdentifier, name] = parseRepoArg(repoArg);
446
+ await apiPost(
447
+ `/api/repositories/${ownerIdentifier}/${name}/unstar`,
448
+ {}
449
+ );
450
+ console.log(chalk2.green(`\u2713 Unstarred @${ownerIdentifier}/${name}`));
451
+ } catch (error) {
452
+ console.error(chalk2.red(`Error: ${error.message}`));
453
+ process.exit(1);
454
+ }
455
+ });
456
+ repos.command("stars").description("List starred repositories").action(async () => {
457
+ try {
458
+ const user = await apiGet("/api/users/me");
459
+ const result = await apiGet(
460
+ `/api/users/${user.id}/starred`
461
+ );
462
+ const stars = result.stars || [];
463
+ if (stars.length === 0) {
464
+ console.log(chalk2.yellow("No starred repositories"));
465
+ return;
466
+ }
467
+ console.log(chalk2.bold(`Starred Repositories (${stars.length})`));
468
+ console.log();
469
+ for (const star of stars) {
470
+ const repo = star.repository;
471
+ const identifier = `@${repo.ownerIdentifier}/${repo.name}`;
472
+ const visibility = repo.visibility === "private" ? "\u{1F512}" : "\u{1F310}";
473
+ console.log(`${visibility} ${chalk2.cyan(identifier)}`);
474
+ if (repo.description) {
475
+ console.log(chalk2.dim(` ${repo.description}`));
476
+ }
477
+ }
478
+ } catch (error) {
479
+ console.error(chalk2.red(`Error: ${error.message}`));
480
+ process.exit(1);
481
+ }
482
+ });
483
+ repos.command("edit").description("Edit repository details").argument("<repo>", "Repository (@owner/name)").option("--name <name>", "New repository name").option("--description <desc>", "New description").option("--visibility <type>", "Visibility (public/private)").action(async (repoArg, options) => {
484
+ try {
485
+ const [ownerIdentifier, name] = parseRepoArg(repoArg);
486
+ const updates = {};
487
+ if (options.name) updates.name = options.name;
488
+ if (options.description !== void 0) updates.description = options.description;
489
+ if (options.visibility) updates.visibility = options.visibility;
490
+ if (Object.keys(updates).length === 0) {
491
+ console.log(chalk2.yellow("No updates specified. Use --name, --description, or --visibility"));
492
+ return;
493
+ }
494
+ await apiPost(
495
+ `/api/repositories/${ownerIdentifier}/${name}/settings`,
496
+ updates
497
+ );
498
+ console.log(chalk2.green("\u2713 Repository updated successfully"));
499
+ if (options.name) {
500
+ console.log(chalk2.dim(` Renamed to: ${options.name}`));
501
+ }
502
+ } catch (error) {
503
+ console.error(chalk2.red(`Error: ${error.message}`));
504
+ process.exit(1);
505
+ }
506
+ });
507
+ repos.command("delete").description("Delete a repository").argument("<repo>", "Repository (@owner/name)").option("--confirm", "Skip confirmation prompt").action(async (repoArg, options) => {
508
+ try {
509
+ const [ownerIdentifier, name] = parseRepoArg(repoArg);
510
+ if (!options.confirm) {
511
+ console.log(chalk2.yellow(`\u26A0\uFE0F WARNING: This will permanently delete @${ownerIdentifier}/${name}`));
512
+ console.log(chalk2.dim("Run with --confirm to proceed"));
513
+ process.exit(1);
514
+ }
515
+ await apiPost(
516
+ `/api/repositories/${ownerIdentifier}/${name}/delete`,
517
+ {}
518
+ );
519
+ console.log(chalk2.green(`\u2713 Repository @${ownerIdentifier}/${name} deleted`));
520
+ } catch (error) {
521
+ console.error(chalk2.red(`Error: ${error.message}`));
522
+ process.exit(1);
523
+ }
524
+ });
326
525
  return repos;
327
526
  }
328
527
  function parseRepoArg(arg) {
@@ -450,6 +649,143 @@ function createIssuesCommand() {
450
649
  process.exit(1);
451
650
  }
452
651
  });
652
+ issues.command("edit").description("Edit an issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").option("--title <title>", "New title").option("--body <body>", "New body/description").option("--state <state>", "State (open/closed)").action(async (options) => {
653
+ try {
654
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
655
+ const updates = {};
656
+ if (options.title) updates.title = options.title;
657
+ if (options.body !== void 0) updates.body = options.body;
658
+ if (options.state) updates.state = options.state;
659
+ if (Object.keys(updates).length === 0) {
660
+ console.log(chalk3.yellow("No updates specified. Use --title, --body, or --state"));
661
+ return;
662
+ }
663
+ await apiPatch(
664
+ `/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}`,
665
+ updates
666
+ );
667
+ console.log(chalk3.green("\u2713 Issue updated successfully"));
668
+ } catch (error) {
669
+ console.error(chalk3.red(`Error: ${error.message}`));
670
+ process.exit(1);
671
+ }
672
+ });
673
+ issues.command("reopen").description("Reopen a closed issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").action(async (options) => {
674
+ try {
675
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
676
+ await apiPatch(
677
+ `/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}`,
678
+ {
679
+ state: "open"
680
+ }
681
+ );
682
+ console.log(chalk3.green("\u2713 Issue reopened successfully"));
683
+ } catch (error) {
684
+ console.error(chalk3.red(`Error: ${error.message}`));
685
+ process.exit(1);
686
+ }
687
+ });
688
+ issues.command("comments").description("List comments on an issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").action(async (options) => {
689
+ try {
690
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
691
+ const result = await apiGet(
692
+ `/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}/comments`
693
+ );
694
+ const comments = result.comments || [];
695
+ if (comments.length === 0) {
696
+ console.log(chalk3.yellow("No comments found"));
697
+ return;
698
+ }
699
+ console.log(chalk3.bold(`Comments (${comments.length})`));
700
+ console.log();
701
+ for (const comment of comments) {
702
+ console.log(chalk3.cyan(`@${comment.author?.username || "unknown"}`));
703
+ console.log(chalk3.dim(` ${new Date(comment.createdAt).toLocaleString()}`));
704
+ console.log(` ${comment.body}`);
705
+ console.log();
706
+ }
707
+ } catch (error) {
708
+ console.error(chalk3.red(`Error: ${error.message}`));
709
+ process.exit(1);
710
+ }
711
+ });
712
+ issues.command("labels").description("List repository labels").requiredOption("--repo <repo>", "Repository (@owner/name)").action(async (options) => {
713
+ try {
714
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
715
+ const labels = await apiGet(
716
+ `/api/repositories/${ownerIdentifier}/${name}/labels`
717
+ );
718
+ if (labels.length === 0) {
719
+ console.log(chalk3.yellow("No labels found"));
720
+ return;
721
+ }
722
+ console.log(chalk3.bold(`Labels (${labels.length})`));
723
+ console.log();
724
+ for (const label of labels) {
725
+ console.log(`${chalk3.hex(label.color || "#000000")("\u25A0")} ${label.name}`);
726
+ if (label.description) {
727
+ console.log(chalk3.dim(` ${label.description}`));
728
+ }
729
+ }
730
+ } catch (error) {
731
+ console.error(chalk3.red(`Error: ${error.message}`));
732
+ process.exit(1);
733
+ }
734
+ });
735
+ issues.command("label-add").description("Add a label to an issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").requiredOption("--label <name>", "Label name").action(async (options) => {
736
+ try {
737
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
738
+ await apiPost(
739
+ `/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}/labels`,
740
+ {
741
+ labelName: options.label
742
+ }
743
+ );
744
+ console.log(chalk3.green(`\u2713 Label "${options.label}" added to issue #${options.number}`));
745
+ } catch (error) {
746
+ console.error(chalk3.red(`Error: ${error.message}`));
747
+ process.exit(1);
748
+ }
749
+ });
750
+ issues.command("label-remove").description("Remove a label from an issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").requiredOption("--label <name>", "Label name").action(async (options) => {
751
+ try {
752
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
753
+ await apiDelete(
754
+ `/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}/labels/${options.label}`
755
+ );
756
+ console.log(chalk3.green(`\u2713 Label "${options.label}" removed from issue #${options.number}`));
757
+ } catch (error) {
758
+ console.error(chalk3.red(`Error: ${error.message}`));
759
+ process.exit(1);
760
+ }
761
+ });
762
+ issues.command("assign").description("Assign a user to an issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").requiredOption("--user <username>", "Username to assign").action(async (options) => {
763
+ try {
764
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
765
+ await apiPost(
766
+ `/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}/assignees`,
767
+ {
768
+ username: options.user
769
+ }
770
+ );
771
+ console.log(chalk3.green(`\u2713 Assigned @${options.user} to issue #${options.number}`));
772
+ } catch (error) {
773
+ console.error(chalk3.red(`Error: ${error.message}`));
774
+ process.exit(1);
775
+ }
776
+ });
777
+ issues.command("unassign").description("Unassign a user from an issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").requiredOption("--user <username>", "Username to unassign").action(async (options) => {
778
+ try {
779
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
780
+ await apiDelete(
781
+ `/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}/assignees/${options.user}`
782
+ );
783
+ console.log(chalk3.green(`\u2713 Unassigned @${options.user} from issue #${options.number}`));
784
+ } catch (error) {
785
+ console.error(chalk3.red(`Error: ${error.message}`));
786
+ process.exit(1);
787
+ }
788
+ });
453
789
  return issues;
454
790
  }
455
791
  function parseRepoArg2(arg) {
@@ -599,6 +935,187 @@ function createPrsCommand() {
599
935
  process.exit(1);
600
936
  }
601
937
  });
938
+ prs.command("edit").description("Edit a pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").option("--title <title>", "New title").option("--body <body>", "New body/description").action(async (options) => {
939
+ try {
940
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
941
+ const updates = {};
942
+ if (options.title) updates.title = options.title;
943
+ if (options.body !== void 0) updates.body = options.body;
944
+ if (Object.keys(updates).length === 0) {
945
+ console.log(chalk4.yellow("No updates specified. Use --title or --body"));
946
+ return;
947
+ }
948
+ const pr = await apiGet(
949
+ `/api/repositories/${ownerIdentifier}/${name}/pulls/${options.number}`
950
+ );
951
+ await apiPatch(`/api/pulls/${pr.id}`, updates);
952
+ console.log(chalk4.green("\u2713 Pull request updated successfully"));
953
+ } catch (error) {
954
+ console.error(chalk4.red(`Error: ${error.message}`));
955
+ process.exit(1);
956
+ }
957
+ });
958
+ prs.command("reopen").description("Reopen a closed pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").action(async (options) => {
959
+ try {
960
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
961
+ const pr = await apiGet(
962
+ `/api/repositories/${ownerIdentifier}/${name}/pulls/${options.number}`
963
+ );
964
+ await apiPatch(`/api/pulls/${pr.id}`, {
965
+ state: "open"
966
+ });
967
+ console.log(chalk4.green("\u2713 Pull request reopened successfully"));
968
+ } catch (error) {
969
+ console.error(chalk4.red(`Error: ${error.message}`));
970
+ process.exit(1);
971
+ }
972
+ });
973
+ prs.command("draft").description("Mark pull request as draft").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").action(async (options) => {
974
+ try {
975
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
976
+ const pr = await apiGet(
977
+ `/api/repositories/${ownerIdentifier}/${name}/pulls/${options.number}`
978
+ );
979
+ await apiPatch(`/api/pulls/${pr.id}`, {
980
+ draft: true
981
+ });
982
+ console.log(chalk4.green("\u2713 Pull request marked as draft"));
983
+ } catch (error) {
984
+ console.error(chalk4.red(`Error: ${error.message}`));
985
+ process.exit(1);
986
+ }
987
+ });
988
+ prs.command("ready").description("Mark pull request as ready for review").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").action(async (options) => {
989
+ try {
990
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
991
+ const pr = await apiGet(
992
+ `/api/repositories/${ownerIdentifier}/${name}/pulls/${options.number}`
993
+ );
994
+ await apiPatch(`/api/pulls/${pr.id}`, {
995
+ draft: false
996
+ });
997
+ console.log(chalk4.green("\u2713 Pull request marked as ready for review"));
998
+ } catch (error) {
999
+ console.error(chalk4.red(`Error: ${error.message}`));
1000
+ process.exit(1);
1001
+ }
1002
+ });
1003
+ prs.command("review").description("Submit a review on a pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").requiredOption("--state <state>", "Review state (approve/request_changes/comment)").option("--body <body>", "Review comment").action(async (options) => {
1004
+ try {
1005
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
1006
+ const pr = await apiGet(
1007
+ `/api/repositories/${ownerIdentifier}/${name}/pulls/${options.number}`
1008
+ );
1009
+ const validStates = ["approve", "request_changes", "comment"];
1010
+ if (!validStates.includes(options.state)) {
1011
+ console.log(chalk4.red(`Invalid state. Must be one of: ${validStates.join(", ")}`));
1012
+ process.exit(1);
1013
+ }
1014
+ await apiPost(`/api/pulls/${pr.id}/reviews`, {
1015
+ state: options.state,
1016
+ body: options.body || ""
1017
+ });
1018
+ const stateLabels = {
1019
+ approve: "approved",
1020
+ request_changes: "requested changes",
1021
+ comment: "commented"
1022
+ };
1023
+ console.log(chalk4.green(`\u2713 Review submitted: ${stateLabels[options.state]}`));
1024
+ } catch (error) {
1025
+ console.error(chalk4.red(`Error: ${error.message}`));
1026
+ process.exit(1);
1027
+ }
1028
+ });
1029
+ prs.command("reviews").description("List reviews on a pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").action(async (options) => {
1030
+ try {
1031
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
1032
+ const pr = await apiGet(
1033
+ `/api/repositories/${ownerIdentifier}/${name}/pulls/${options.number}`
1034
+ );
1035
+ const reviews = await apiGet(`/api/pulls/${pr.id}/reviews`);
1036
+ if (reviews.length === 0) {
1037
+ console.log(chalk4.yellow("No reviews found"));
1038
+ return;
1039
+ }
1040
+ console.log(chalk4.bold(`Reviews (${reviews.length})`));
1041
+ console.log();
1042
+ for (const review of reviews) {
1043
+ const stateIcons = {
1044
+ approved: "\u2705",
1045
+ changes_requested: "\u{1F504}",
1046
+ commented: "\u{1F4AC}"
1047
+ };
1048
+ const icon = stateIcons[review.state] || "\u2022";
1049
+ console.log(
1050
+ `${icon} ${chalk4.cyan(`@${review.author?.username || "unknown"}`)} ${chalk4.dim(review.state)}`
1051
+ );
1052
+ console.log(chalk4.dim(` ${new Date(review.createdAt).toLocaleString()}`));
1053
+ if (review.body) {
1054
+ console.log(` ${review.body}`);
1055
+ }
1056
+ console.log();
1057
+ }
1058
+ } catch (error) {
1059
+ console.error(chalk4.red(`Error: ${error.message}`));
1060
+ process.exit(1);
1061
+ }
1062
+ });
1063
+ prs.command("commits").description("List commits in a pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").action(async (options) => {
1064
+ try {
1065
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
1066
+ const pr = await apiGet(
1067
+ `/api/repositories/${ownerIdentifier}/${name}/pulls/${options.number}`
1068
+ );
1069
+ const commits = await apiGet(`/api/pulls/${pr.id}/commits`);
1070
+ if (commits.length === 0) {
1071
+ console.log(chalk4.yellow("No commits found"));
1072
+ return;
1073
+ }
1074
+ console.log(chalk4.bold(`Commits (${commits.length})`));
1075
+ console.log();
1076
+ for (const commit of commits) {
1077
+ console.log(chalk4.cyan(commit.sha?.substring(0, 7) || "unknown"));
1078
+ console.log(` ${commit.message || "(no message)"}`);
1079
+ console.log(
1080
+ chalk4.dim(` by ${commit.author?.name || "unknown"} on ${new Date(commit.createdAt).toLocaleDateString()}`)
1081
+ );
1082
+ console.log();
1083
+ }
1084
+ } catch (error) {
1085
+ console.error(chalk4.red(`Error: ${error.message}`));
1086
+ process.exit(1);
1087
+ }
1088
+ });
1089
+ prs.command("diff").description("View pull request diff").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").action(async (options) => {
1090
+ try {
1091
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
1092
+ const pr = await apiGet(
1093
+ `/api/repositories/${ownerIdentifier}/${name}/pulls/${options.number}`
1094
+ );
1095
+ const diff = await apiGet(`/api/pulls/${pr.id}/diff`);
1096
+ if (!diff || !diff.files || diff.files.length === 0) {
1097
+ console.log(chalk4.yellow("No changes found"));
1098
+ return;
1099
+ }
1100
+ console.log(chalk4.bold(`Diff for PR #${options.number}`));
1101
+ console.log();
1102
+ for (const file of diff.files) {
1103
+ console.log(chalk4.cyan(`${file.path}`));
1104
+ console.log(
1105
+ chalk4.dim(` ${file.additions || 0} additions, ${file.deletions || 0} deletions`)
1106
+ );
1107
+ }
1108
+ console.log();
1109
+ console.log(
1110
+ chalk4.dim(
1111
+ `Total: ${diff.additions || 0} additions, ${diff.deletions || 0} deletions across ${diff.files.length} files`
1112
+ )
1113
+ );
1114
+ } catch (error) {
1115
+ console.error(chalk4.red(`Error: ${error.message}`));
1116
+ process.exit(1);
1117
+ }
1118
+ });
602
1119
  return prs;
603
1120
  }
604
1121
  function parseRepoArg3(arg) {
@@ -611,16 +1128,333 @@ function parseRepoArg3(arg) {
611
1128
  return [match[1], match[2]];
612
1129
  }
613
1130
 
1131
+ // src/commands/orgs.ts
1132
+ import { Command as Command5 } from "commander";
1133
+ import chalk5 from "chalk";
1134
+ function createOrgsCommand() {
1135
+ const orgs = new Command5("orgs").alias("org").description("Manage organizations");
1136
+ orgs.command("list").description("List organizations").option("--member", "Show only organizations you're a member of").action(async (options) => {
1137
+ try {
1138
+ let organizations;
1139
+ if (options.member) {
1140
+ const user = await apiGet("/api/users/me");
1141
+ const result = await apiGet(
1142
+ `/api/users/${user.id}/organizations`
1143
+ );
1144
+ organizations = result.organizations || [];
1145
+ } else {
1146
+ organizations = await apiGet("/api/organizations");
1147
+ }
1148
+ if (organizations.length === 0) {
1149
+ console.log(chalk5.yellow("No organizations found"));
1150
+ return;
1151
+ }
1152
+ console.log(chalk5.bold(`Organizations (${organizations.length})`));
1153
+ console.log();
1154
+ for (const org of organizations) {
1155
+ console.log(chalk5.cyan(`@${org.slug}`));
1156
+ console.log(chalk5.dim(` ${org.name}`));
1157
+ if (org.metadata?.description) {
1158
+ console.log(chalk5.dim(` ${org.metadata.description}`));
1159
+ }
1160
+ }
1161
+ } catch (error) {
1162
+ console.error(chalk5.red(`Error: ${error.message}`));
1163
+ process.exit(1);
1164
+ }
1165
+ });
1166
+ orgs.command("view").description("View organization details").argument("<slug>", "Organization slug").action(async (slug) => {
1167
+ try {
1168
+ const org = await apiGet(`/api/organizations/${slug}`);
1169
+ console.log(chalk5.bold(`@${org.slug}`));
1170
+ console.log(chalk5.dim(org.name));
1171
+ console.log();
1172
+ if (org.metadata?.description) {
1173
+ console.log(org.metadata.description);
1174
+ console.log();
1175
+ }
1176
+ console.log(chalk5.dim(`Created: ${new Date(org.createdAt).toLocaleDateString()}`));
1177
+ } catch (error) {
1178
+ console.error(chalk5.red(`Error: ${error.message}`));
1179
+ process.exit(1);
1180
+ }
1181
+ });
1182
+ orgs.command("create").description("Create a new organization").requiredOption("--name <name>", "Organization name").requiredOption("--slug <slug>", "Organization slug (URL identifier)").option("--description <desc>", "Organization description").action(async (options) => {
1183
+ try {
1184
+ const org = await apiPost("/api/organizations", {
1185
+ name: options.name,
1186
+ slug: options.slug,
1187
+ metadata: options.description ? { description: options.description } : void 0
1188
+ });
1189
+ console.log(chalk5.green("\u2713 Organization created successfully"));
1190
+ console.log(chalk5.dim(` @${org.slug}`));
1191
+ } catch (error) {
1192
+ console.error(chalk5.red(`Error: ${error.message}`));
1193
+ process.exit(1);
1194
+ }
1195
+ });
1196
+ orgs.command("members").description("List organization members").argument("<slug>", "Organization slug").action(async (slug) => {
1197
+ try {
1198
+ const members = await apiGet(`/api/organizations/${slug}/members`);
1199
+ if (members.length === 0) {
1200
+ console.log(chalk5.yellow("No members found"));
1201
+ return;
1202
+ }
1203
+ console.log(chalk5.bold(`Members (${members.length})`));
1204
+ console.log();
1205
+ for (const member of members) {
1206
+ const roleIcon = member.role === "owner" ? "\u{1F451}" : member.role === "admin" ? "\u26A1" : "\u2022";
1207
+ console.log(`${roleIcon} ${chalk5.cyan(`@${member.user?.username || "unknown"}`)} ${chalk5.dim(member.role)}`);
1208
+ }
1209
+ } catch (error) {
1210
+ console.error(chalk5.red(`Error: ${error.message}`));
1211
+ process.exit(1);
1212
+ }
1213
+ });
1214
+ orgs.command("member-add").description("Add a member to organization").argument("<slug>", "Organization slug").argument("<username>", "Username to add").option("--role <role>", "Member role (owner/admin/member)", "member").action(async (slug, username, options) => {
1215
+ try {
1216
+ await apiPost(`/api/organizations/${slug}/members`, {
1217
+ username,
1218
+ role: options.role
1219
+ });
1220
+ console.log(chalk5.green(`\u2713 Added @${username} to @${slug} as ${options.role}`));
1221
+ } catch (error) {
1222
+ console.error(chalk5.red(`Error: ${error.message}`));
1223
+ process.exit(1);
1224
+ }
1225
+ });
1226
+ orgs.command("member-remove").description("Remove a member from organization").argument("<slug>", "Organization slug").argument("<username>", "Username to remove").action(async (slug, username) => {
1227
+ try {
1228
+ await apiDelete(`/api/organizations/${slug}/members/${username}`);
1229
+ console.log(chalk5.green(`\u2713 Removed @${username} from @${slug}`));
1230
+ } catch (error) {
1231
+ console.error(chalk5.red(`Error: ${error.message}`));
1232
+ process.exit(1);
1233
+ }
1234
+ });
1235
+ orgs.command("teams").description("List organization teams").argument("<slug>", "Organization slug").action(async (slug) => {
1236
+ try {
1237
+ const teams = await apiGet(`/api/organizations/${slug}/teams`);
1238
+ if (teams.length === 0) {
1239
+ console.log(chalk5.yellow("No teams found"));
1240
+ return;
1241
+ }
1242
+ console.log(chalk5.bold(`Teams (${teams.length})`));
1243
+ console.log();
1244
+ for (const team of teams) {
1245
+ console.log(chalk5.cyan(team.name));
1246
+ if (team.description) {
1247
+ console.log(chalk5.dim(` ${team.description}`));
1248
+ }
1249
+ }
1250
+ } catch (error) {
1251
+ console.error(chalk5.red(`Error: ${error.message}`));
1252
+ process.exit(1);
1253
+ }
1254
+ });
1255
+ orgs.command("team-create").description("Create a team in organization").argument("<slug>", "Organization slug").requiredOption("--name <name>", "Team name").option("--description <desc>", "Team description").action(async (slug, options) => {
1256
+ try {
1257
+ const team = await apiPost(`/api/organizations/${slug}/teams`, {
1258
+ name: options.name,
1259
+ description: options.description
1260
+ });
1261
+ console.log(chalk5.green(`\u2713 Team "${team.name}" created in @${slug}`));
1262
+ } catch (error) {
1263
+ console.error(chalk5.red(`Error: ${error.message}`));
1264
+ process.exit(1);
1265
+ }
1266
+ });
1267
+ orgs.command("team-members").description("List team members").argument("<slug>", "Organization slug").argument("<team>", "Team name").action(async (slug, team) => {
1268
+ try {
1269
+ const members = await apiGet(
1270
+ `/api/organizations/${slug}/teams/${team}/members`
1271
+ );
1272
+ if (members.length === 0) {
1273
+ console.log(chalk5.yellow("No team members found"));
1274
+ return;
1275
+ }
1276
+ console.log(chalk5.bold(`Team Members (${members.length})`));
1277
+ console.log();
1278
+ for (const member of members) {
1279
+ console.log(chalk5.cyan(`@${member.user?.username || "unknown"}`));
1280
+ }
1281
+ } catch (error) {
1282
+ console.error(chalk5.red(`Error: ${error.message}`));
1283
+ process.exit(1);
1284
+ }
1285
+ });
1286
+ orgs.command("team-add-member").description("Add a member to team").argument("<slug>", "Organization slug").argument("<team>", "Team name").argument("<username>", "Username to add").action(async (slug, team, username) => {
1287
+ try {
1288
+ await apiPost(
1289
+ `/api/organizations/${slug}/teams/${team}/members`,
1290
+ {
1291
+ username
1292
+ }
1293
+ );
1294
+ console.log(chalk5.green(`\u2713 Added @${username} to team ${team}`));
1295
+ } catch (error) {
1296
+ console.error(chalk5.red(`Error: ${error.message}`));
1297
+ process.exit(1);
1298
+ }
1299
+ });
1300
+ orgs.command("team-remove-member").description("Remove a member from team").argument("<slug>", "Organization slug").argument("<team>", "Team name").argument("<username>", "Username to remove").action(async (slug, team, username) => {
1301
+ try {
1302
+ await apiDelete(
1303
+ `/api/organizations/${slug}/teams/${team}/members/${username}`
1304
+ );
1305
+ console.log(chalk5.green(`\u2713 Removed @${username} from team ${team}`));
1306
+ } catch (error) {
1307
+ console.error(chalk5.red(`Error: ${error.message}`));
1308
+ process.exit(1);
1309
+ }
1310
+ });
1311
+ return orgs;
1312
+ }
1313
+
1314
+ // src/commands/user.ts
1315
+ import { Command as Command6 } from "commander";
1316
+ import chalk6 from "chalk";
1317
+ function createUserCommand() {
1318
+ const user = new Command6("user").description("Manage user profile and settings");
1319
+ user.command("profile").description("View user profile").argument("[username]", "Username (defaults to current user)").action(async (username) => {
1320
+ try {
1321
+ let profile;
1322
+ if (username) {
1323
+ profile = await apiGet(`/api/users/${username}`);
1324
+ } else {
1325
+ profile = await apiGet("/api/users/me");
1326
+ }
1327
+ console.log(chalk6.bold(`@${profile.username}`));
1328
+ if (profile.name) {
1329
+ console.log(chalk6.dim(profile.name));
1330
+ }
1331
+ console.log();
1332
+ if (profile.bio) {
1333
+ console.log(profile.bio);
1334
+ console.log();
1335
+ }
1336
+ if (profile.location) {
1337
+ console.log(chalk6.dim(`\u{1F4CD} ${profile.location}`));
1338
+ }
1339
+ if (profile.website) {
1340
+ console.log(chalk6.dim(`\u{1F517} ${profile.website}`));
1341
+ }
1342
+ if (profile.company) {
1343
+ console.log(chalk6.dim(`\u{1F3E2} ${profile.company}`));
1344
+ }
1345
+ if (profile.pronouns) {
1346
+ console.log(chalk6.dim(`Pronouns: ${profile.pronouns}`));
1347
+ }
1348
+ if (profile.gitEmail) {
1349
+ console.log(chalk6.dim(`Git Email: ${profile.gitEmail}`));
1350
+ }
1351
+ console.log();
1352
+ console.log(chalk6.dim(`User type: ${profile.userType || "human"}`));
1353
+ console.log(chalk6.dim(`Joined: ${new Date(profile.createdAt).toLocaleDateString()}`));
1354
+ } catch (error) {
1355
+ console.error(chalk6.red(`Error: ${error.message}`));
1356
+ process.exit(1);
1357
+ }
1358
+ });
1359
+ user.command("edit").description("Edit your profile").option("--name <name>", "Display name").option("--bio <bio>", "Bio/about").option("--location <location>", "Location").option("--website <url>", "Website URL").option("--pronouns <pronouns>", "Pronouns").option("--company <company>", "Company name").option("--git-email <email>", "Git commit email").action(async (options) => {
1360
+ try {
1361
+ const updates = {};
1362
+ if (options.name !== void 0) updates.name = options.name;
1363
+ if (options.bio !== void 0) updates.bio = options.bio;
1364
+ if (options.location !== void 0) updates.location = options.location;
1365
+ if (options.website !== void 0) updates.website = options.website;
1366
+ if (options.pronouns !== void 0) updates.pronouns = options.pronouns;
1367
+ if (options.company !== void 0) updates.company = options.company;
1368
+ if (options.gitEmail !== void 0) updates.gitEmail = options.gitEmail;
1369
+ if (Object.keys(updates).length === 0) {
1370
+ console.log(
1371
+ chalk6.yellow(
1372
+ "No updates specified. Use --name, --bio, --location, --website, --pronouns, --company, or --git-email"
1373
+ )
1374
+ );
1375
+ return;
1376
+ }
1377
+ await apiPatch("/api/users/me", updates);
1378
+ console.log(chalk6.green("\u2713 Profile updated successfully"));
1379
+ } catch (error) {
1380
+ console.error(chalk6.red(`Error: ${error.message}`));
1381
+ process.exit(1);
1382
+ }
1383
+ });
1384
+ user.command("search").description("Search for users").argument("<query>", "Search query").option("--limit <number>", "Limit results", "10").action(async (query, options) => {
1385
+ try {
1386
+ const results = await apiGet(
1387
+ `/api/search?q=${encodeURIComponent(query)}&type=users&limit=${options.limit}`
1388
+ );
1389
+ const users = results.users || [];
1390
+ if (users.length === 0) {
1391
+ console.log(chalk6.yellow("No users found"));
1392
+ return;
1393
+ }
1394
+ console.log(chalk6.bold(`Users (${users.length})`));
1395
+ console.log();
1396
+ for (const u of users) {
1397
+ console.log(chalk6.cyan(`@${u.username}`));
1398
+ if (u.name) {
1399
+ console.log(chalk6.dim(` ${u.name}`));
1400
+ }
1401
+ if (u.bio) {
1402
+ const shortBio = u.bio.length > 60 ? u.bio.substring(0, 60) + "..." : u.bio;
1403
+ console.log(chalk6.dim(` ${shortBio}`));
1404
+ }
1405
+ console.log();
1406
+ }
1407
+ } catch (error) {
1408
+ console.error(chalk6.red(`Error: ${error.message}`));
1409
+ process.exit(1);
1410
+ }
1411
+ });
1412
+ user.command("repos").description("List user repositories").argument("[username]", "Username (defaults to current user)").action(async (username) => {
1413
+ try {
1414
+ let targetUsername;
1415
+ if (username) {
1416
+ targetUsername = username;
1417
+ } else {
1418
+ const me = await apiGet("/api/users/me");
1419
+ targetUsername = me.username;
1420
+ }
1421
+ const result = await apiGet(
1422
+ `/api/repositories/user/${targetUsername}`
1423
+ );
1424
+ const repositories = result.repos || [];
1425
+ if (repositories.length === 0) {
1426
+ console.log(chalk6.yellow(`No repositories found for @${targetUsername}`));
1427
+ return;
1428
+ }
1429
+ console.log(chalk6.bold(`@${targetUsername}'s Repositories (${repositories.length})`));
1430
+ console.log();
1431
+ for (const repo of repositories) {
1432
+ const visibility = repo.visibility === "private" ? "\u{1F512}" : "\u{1F310}";
1433
+ console.log(`${visibility} ${chalk6.cyan(repo.name)}`);
1434
+ if (repo.description) {
1435
+ console.log(chalk6.dim(` ${repo.description}`));
1436
+ }
1437
+ }
1438
+ } catch (error) {
1439
+ console.error(chalk6.red(`Error: ${error.message}`));
1440
+ process.exit(1);
1441
+ }
1442
+ });
1443
+ return user;
1444
+ }
1445
+
614
1446
  // src/index.ts
615
1447
  var __filename2 = fileURLToPath(import.meta.url);
616
1448
  var __dirname2 = dirname(__filename2);
617
1449
  var packageJson = JSON.parse(
618
1450
  readFileSync(join2(__dirname2, "../package.json"), "utf-8")
619
1451
  );
620
- var program = new Command5();
1452
+ var program = new Command7();
621
1453
  program.name("forge").description("CLI tool for Kernelius Forge - the agent-native Git platform").version(packageJson.version);
622
1454
  program.addCommand(createAuthCommand());
623
1455
  program.addCommand(createReposCommand());
624
1456
  program.addCommand(createIssuesCommand());
625
1457
  program.addCommand(createPrsCommand());
1458
+ program.addCommand(createOrgsCommand());
1459
+ program.addCommand(createUserCommand());
626
1460
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernelius/forge-cli",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Command-line tool for Kernelius Forge - the agent-native Git platform",
5
5
  "type": "module",
6
6
  "bin": {