@braingrid/cli 0.2.26 → 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,28 @@ 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
+
22
+ ## [0.2.27] - 2025-01-27
23
+
24
+ ### Added
25
+
26
+ - **Requirement tagging support**
27
+ - New `--tags` option for `requirement create` and `specify` commands
28
+ - Accepts comma-separated tags (max 5 per requirement)
29
+ - Tags are validated, trimmed, and empty values filtered
30
+ - Tags displayed in all output formats (table, markdown, XML, JSON)
31
+
10
32
  ## [0.2.26] - 2025-01-20
11
33
 
12
34
  ### 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.26" : "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",
@@ -2744,6 +2744,11 @@ function formatRequirementOutput(requirement2, options) {
2744
2744
  }
2745
2745
  message += `${chalk5.bold("Status:")} ${requirement2.status}
2746
2746
  `;
2747
+ if (requirement2.tags && requirement2.tags.length > 0) {
2748
+ const tagNames = requirement2.tags.map((tag) => tag.name).join(", ");
2749
+ message += `${chalk5.bold("Tags:")} ${tagNames}
2750
+ `;
2751
+ }
2747
2752
  if (requirement2.assignee) {
2748
2753
  const assigneeName = requirement2.assignee.first_name || requirement2.assignee.last_name ? `${requirement2.assignee.first_name || ""} ${requirement2.assignee.last_name || ""}`.trim() : requirement2.assignee.email;
2749
2754
  message += `${chalk5.bold("Assigned to:")} ${assigneeName} (${requirement2.assignee.email})
@@ -2890,15 +2895,16 @@ function formatRequirementListMarkdown(requirements, pagination) {
2890
2895
  md += "_No requirements found._\n";
2891
2896
  return md;
2892
2897
  }
2893
- md += "| Short ID | Status | Name | Branch | Progress |\n";
2894
- md += "|----------|--------|------|--------|----------|\n";
2898
+ md += "| Short ID | Status | Name | Branch | Tags | Progress |\n";
2899
+ md += "|----------|--------|------|--------|------|----------|\n";
2895
2900
  for (const req of requirements) {
2896
2901
  const shortId = req.short_id || req.id.slice(0, 11);
2897
2902
  const status = req.status;
2898
2903
  const name = req.name;
2899
2904
  const branch = req.branch || "N/A";
2905
+ const tags = req.tags && req.tags.length > 0 ? req.tags.map((t) => t.name).join(", ") : "-";
2900
2906
  const progress = req.task_progress ? `${req.task_progress.progress_percentage}%` : "N/A";
2901
- md += `| ${shortId} | ${status} | ${name} | ${branch} | ${progress} |
2907
+ md += `| ${shortId} | ${status} | ${name} | ${branch} | ${tags} | ${progress} |
2902
2908
  `;
2903
2909
  }
2904
2910
  if (pagination) {
@@ -2971,6 +2977,15 @@ function formatRequirementListXml(requirements, pagination) {
2971
2977
  `;
2972
2978
  xml += " </assignee>\n";
2973
2979
  }
2980
+ if (req.tags && req.tags.length > 0) {
2981
+ xml += ` <tags count="${req.tags.length}">
2982
+ `;
2983
+ for (const tag of req.tags) {
2984
+ xml += ` <tag>${escapeXml(tag.name)}</tag>
2985
+ `;
2986
+ }
2987
+ xml += " </tags>\n";
2988
+ }
2974
2989
  xml += ` <created_at>${escapeXml(req.created_at)}</created_at>
2975
2990
  `;
2976
2991
  xml += ` <updated_at>${escapeXml(req.updated_at)}</updated_at>
@@ -3087,6 +3102,12 @@ function formatRequirementBuildMarkdown(requirement2, options) {
3087
3102
  md += `**Status:** ${requirement2.status}
3088
3103
 
3089
3104
  `;
3105
+ if (requirement2.tags && requirement2.tags.length > 0) {
3106
+ const tagNames = requirement2.tags.map((tag) => tag.name).join(", ");
3107
+ md += `**Tags:** ${tagNames}
3108
+
3109
+ `;
3110
+ }
3090
3111
  if (requirement2.assignee) {
3091
3112
  const assigneeName = requirement2.assignee.first_name || requirement2.assignee.last_name ? `${requirement2.assignee.first_name || ""} ${requirement2.assignee.last_name || ""}`.trim() : requirement2.assignee.email;
3092
3113
  md += `**Assigned to:** ${assigneeName} (${requirement2.assignee.email})
@@ -3223,6 +3244,15 @@ function formatRequirementBuildXml(requirement2) {
3223
3244
  xml += ` <branch>${escapeXml(requirement2.branch)}</branch>
3224
3245
  `;
3225
3246
  }
3247
+ if (requirement2.tags && requirement2.tags.length > 0) {
3248
+ xml += ` <tags count="${requirement2.tags.length}">
3249
+ `;
3250
+ for (const tag of requirement2.tags) {
3251
+ xml += ` <tag>${escapeXml(tag.name)}</tag>
3252
+ `;
3253
+ }
3254
+ xml += " </tags>\n";
3255
+ }
3226
3256
  if (requirement2.assignee) {
3227
3257
  xml += " <assignee>\n";
3228
3258
  xml += ` <email>${escapeXml(requirement2.assignee.email)}</email>
@@ -3974,8 +4004,57 @@ var RequirementService = class {
3974
4004
  });
3975
4005
  });
3976
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
+ }
3977
4025
  };
