@braingrid/cli 0.2.27 → 0.2.28

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
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.28] - 2025-01-28
11
+
12
+ ### Added
13
+
14
+ - **Tag management CLI commands for requirements**
15
+ - `requirement tag list [id]` - List all tags for a requirement
16
+ - `requirement tag add [id] --name <name> --color <hex>` - Add a tag with name and hex color
17
+ - `requirement tag remove [id] --name <name>` - Remove a tag by name
18
+ - All commands support auto-detection of requirement ID from git branch
19
+ - Multiple output formats supported (table, json, xml, markdown)
20
+ - Hex color validation for #RGB and #RRGGBB formats
21
+
10
22
  ## [0.2.27] - 2025-01-27
11
23
 
12
24
  ### Added
package/dist/cli.js CHANGED
@@ -25,7 +25,7 @@ import axios from "axios";
25
25
 
26
26
  // src/build-config.ts
27
27
  var BUILD_ENV = true ? "production" : process.env.NODE_ENV === "test" ? "development" : "production";
28
- var CLI_VERSION = true ? "0.2.27" : "0.0.0-test";
28
+ var CLI_VERSION = true ? "0.2.28" : "0.0.0-test";
29
29
  var PRODUCTION_CONFIG = {
30
30
  apiUrl: "https://app.braingrid.ai",
31
31
  workosAuthUrl: "https://auth.braingrid.ai",
@@ -4004,6 +4004,24 @@ var RequirementService = class {
4004
4004
  });
4005
4005
  });
4006
4006
  }
4007
+ // Tag management methods
4008
+ async listRequirementTags(projectId, requirementId) {
4009
+ const url = `${this.baseUrl}/api/v1/projects/${projectId}/requirements/${requirementId}/tags`;
4010
+ const headers = this.getHeaders();
4011
+ const response = await this.axios.get(url, { headers });
4012
+ return response.data;
4013
+ }
4014
+ async addRequirementTag(projectId, requirementId, tag) {
4015
+ const url = `${this.baseUrl}/api/v1/projects/${projectId}/requirements/${requirementId}/tags`;
4016
+ const headers = this.getHeaders();
4017
+ const response = await this.axios.post(url, tag, { headers });
4018
+ return response.data;
4019
+ }
4020
+ async removeRequirementTag(projectId, requirementId, tagName) {
4021
+ const url = `${this.baseUrl}/api/v1/projects/${projectId}/requirements/${requirementId}/tags/${encodeURIComponent(tagName)}`;
4022
+ const headers = this.getHeaders();
4023
+ await this.axios.delete(url, { headers });
4024
+ }
4007
4025
  };
4008
4026
 
4009
4027
  // src/utils/tag-validation.ts
@@ -4022,6 +4040,21 @@ function validateTags(tagsString) {
4022
4040
  return { valid: true, tags };
4023
4041
  }
4024
4042
 
4043
+ // src/utils/color-validation.ts
4044
+ function validateHexColor(color) {
4045
+ if (!color || color.trim().length === 0) {
4046
+ return { valid: false, error: "Color is required" };
4047
+ }
4048
+ const hexPattern = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
4049
+ if (!hexPattern.test(color)) {
4050
+ return {
4051
+ valid: false,
4052
+ error: "Invalid hex color format. Use #RGB or #RRGGBB (e.g., #FF0000 or #F00)"
4053
+ };
4054
+ }
4055
+ return { valid: true };
4056
+ }
4057
+
4025
4058
  // src/handlers/requirement.handlers.ts
4026
4059
  function getServices2() {
4027
4060
  const config = getConfig();
@@ -4864,6 +4897,286 @@ async function handleReviewAcceptance(opts) {
4864
4897
  };
4865
4898
  }
4866
4899
  }
