@kernelius/forge-cli 0.1.4 → 0.2.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/CHANGELOG.md CHANGED
@@ -5,6 +5,80 @@ 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.1] - 2026-02-01
9
+
10
+ ### Added
11
+ - **Organization Type Support**: Add `--type` option to `forge orgs create` command
12
+ - Store organization type in metadata (e.g., `company`, `team`, `open-source`, `personal`)
13
+ - Display org type in `forge orgs list` and `forge orgs view` commands
14
+ - Flexible string value - use any type that fits your use case
15
+
16
+ ### Changed
17
+ - Update organization endpoints to use new API routes (`/api/orgs`, `/api/user/orgs`)
18
+ - Improved org list command to show metadata (type, description)
19
+
20
+ ### Examples
21
+ ```bash
22
+ forge orgs create --name "Acme Corp" --slug "acme" --type "company"
23
+ forge orgs create --name "React Hooks" --slug "react-hooks" --type "open-source"
24
+ forge orgs create --name "Dev Team" --slug "dev-team" --type "team"
25
+ ```
26
+
27
+ ## [0.2.0] - 2026-02-01
28
+
29
+ ### Added
30
+ **Major Feature Expansion: ~60+ new commands for UI parity**
31
+
32
+ #### Repository Commands
33
+ - `repos fork` - Fork repositories
34
+ - `repos star` / `repos unstar` - Star/unstar repositories
35
+ - `repos stars` - List starred repositories
36
+ - `repos edit` - Edit repository details (name, description, visibility)
37
+ - `repos delete` - Delete repositories
38
+
39
+ #### Issue Management
40
+ - `issues edit` - Edit issue title, body, or state
41
+ - `issues reopen` - Reopen closed issues
42
+ - `issues comments` - List issue comments
43
+ - `issues labels` - List repository labels
44
+ - `issues label-add` / `issues label-remove` - Manage issue labels
45
+ - `issues assign` / `issues unassign` - Manage issue assignees
46
+
47
+ #### Pull Request Management
48
+ - `prs edit` - Edit PR title or body
49
+ - `prs reopen` - Reopen closed PRs
50
+ - `prs draft` / `prs ready` - Manage draft status
51
+ - `prs review` - Submit reviews (approve/request changes/comment)
52
+ - `prs reviews` - List PR reviews
53
+ - `prs commits` - List commits in PR
54
+ - `prs diff` - View PR diff statistics
55
+
56
+ #### Organization Management (New)
57
+ - `orgs list` - List organizations
58
+ - `orgs view` - View organization details
59
+ - `orgs create` - Create new organization
60
+ - `orgs members` - List organization members
61
+ - `orgs member-add` / `orgs member-remove` - Manage members
62
+ - `orgs teams` - List organization teams
63
+ - `orgs team-create` - Create teams
64
+ - `orgs team-members` - List team members
65
+ - `orgs team-add-member` / `orgs team-remove-member` - Manage team membership
66
+
67
+ #### User Management (New)
68
+ - `user profile` - View user profiles (self or others)
69
+ - `user edit` - Edit profile (name, bio, location, website, pronouns, company, git-email)
70
+ - `user search` - Search for users
71
+ - `user repos` - List user repositories
72
+
73
+ ### Changed
74
+ - Significantly improved CLI coverage from ~13% to ~40% of UI capabilities
75
+ - All commands now support proper error handling and formatted output
76
+ - Added color-coded output with emojis for better UX
77
+
78
+ ### Documentation
79
+ - Created comprehensive gap analysis document
80
+ - Identified all UI features for complete parity roadmap
81
+
8
82
  ## [0.1.4] - 2026-02-01
9
83
 
10
84
  ### Added
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() {
@@ -404,6 +407,121 @@ function createReposCommand() {
404
407
  process.exit(1);
405
408
  }
406
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
+ });
407
525
  return repos;
408
526
  }