3978
4026
 
4027
+ // src/utils/tag-validation.ts
4028
+ var MAX_TAGS = 5;
4029
+ function validateTags(tagsString) {
4030
+ if (!tagsString || tagsString.trim().length === 0) {
4031
+ return { valid: true, tags: [] };
4032
+ }
4033
+ const tags = tagsString.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
4034
+ if (tags.length > MAX_TAGS) {
4035
+ return {
4036
+ valid: false,
4037
+ error: `Maximum ${MAX_TAGS} tags allowed`
4038
+ };
4039
+ }
4040
+ return { valid: true, tags };
4041
+ }
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
+
3979
4058
  // src/handlers/requirement.handlers.ts
3980
4059
  function getServices2() {
3981
4060
  const config = getConfig();
@@ -4203,11 +4282,23 @@ async function handleRequirementCreate(opts) {
4203
4282
  };
4204
4283
  }
4205
4284
  }
4285
+ let validatedTags;
4286
+ if (opts.tags) {
4287
+ const tagResult = validateTags(opts.tags);
4288
+ if (!tagResult.valid) {
4289
+ return {
4290
+ success: false,
4291
+ message: chalk7.red(`\u274C ${tagResult.error}`)
4292
+ };
4293
+ }
4294
+ validatedTags = tagResult.tags;
4295
+ }
4206
4296
  stopSpinner = showSpinner("Creating requirement", chalk7.gray);
4207
4297
  const requirement2 = await requirementService.createProjectRequirement(projectId, {
4208
4298
  name: opts.name,
4209
4299
  content: opts.content || null,
4210
- assigned_to: opts.assignedTo || null
4300
+ assigned_to: opts.assignedTo || null,
4301
+ tags: validatedTags
4211
4302
  });
4212
4303
  stopSpinner();
4213
4304
  stopSpinner = null;
@@ -4280,9 +4371,21 @@ async function handleRequirementSpecify(opts) {
4280
4371
  message: chalk7.red("\u274C Prompt must be no more than 5000 characters long")
4281
4372
  };
4282
4373
  }
4374
+ let validatedTags;
4375
+ if (opts.tags) {
4376
+ const tagResult = validateTags(opts.tags);
4377
+ if (!tagResult.valid) {
4378
+ return {
4379
+ success: false,
4380
+ message: chalk7.red(`\u274C ${tagResult.error}`)
4381
+ };
4382
+ }
4383
+ validatedTags = tagResult.tags;
4384
+ }
4283
4385
  stopSpinner = showSpinner("Specifying requirement...");
4284
4386
  const requirement2 = await requirementService.specifyRequirement(projectId, {
4285
- prompt: opts.prompt
4387
+ prompt: opts.prompt,
4388
+ tags: validatedTags
4286
4389
  });
4287
4390
  stopSpinner();
4288
4391
  stopSpinner = null;
@@ -4794,6 +4897,286 @@ async function handleReviewAcceptance(opts) {
4794
4897
  };
4795
4898
  }
4796
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
+ }
4797
5180
 
4798
5181
  // src/handlers/task.handlers.ts
4799
5182
  import chalk8 from "chalk";
@@ -7610,7 +7993,7 @@ program.command("update").description("Update BrainGrid CLI to the latest versio
7610
7993
  program.command("specify").description("Create AI-refined requirement from prompt").option(
7611
7994
  "-p, --project <id>",
7612
7995
  "project ID (auto-detects from .braingrid/project.json if not provided)"
7613
- ).requiredOption("--prompt <prompt>", "requirement description (10-5000 characters)").option("--format <format>", "output format (table, json, xml, markdown)", "table").action(async (opts) => {
7996
+ ).requiredOption("--prompt <prompt>", "requirement description (10-5000 characters)").option("-t, --tags <tags>", "comma-separated tags (max 5)").option("--format <format>", "output format (table, json, xml, markdown)", "table").action(async (opts) => {
7614
7997
  const result = await handleRequirementSpecify(opts);
7615
7998
  console.log(result.message);
7616
7999
  if (!result.success) {
@@ -7695,7 +8078,7 @@ requirement.command("show [id]").description("Show requirement details (auto-det
7695
8078
  requirement.command("create").description("Create a new requirement").option(
7696
8079
  "-p, --project <id>",
7697
8080
  "project ID (auto-detects from .braingrid/project.json if not provided)"
7698
- ).requiredOption("-n, --name <name>", "requirement name").option("-c, --content <content>", "requirement content/description").option("-a, --assigned-to <uuid>", "user UUID to assign the requirement to").action(async (opts) => {
8081
+ ).requiredOption("-n, --name <name>", "requirement name").option("-c, --content <content>", "requirement content/description").option("-a, --assigned-to <uuid>", "user UUID to assign the requirement to").option("-t, --tags <tags>", "comma-separated tags (max 5)").action(async (opts) => {
7699
8082
  const result = await handleRequirementCreate(opts);
7700
8083
  console.log(result.message);
7701
8084
  if (!result.success) {
@@ -7767,6 +8150,37 @@ requirement.command("review [id]").description("Run AI code review for a require
7767
8150
  process.exit(1);
7768
8151
  }
7769
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
+ });
7770
8184
  var task = program.command("task").description("Manage tasks");
7771
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) => {
7772
8186
  const result = await handleTaskList(opts);