4900
+ async function handleRequirementTagList(opts) {
4901
+ let stopSpinner = null;
4902
+ try {
4903
+ const { requirementService, auth } = getServices2();
4904
+ const isAuthenticated = await auth.isAuthenticated();
4905
+ if (!isAuthenticated) {
4906
+ return {
4907
+ success: false,
4908
+ message: chalk7.red("\u274C Not authenticated. Please run `braingrid login` first.")
4909
+ };
4910
+ }
4911
+ const format = opts.format || "table";
4912
+ if (!["table", "json", "xml", "markdown"].includes(format)) {
4913
+ return {
4914
+ success: false,
4915
+ message: chalk7.red(
4916
+ `\u274C Invalid format: ${format}. Supported formats: table, json, xml, markdown`
4917
+ )
4918
+ };
4919
+ }
4920
+ const requirementResult = await workspaceManager.getRequirement(opts.requirementId);
4921
+ if (!requirementResult.success) {
4922
+ return {
4923
+ success: false,
4924
+ message: requirementResult.error
4925
+ };
4926
+ }
4927
+ const requirementId = requirementResult.requirementId;
4928
+ const workspace = await workspaceManager.getProject(opts.project);
4929
+ if (!workspace.success) {
4930
+ return {
4931
+ success: false,
4932
+ message: workspace.error
4933
+ };
4934
+ }
4935
+ const projectId = workspace.projectId;
4936
+ const normalizedId = normalizeRequirementId(requirementId);
4937
+ stopSpinner = showSpinner("Loading tags", chalk7.gray);
4938
+ const tags = await requirementService.listRequirementTags(projectId, normalizedId);
4939
+ stopSpinner();
4940
+ stopSpinner = null;
4941
+ let output;
4942
+ switch (format) {
4943
+ case "json": {
4944
+ output = JSON.stringify(tags, null, 2);
4945
+ break;
4946
+ }
4947
+ case "xml": {
4948
+ output = '<?xml version="1.0" encoding="UTF-8"?>\n<tags>\n';
4949
+ for (const tag of tags) {
4950
+ output += ` <tag>
4951
+ `;
4952
+ output += ` <name>${tag.name}</name>
4953
+ `;
4954
+ output += ` <color>${tag.color || ""}</color>
4955
+ `;
4956
+ output += ` <id>${tag.id}</id>
4957
+ `;
4958
+ output += ` </tag>
4959
+ `;
4960
+ }
4961
+ output += "</tags>";
4962
+ break;
4963
+ }
4964
+ case "markdown": {
4965
+ output = `# Tags for ${normalizedId}
4966
+
4967
+ `;
4968
+ if (tags.length === 0) {
4969
+ output += "_No tags found_\n";
4970
+ } else {
4971
+ output += "| Name | Color |\n";
4972
+ output += "|------|-------|\n";
4973
+ for (const tag of tags) {
4974
+ output += `| ${tag.name} | ${tag.color || "N/A"} |
4975
+ `;
4976
+ }
4977
+ }
4978
+ break;
4979
+ }
4980
+ case "table":
4981
+ default: {
4982
+ if (tags.length === 0) {
4983
+ return {
4984
+ success: true,
4985
+ message: chalk7.yellow(`No tags found for ${normalizedId}.`),
4986
+ data: tags
4987
+ };
4988
+ }
4989
+ output = chalk7.bold(`\u{1F3F7}\uFE0F Tags for ${normalizedId}
4990
+
4991
+ `);
4992
+ output += "Name Color\n";
4993
+ output += "\u2500".repeat(40) + "\n";
4994
+ for (const tag of tags) {
4995
+ const name = tag.name.padEnd(19);
4996
+ const color = tag.color || "N/A";
4997
+ output += `${name} ${color}
4998
+ `;
4999
+ }
5000
+ break;
5001
+ }
5002
+ }
5003
+ return {
5004
+ success: true,
5005
+ message: output,
5006
+ data: tags
5007
+ };
5008
+ } catch (error) {
5009
+ if (stopSpinner) {
5010
+ stopSpinner();
5011
+ }
5012
+ return {
5013
+ success: false,
5014
+ message: formatError(
5015
+ error,
5016
+ getResourceContext("requirement", opts.requirementId || "unknown")
5017
+ )
5018
+ };
5019
+ }
5020
+ }
5021
+ async function handleRequirementTagAdd(opts) {
5022
+ let stopSpinner = null;
5023
+ try {
5024
+ const { requirementService, auth } = getServices2();
5025
+ const isAuthenticated = await auth.isAuthenticated();
5026
+ if (!isAuthenticated) {
5027
+ return {
5028
+ success: false,
5029
+ message: chalk7.red("\u274C Not authenticated. Please run `braingrid login` first.")
5030
+ };
5031
+ }
5032
+ if (!opts.name || opts.name.trim().length === 0) {
5033
+ return {
5034
+ success: false,
5035
+ message: chalk7.red("\u274C Tag name is required")
5036
+ };
5037
+ }
5038
+ const colorResult = validateHexColor(opts.color);
5039
+ if (!colorResult.valid) {
5040
+ return {
5041
+ success: false,
5042
+ message: chalk7.red(`\u274C ${colorResult.error}`)
5043
+ };
5044
+ }
5045
+ const requirementResult = await workspaceManager.getRequirement(opts.requirementId);
5046
+ if (!requirementResult.success) {
5047
+ return {
5048
+ success: false,
5049
+ message: requirementResult.error
5050
+ };
5051
+ }
5052
+ const requirementId = requirementResult.requirementId;
5053
+ const workspace = await workspaceManager.getProject(opts.project);
5054
+ if (!workspace.success) {
5055
+ return {
5056
+ success: false,
5057
+ message: workspace.error
5058
+ };
5059
+ }
5060
+ const projectId = workspace.projectId;
5061
+ const normalizedId = normalizeRequirementId(requirementId);
5062
+ stopSpinner = showSpinner("Adding tag", chalk7.gray);
5063
+ const response = await requirementService.addRequirementTag(projectId, normalizedId, {
5064
+ name: opts.name.trim(),
5065
+ color: opts.color
5066
+ });
5067
+ stopSpinner();
5068
+ stopSpinner = null;
5069
+ return {
5070
+ success: true,
5071
+ message: chalk7.green(
5072
+ `\u2705 Added tag "${response.tag.name}" (${response.tag.color}) to ${normalizedId}`
5073
+ ),
5074
+ data: response
5075
+ };
5076
+ } catch (error) {
5077
+ if (stopSpinner) {
5078
+ stopSpinner();
5079
+ }
5080
+ if (error && typeof error === "object" && "response" in error) {
5081
+ const axiosError = error;
5082
+ if (axiosError.response?.status === 400) {
5083
+ const errorData = axiosError.response.data;
5084
+ if (errorData?.code === "MAX_TAGS_EXCEEDED") {
5085
+ return {
5086
+ success: false,
5087
+ message: chalk7.red("\u274C Maximum 5 tags allowed per requirement")
5088
+ };
5089
+ }
5090
+ if (errorData?.code === "TAG_ALREADY_ASSOCIATED") {
5091
+ return {
5092
+ success: false,
5093
+ message: chalk7.red(`\u274C Tag "${opts.name}" is already associated with this requirement`)
5094
+ };
5095
+ }
5096
+ }
5097
+ }
5098
+ return {
5099
+ success: false,
5100
+ message: formatError(
5101
+ error,
5102
+ getResourceContext("requirement", opts.requirementId || "unknown")
5103
+ )
5104
+ };
5105
+ }
5106
+ }
5107
+ async function handleRequirementTagRemove(opts) {
5108
+ let stopSpinner = null;
5109
+ try {
5110
+ const { requirementService, auth } = getServices2();
5111
+ const isAuthenticated = await auth.isAuthenticated();
5112
+ if (!isAuthenticated) {
5113
+ return {
5114
+ success: false,
5115
+ message: chalk7.red("\u274C Not authenticated. Please run `braingrid login` first.")
5116
+ };
5117
+ }
5118
+ if (!opts.name || opts.name.trim().length === 0) {
5119
+ return {
5120
+ success: false,
5121
+ message: chalk7.red("\u274C Tag name is required")
5122
+ };
5123
+ }
5124
+ const requirementResult = await workspaceManager.getRequirement(opts.requirementId);
5125
+ if (!requirementResult.success) {
5126
+ return {
5127
+ success: false,
5128
+ message: requirementResult.error
5129
+ };
5130
+ }
5131
+ const requirementId = requirementResult.requirementId;
5132
+ const workspace = await workspaceManager.getProject(opts.project);
5133
+ if (!workspace.success) {
5134
+ return {
5135
+ success: false,
5136
+ message: workspace.error
5137
+ };
5138
+ }
5139
+ const projectId = workspace.projectId;
5140
+ const normalizedId = normalizeRequirementId(requirementId);
5141
+ stopSpinner = showSpinner("Removing tag", chalk7.gray);
5142
+ await requirementService.removeRequirementTag(projectId, normalizedId, opts.name.trim());
5143
+ stopSpinner();
5144
+ stopSpinner = null;
5145
+ return {
5146
+ success: true,
5147
+ message: chalk7.green(`\u2705 Removed tag "${opts.name}" from ${normalizedId}`)
5148
+ };
5149
+ } catch (error) {
5150
+ if (stopSpinner) {
5151
+ stopSpinner();
5152
+ }
5153
+ if (error && typeof error === "object" && "response" in error) {
5154
+ const axiosError = error;
5155
+ if (axiosError.response?.status === 404) {
5156
+ const errorData = axiosError.response.data;
5157
+ if (errorData?.code === "TAG_NOT_FOUND") {
5158
+ return {
5159
+ success: false,
5160
+ message: chalk7.red(`\u274C Tag "${opts.name}" not found`)
5161
+ };
5162
+ }
5163
+ if (errorData?.code === "ASSOCIATION_NOT_FOUND") {
5164
+ return {
5165
+ success: false,
5166
+ message: chalk7.red(`\u274C Tag "${opts.name}" is not associated with this requirement`)
5167
+ };
5168
+ }
5169
+ }
5170
+ }
5171
+ return {
5172
+ success: false,
5173
+ message: formatError(
5174
+ error,
5175
+ getResourceContext("requirement", opts.requirementId || "unknown")
5176
+ )
5177
+ };
5178
+ }
5179
+ }
4867
5180
 