409
527
  function parseRepoArg(arg) {
@@ -531,6 +649,143 @@ function createIssuesCommand() {
531
649
  process.exit(1);
532
650
  }
533
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
+ });
534
789
  return issues;
535
790
  }
536
791
  function parseRepoArg2(arg) {
@@ -680,6 +935,187 @@ function createPrsCommand() {
680
935
  process.exit(1);
681
936
  }
682
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
+ });
683
1119
  return prs;
684
1120
  }
685
1121
  function parseRepoArg3(arg) {
@@ -692,16 +1128,347 @@ function parseRepoArg3(arg) {
692
1128
  return [match[1], match[2]];
693
1129
  }
694
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 result = await apiGet("/api/user/orgs");
1141
+ organizations = result.organizations || [];
1142
+ } else {
1143
+ const result = await apiGet("/api/orgs/public");
1144
+ organizations = result.organizations || [];
1145
+ }
1146
+ if (organizations.length === 0) {
1147
+ console.log(chalk5.yellow("No organizations found"));
1148
+ return;
1149
+ }
1150
+ console.log(chalk5.bold(`Organizations (${organizations.length})`));
1151
+ console.log();
1152
+ for (const org of organizations) {
1153
+ console.log(chalk5.cyan(`@${org.slug}`));
1154
+ console.log(chalk5.dim(` ${org.name}`));
1155
+ if (org.metadata?.type) {
1156
+ console.log(chalk5.dim(` Type: ${org.metadata.type}`));
1157
+ }
1158
+ if (org.metadata?.description) {
1159
+ console.log(chalk5.dim(` ${org.metadata.description}`));
1160
+ }
1161
+ }
1162
+ } catch (error) {
1163
+ console.error(chalk5.red(`Error: ${error.message}`));
1164
+ process.exit(1);
1165
+ }
1166
+ });
1167
+ orgs.command("view").description("View organization details").argument("<slug>", "Organization slug").action(async (slug) => {
1168
+ try {
1169
+ const org = await apiGet(`/api/orgs/${slug}`);
1170
+ console.log(chalk5.bold(`@${org.slug}`));
1171
+ console.log(chalk5.dim(org.name));
1172
+ if (org.metadata?.type) {
1173
+ console.log(chalk5.dim(`Type: ${org.metadata.type}`));
1174
+ }
1175
+ console.log();
1176
+ if (org.metadata?.description) {
1177
+ console.log(org.metadata.description);
1178
+ console.log();
1179
+ }
1180
+ console.log(chalk5.dim(`Created: ${new Date(org.createdAt).toLocaleDateString()}`));
1181
+ } catch (error) {
1182
+ console.error(chalk5.red(`Error: ${error.message}`));
1183
+ process.exit(1);
1184
+ }
1185
+ });
1186
+ 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").option("--type <type>", "Organization type (e.g., personal, team, company, open-source)").action(async (options) => {
1187
+ try {
1188
+ const metadata = {};
1189
+ if (options.description) {
1190
+ metadata.description = options.description;
1191
+ }
1192
+ if (options.type) {
1193
+ metadata.type = options.type;
1194
+ }
1195
+ const org = await apiPost("/api/orgs", {
1196
+ name: options.name,
1197
+ slug: options.slug,
1198
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0
1199
+ });
1200
+ console.log(chalk5.green("\u2713 Organization created successfully"));
1201
+ console.log(chalk5.dim(` @${org.slug}`));
1202
+ if (options.type) {
1203
+ console.log(chalk5.dim(` Type: ${options.type}`));
1204
+ }
1205
+ } catch (error) {
1206
+ console.error(chalk5.red(`Error: ${error.message}`));
1207
+ process.exit(1);
1208
+ }
1209
+ });
1210
+ orgs.command("members").description("List organization members").argument("<slug>", "Organization slug").action(async (slug) => {
1211
+ try {
1212
+ const members = await apiGet(`/api/organizations/${slug}/members`);
1213
+ if (members.length === 0) {
1214
+ console.log(chalk5.yellow("No members found"));
1215
+ return;
1216
+ }
1217
+ console.log(chalk5.bold(`Members (${members.length})`));
1218
+ console.log();
1219
+ for (const member of members) {
1220
+ const roleIcon = member.role === "owner" ? "\u{1F451}" : member.role === "admin" ? "\u26A1" : "\u2022";
1221
+ console.log(`${roleIcon} ${chalk5.cyan(`@${member.user?.username || "unknown"}`)} ${chalk5.dim(member.role)}`);
1222
+ }
1223
+ } catch (error) {
1224
+ console.error(chalk5.red(`Error: ${error.message}`));
1225
+ process.exit(1);
1226
+ }
1227
+ });
1228
+ 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) => {
1229
+ try {
1230
+ await apiPost(`/api/organizations/${slug}/members`, {
1231
+ username,
1232
+ role: options.role
1233
+ });
1234
+ console.log(chalk5.green(`\u2713 Added @${username} to @${slug} as ${options.role}`));
1235
+ } catch (error) {
1236
+ console.error(chalk5.red(`Error: ${error.message}`));
1237
+ process.exit(1);
1238
+ }
1239
+ });
1240
+ orgs.command("member-remove").description("Remove a member from organization").argument("<slug>", "Organization slug").argument("<username>", "Username to remove").action(async (slug, username) => {
1241
+ try {
1242
+ await apiDelete(`/api/organizations/${slug}/members/${username}`);
1243
+ console.log(chalk5.green(`\u2713 Removed @${username} from @${slug}`));
1244
+ } catch (error) {
1245
+ console.error(chalk5.red(`Error: ${error.message}`));
1246
+ process.exit(1);
1247
+ }
1248
+ });
1249
+ orgs.command("teams").description("List organization teams").argument("<slug>", "Organization slug").action(async (slug) => {
1250
+ try {
1251
+ const teams = await apiGet(`/api/organizations/${slug}/teams`);
1252
+ if (teams.length === 0) {
1253
+ console.log(chalk5.yellow("No teams found"));
1254
+ return;
1255
+ }
1256
+ console.log(chalk5.bold(`Teams (${teams.length})`));
1257
+ console.log();
1258
+ for (const team of teams) {
1259
+ console.log(chalk5.cyan(team.name));
1260
+ if (team.description) {
1261
+ console.log(chalk5.dim(` ${team.description}`));
1262
+ }
1263
+ }
1264
+ } catch (error) {
1265
+ console.error(chalk5.red(`Error: ${error.message}`));
1266
+ process.exit(1);
1267
+ }
1268
+ });
1269
+ 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) => {
1270
+ try {
1271
+ const team = await apiPost(`/api/organizations/${slug}/teams`, {
1272
+ name: options.name,
1273
+ description: options.description
1274
+ });
1275
+ console.log(chalk5.green(`\u2713 Team "${team.name}" created in @${slug}`));
1276
+ } catch (error) {
1277
+ console.error(chalk5.red(`Error: ${error.message}`));
1278
+ process.exit(1);
1279
+ }
1280
+ });
1281
+ orgs.command("team-members").description("List team members").argument("<slug>", "Organization slug").argument("<team>", "Team name").action(async (slug, team) => {
1282
+ try {
1283
+ const members = await apiGet(
1284
+ `/api/organizations/${slug}/teams/${team}/members`
1285
+ );
1286
+ if (members.length === 0) {
1287
+ console.log(chalk5.yellow("No team members found"));
1288
+ return;
1289
+ }
1290
+ console.log(chalk5.bold(`Team Members (${members.length})`));
1291
+ console.log();
1292
+ for (const member of members) {
1293
+ console.log(chalk5.cyan(`@${member.user?.username || "unknown"}`));
1294
+ }
1295
+ } catch (error) {
1296
+ console.error(chalk5.red(`Error: ${error.message}`));
1297
+ process.exit(1);
1298
+ }
1299
+ });
1300
+ 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) => {
1301
+ try {
1302
+ await apiPost(
1303
+ `/api/organizations/${slug}/teams/${team}/members`,
1304
+ {
1305
+ username
1306
+ }
1307
+ );
1308
+ console.log(chalk5.green(`\u2713 Added @${username} to team ${team}`));
1309
+ } catch (error) {
1310
+ console.error(chalk5.red(`Error: ${error.message}`));
1311
+ process.exit(1);
1312
+ }
1313
+ });
1314
+ 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) => {
1315
+ try {
1316
+ await apiDelete(
1317
+ `/api/organizations/${slug}/teams/${team}/members/${username}`
1318
+ );
1319
+ console.log(chalk5.green(`\u2713 Removed @${username} from team ${team}`));
1320
+ } catch (error) {
1321
+ console.error(chalk5.red(`Error: ${error.message}`));
1322
+ process.exit(1);
1323
+ }
1324
+ });
1325
+ return orgs;
1326
+ }
1327
+
1328
+ // src/commands/user.ts
1329
+ import { Command as Command6 } from "commander";
1330
+ import chalk6 from "chalk";
1331
+ function createUserCommand() {
1332
+ const user = new Command6("user").description("Manage user profile and settings");
1333
+ user.command("profile").description("View user profile").argument("[username]", "Username (defaults to current user)").action(async (username) => {
1334
+ try {
1335
+ let profile;
1336
+ if (username) {
1337
+ profile = await apiGet(`/api/users/${username}`);
1338
+ } else {
1339
+ profile = await apiGet("/api/users/me");
1340
+ }
1341
+ console.log(chalk6.bold(`@${profile.username}`));
1342
+ if (profile.name) {
1343
+ console.log(chalk6.dim(profile.name));
1344
+ }
1345
+ console.log();
1346
+ if (profile.bio) {
1347
+ console.log(profile.bio);
1348
+ console.log();
1349
+ }
1350
+ if (profile.location) {
1351
+ console.log(chalk6.dim(`\u{1F4CD} ${profile.location}`));
1352
+ }
1353
+ if (profile.website) {
1354
+ console.log(chalk6.dim(`\u{1F517} ${profile.website}`));
1355
+ }
1356
+ if (profile.company) {
1357
+ console.log(chalk6.dim(`\u{1F3E2} ${profile.company}`));
1358
+ }
1359
+ if (profile.pronouns) {
1360
+ console.log(chalk6.dim(`Pronouns: ${profile.pronouns}`));
1361
+ }
1362
+ if (profile.gitEmail) {
1363
+ console.log(chalk6.dim(`Git Email: ${profile.gitEmail}`));
1364
+ }
1365
+ console.log();
1366
+ console.log(chalk6.dim(`User type: ${profile.userType || "human"}`));
1367
+ console.log(chalk6.dim(`Joined: ${new Date(profile.createdAt).toLocaleDateString()}`));
1368
+ } catch (error) {
1369
+ console.error(chalk6.red(`Error: ${error.message}`));
1370
+ process.exit(1);
1371
+ }
1372
+ });
1373
+ 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) => {
1374
+ try {
1375
+ const updates = {};
1376
+ if (options.name !== void 0) updates.name = options.name;
1377
+ if (options.bio !== void 0) updates.bio = options.bio;
1378
+ if (options.location !== void 0) updates.location = options.location;
1379
+ if (options.website !== void 0) updates.website = options.website;
1380
+ if (options.pronouns !== void 0) updates.pronouns = options.pronouns;
1381
+ if (options.company !== void 0) updates.company = options.company;
1382
+ if (options.gitEmail !== void 0) updates.gitEmail = options.gitEmail;
1383
+ if (Object.keys(updates).length === 0) {
1384
+ console.log(
1385
+ chalk6.yellow(
1386
+ "No updates specified. Use --name, --bio, --location, --website, --pronouns, --company, or --git-email"
1387
+ )
1388
+ );
1389
+ return;
1390
+ }
1391
+ await apiPatch("/api/users/me", updates);
1392
+ console.log(chalk6.green("\u2713 Profile updated successfully"));
1393
+ } catch (error) {
1394
+ console.error(chalk6.red(`Error: ${error.message}`));
1395
+ process.exit(1);
1396
+ }
1397
+ });
1398
+ user.command("search").description("Search for users").argument("<query>", "Search query").option("--limit <number>", "Limit results", "10").action(async (query, options) => {
1399
+ try {
1400
+ const results = await apiGet(
1401
+ `/api/search?q=${encodeURIComponent(query)}&type=users&limit=${options.limit}`
1402
+ );
1403
+ const users = results.users || [];
1404
+ if (users.length === 0) {
1405
+ console.log(chalk6.yellow("No users found"));
1406
+ return;
1407
+ }
1408
+ console.log(chalk6.bold(`Users (${users.length})`));
1409
+ console.log();
1410
+ for (const u of users) {
1411
+ console.log(chalk6.cyan(`@${u.username}`));
1412
+ if (u.name) {
1413
+ console.log(chalk6.dim(` ${u.name}`));
1414
+ }
1415
+ if (u.bio) {
1416
+ const shortBio = u.bio.length > 60 ? u.bio.substring(0, 60) + "..." : u.bio;
1417
+ console.log(chalk6.dim(` ${shortBio}`));
1418
+ }
1419
+ console.log();
1420
+ }
1421
+ } catch (error) {
1422
+ console.error(chalk6.red(`Error: ${error.message}`));
1423
+ process.exit(1);
1424
+ }
1425
+ });
1426
+ user.command("repos").description("List user repositories").argument("[username]", "Username (defaults to current user)").action(async (username) => {
1427
+ try {
1428
+ let targetUsername;
1429
+ if (username) {
1430
+ targetUsername = username;
1431
+ } else {
1432
+ const me = await apiGet("/api/users/me");
1433
+ targetUsername = me.username;
1434
+ }
1435
+ const result = await apiGet(
1436
+ `/api/repositories/user/${targetUsername}`
1437
+ );
1438
+ const repositories = result.repos || [];
1439
+ if (repositories.length === 0) {
1440
+ console.log(chalk6.yellow(`No repositories found for @${targetUsername}`));
1441
+ return;
1442
+ }
1443
+ console.log(chalk6.bold(`@${targetUsername}'s Repositories (${repositories.length})`));
1444
+ console.log();
1445
+ for (const repo of repositories) {
1446
+ const visibility = repo.visibility === "private" ? "\u{1F512}" : "\u{1F310}";
1447
+ console.log(`${visibility} ${chalk6.cyan(repo.name)}`);
1448
+ if (repo.description) {
1449
+ console.log(chalk6.dim(` ${repo.description}`));
1450
+ }
1451
+ }
1452
+ } catch (error) {
1453
+ console.error(chalk6.red(`Error: ${error.message}`));
1454
+ process.exit(1);
1455
+ }
1456
+ });
1457
+ return user;
1458
+ }
1459
+
695
1460
  // src/index.ts
696
1461
  var __filename2 = fileURLToPath(import.meta.url);
697
1462
  var __dirname2 = dirname(__filename2);
698
1463
  var packageJson = JSON.parse(
699
1464
  readFileSync(join2(__dirname2, "../package.json"), "utf-8")
700
1465
  );
701
- var program = new Command5();
1466
+ var program = new Command7();
702
1467
  program.name("forge").description("CLI tool for Kernelius Forge - the agent-native Git platform").version(packageJson.version);
703
1468
  program.addCommand(createAuthCommand());
704
1469
  program.addCommand(createReposCommand());
705
1470
  program.addCommand(createIssuesCommand());
706
1471
  program.addCommand(createPrsCommand());
1472
+ program.addCommand(createOrgsCommand());
1473
+ program.addCommand(createUserCommand());
707
1474
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernelius/forge-cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Command-line tool for Kernelius Forge - the agent-native Git platform",
5
5
  "type": "module",
6
6
  "bin": {