4868
5181
  // src/handlers/task.handlers.ts
4869
5182
  import chalk8 from "chalk";
@@ -7837,6 +8150,37 @@ requirement.command("review [id]").description("Run AI code review for a require
7837
8150
  process.exit(1);
7838
8151
  }
7839
8152
  });
8153
+ var requirementTag = requirement.command("tag").description("Manage requirement tags");
8154
+ requirementTag.command("list [id]").description("List tags for a requirement (auto-detects ID from git branch if not provided)").option(
8155
+ "-p, --project <id>",
8156
+ "project ID (auto-detects from .braingrid/project.json if not provided)"
8157
+ ).option("--format <format>", "output format (table, json, xml, markdown)", "table").action(async (id, opts) => {
8158
+ const result = await handleRequirementTagList({ ...opts, requirementId: id });
8159
+ console.log(result.message);
8160
+ if (!result.success) {
8161
+ process.exit(1);
8162
+ }
8163
+ });
8164
+ requirementTag.command("add [id]").description("Add a tag to a requirement (auto-detects ID from git branch if not provided)").option(
8165
+ "-p, --project <id>",
8166
+ "project ID (auto-detects from .braingrid/project.json if not provided)"
8167
+ ).requiredOption("--name <name>", "tag name").requiredOption("--color <hex>", "hex color code (e.g., #FF0000)").action(async (id, opts) => {
8168
+ const result = await handleRequirementTagAdd({ ...opts, requirementId: id });
8169
+ console.log(result.message);
8170
+ if (!result.success) {
8171
+ process.exit(1);
8172
+ }
8173
+ });
8174
+ requirementTag.command("remove [id]").description("Remove a tag from a requirement (auto-detects ID from git branch if not provided)").option(
8175
+ "-p, --project <id>",
8176
+ "project ID (auto-detects from .braingrid/project.json if not provided)"
8177
+ ).requiredOption("--name <name>", "tag name to remove").action(async (id, opts) => {
8178
+ const result = await handleRequirementTagRemove({ ...opts, requirementId: id });
8179
+ console.log(result.message);
8180
+ if (!result.success) {
8181
+ process.exit(1);
8182
+ }
8183
+ });
7840
8184
  var task = program.command("task").description("Manage tasks");
7841
8185
  task.command("list").description("List tasks for a requirement").option("-r, --requirement <id>", "requirement ID (REQ-456, auto-detects project if initialized)").option("-p, --project <id>", "project ID (PROJ-123, optional if project is initialized)").option("--format <format>", "output format (table, json, xml, markdown)", "markdown").option("--page <page>", "page number for pagination", "1").option("--limit <limit>", "number of tasks per page", "20").action(async (opts) => {
7842
8186
  const result = await handleTaskList(opts);