@braingrid/cli 0.2.25 → 0.2.27
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 +25 -0
- package/README.md +7 -1
- package/dist/cli.js +297 -84
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.27] - 2025-01-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Requirement tagging support**
|
|
15
|
+
- New `--tags` option for `requirement create` and `specify` commands
|
|
16
|
+
- Accepts comma-separated tags (max 5 per requirement)
|
|
17
|
+
- Tags are validated, trimmed, and empty values filtered
|
|
18
|
+
- Tags displayed in all output formats (table, markdown, XML, JSON)
|
|
19
|
+
|
|
20
|
+
## [0.2.26] - 2025-01-20
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **BRAINGRID_API_TOKEN environment variable support**
|
|
25
|
+
- Enables authentication via JWT token for sandbox/CI environments
|
|
26
|
+
- Allows CLI usage without interactive OAuth login flow
|
|
27
|
+
- Useful for automated pipelines and testing scenarios
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **README documentation updates**
|
|
32
|
+
- Added `requirement create-branch` and `requirement review` commands to docs
|
|
33
|
+
- Updated shell completion subcommands list
|
|
34
|
+
|
|
10
35
|
## [0.2.25] - 2025-12-22
|
|
11
36
|
|
|
12
37
|
### Added
|
package/README.md
CHANGED
|
@@ -213,6 +213,8 @@ braingrid requirement update [id] [--status IDEA|PLANNED|IN_PROGRESS|REVIEW|COMP
|
|
|
213
213
|
braingrid requirement delete [id] [--force]
|
|
214
214
|
braingrid requirement breakdown [id]
|
|
215
215
|
braingrid requirement build [id] [--format markdown|json|xml]
|
|
216
|
+
braingrid requirement create-branch [id] [--name <branch-name>] [--base <branch>]
|
|
217
|
+
braingrid requirement review [id] [--pr <number>]
|
|
216
218
|
|
|
217
219
|
# Working with a different project:
|
|
218
220
|
braingrid requirement list -p PROJ-456 [--status PLANNED]
|
|
@@ -226,6 +228,10 @@ braingrid requirement create -p PROJ-456 --name "Description"
|
|
|
226
228
|
> **Note:** The `-r`/`--requirement` parameter is optional and accepts formats like `REQ-456`, `req-456`, or `456`. The CLI will automatically detect the requirement ID from your git branch name (e.g., `feature/REQ-123-description` or `REQ-123-fix-bug`) if it is not provided.
|
|
227
229
|
>
|
|
228
230
|
> **Note:** The `requirement list` command displays requirements with their status, name, branch (if assigned), and progress percentage.
|
|
231
|
+
>
|
|
232
|
+
> **Note:** The `create-branch` command creates a GitHub branch for a requirement. It auto-generates a branch name in the format `{username}/REQ-123-slug` if not provided.
|
|
233
|
+
>
|
|
234
|
+
> **Note:** The `review` command runs an AI-powered acceptance review on a pull request, validating it against the requirement's acceptance criteria. It auto-detects the PR number from the current branch if not provided.
|
|
229
235
|
|
|
230
236
|
### Task Commands
|
|
231
237
|
|
|
@@ -309,7 +315,7 @@ eval "$(braingrid completion zsh)"
|
|
|
309
315
|
### What Gets Completed
|
|
310
316
|
|
|
311
317
|
- **Commands**: `login`, `logout`, `project`, `requirement`, `task`, etc.
|
|
312
|
-
- **Subcommands**: `list`, `show`, `create`, `update`, `delete`, `breakdown`, `build`
|
|
318
|
+
- **Subcommands**: `list`, `show`, `create`, `update`, `delete`, `breakdown`, `build`, `create-branch`, `review`
|
|
313
319
|
- **Options**: `--help`, `--format`, `--status`, `--project`, `--requirement`
|
|
314
320
|
- **Values**: Status values (`IDEA`, `PLANNED`, `IN_PROGRESS`, etc.), format options (`table`, `json`, `xml`, `markdown`)
|
|
315
321
|
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,81 @@ import chalk6 from "chalk";
|
|
|
23
23
|
// src/utils/axios-with-auth.ts
|
|
24
24
|
import axios from "axios";
|
|
25
25
|
|
|
26
|
+
// src/build-config.ts
|
|
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";
|
|
29
|
+
var PRODUCTION_CONFIG = {
|
|
30
|
+
apiUrl: "https://app.braingrid.ai",
|
|
31
|
+
workosAuthUrl: "https://auth.braingrid.ai",
|
|
32
|
+
workosClientId: "client_01K6H010C9K69HSDPM9CQM85S7"
|
|
33
|
+
};
|
|
34
|
+
var DEVELOPMENT_CONFIG = {
|
|
35
|
+
apiUrl: "https://app.dev.braingrid.ai",
|
|
36
|
+
workosAuthUrl: "https://balanced-celebration-78-staging.authkit.app",
|
|
37
|
+
workosClientId: "client_01K6H04GF21T4JXNS3JDQM3YNE"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/utils/config.ts
|
|
41
|
+
var BRAINGRID_API_TOKEN = process.env.BRAINGRID_API_TOKEN;
|
|
42
|
+
function getConfig() {
|
|
43
|
+
const baseConfig = BUILD_ENV === "production" ? PRODUCTION_CONFIG : DEVELOPMENT_CONFIG;
|
|
44
|
+
let apiUrl = baseConfig.apiUrl;
|
|
45
|
+
if (BUILD_ENV === "development") {
|
|
46
|
+
if (process.env.NODE_ENV === "local" || process.env.NODE_ENV === "test") {
|
|
47
|
+
apiUrl = "http://localhost:3377";
|
|
48
|
+
} else if (process.env.NODE_ENV === "development") {
|
|
49
|
+
apiUrl = DEVELOPMENT_CONFIG.apiUrl;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const getWorkOSAuthUrl = () => {
|
|
53
|
+
if (process.env.WORKOS_AUTH_URL) {
|
|
54
|
+
return process.env.WORKOS_AUTH_URL;
|
|
55
|
+
}
|
|
56
|
+
if (BUILD_ENV === "production") {
|
|
57
|
+
return PRODUCTION_CONFIG.workosAuthUrl;
|
|
58
|
+
}
|
|
59
|
+
const env = process.env.NODE_ENV || "development";
|
|
60
|
+
if (env === "local" || env === "test" || env === "development" || env === "staging") {
|
|
61
|
+
return DEVELOPMENT_CONFIG.workosAuthUrl;
|
|
62
|
+
}
|
|
63
|
+
return PRODUCTION_CONFIG.workosAuthUrl;
|
|
64
|
+
};
|
|
65
|
+
const getOAuthClientId = () => {
|
|
66
|
+
if (process.env.WORKOS_CLIENT_ID) {
|
|
67
|
+
return process.env.WORKOS_CLIENT_ID;
|
|
68
|
+
}
|
|
69
|
+
if (BUILD_ENV === "production") {
|
|
70
|
+
return PRODUCTION_CONFIG.workosClientId;
|
|
71
|
+
}
|
|
72
|
+
const env = process.env.NODE_ENV || "development";
|
|
73
|
+
if (env === "local" || env === "test" || env === "development" || env === "staging") {
|
|
74
|
+
return DEVELOPMENT_CONFIG.workosClientId;
|
|
75
|
+
}
|
|
76
|
+
return PRODUCTION_CONFIG.workosClientId;
|
|
77
|
+
};
|
|
78
|
+
const getWebAppUrl = () => {
|
|
79
|
+
if (process.env.BRAINGRID_WEB_URL) {
|
|
80
|
+
return process.env.BRAINGRID_WEB_URL;
|
|
81
|
+
}
|
|
82
|
+
if (BUILD_ENV === "production") {
|
|
83
|
+
return PRODUCTION_CONFIG.apiUrl;
|
|
84
|
+
}
|
|
85
|
+
const env = process.env.NODE_ENV || "development";
|
|
86
|
+
if (env === "local" || env === "test") {
|
|
87
|
+
return "http://localhost:3377";
|
|
88
|
+
}
|
|
89
|
+
return DEVELOPMENT_CONFIG.apiUrl;
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
apiUrl: process.env.BRAINGRID_API_URL || apiUrl,
|
|
93
|
+
organizationId: process.env.BRAINGRID_ORG_ID,
|
|
94
|
+
clientId: process.env.BRAINGRID_CLIENT_ID || "braingrid-cli",
|
|
95
|
+
oauthClientId: getOAuthClientId(),
|
|
96
|
+
getWorkOSAuthUrl,
|
|
97
|
+
getWebAppUrl
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
26
101
|
// src/utils/logger.ts
|
|
27
102
|
import fs from "fs";
|
|
28
103
|
import path from "path";
|
|
@@ -211,6 +286,10 @@ function createAuthenticatedAxios(auth) {
|
|
|
211
286
|
});
|
|
212
287
|
instance.interceptors.request.use(
|
|
213
288
|
async (config) => {
|
|
289
|
+
if (BRAINGRID_API_TOKEN) {
|
|
290
|
+
config.headers.Authorization = `Bearer ${BRAINGRID_API_TOKEN}`;
|
|
291
|
+
return config;
|
|
292
|
+
}
|
|
214
293
|
const session = await auth.getStoredSession();
|
|
215
294
|
if (session) {
|
|
216
295
|
config.headers.Authorization = `Bearer ${session.sealed_session}`;
|
|
@@ -251,6 +330,11 @@ function createAuthenticatedAxios(auth) {
|
|
|
251
330
|
const isRedirectToAuth = isAuthRedirect(error);
|
|
252
331
|
if ((is401 || isRedirectToAuth) && !originalRequest._retry) {
|
|
253
332
|
originalRequest._retry = true;
|
|
333
|
+
if (BRAINGRID_API_TOKEN) {
|
|
334
|
+
logger.warn("[AUTH] Sandbox token expired or invalid");
|
|
335
|
+
const sandboxError = new Error(auth.getSandboxExpiredMessage());
|
|
336
|
+
return Promise.reject(sandboxError);
|
|
337
|
+
}
|
|
254
338
|
if (isRedirectToAuth) {
|
|
255
339
|
logger.debug("[AUTH] Received redirect to auth endpoint - token likely expired");
|
|
256
340
|
} else {
|
|
@@ -422,82 +506,6 @@ import { createHash, randomBytes } from "crypto";
|
|
|
422
506
|
import { createServer } from "http";
|
|
423
507
|
import open from "open";
|
|
424
508
|
import axios3, { AxiosError as AxiosError2 } from "axios";
|
|
425
|
-
|
|
426
|
-
// src/build-config.ts
|
|
427
|
-
var BUILD_ENV = true ? "production" : process.env.NODE_ENV === "test" ? "development" : "production";
|
|
428
|
-
var CLI_VERSION = true ? "0.2.25" : "0.0.0-test";
|
|
429
|
-
var PRODUCTION_CONFIG = {
|
|
430
|
-
apiUrl: "https://app.braingrid.ai",
|
|
431
|
-
workosAuthUrl: "https://auth.braingrid.ai",
|
|
432
|
-
workosClientId: "client_01K6H010C9K69HSDPM9CQM85S7"
|
|
433
|
-
};
|
|
434
|
-
var DEVELOPMENT_CONFIG = {
|
|
435
|
-
apiUrl: "https://app.dev.braingrid.ai",
|
|
436
|
-
workosAuthUrl: "https://balanced-celebration-78-staging.authkit.app",
|
|
437
|
-
workosClientId: "client_01K6H04GF21T4JXNS3JDQM3YNE"
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
// src/utils/config.ts
|
|
441
|
-
function getConfig() {
|
|
442
|
-
const baseConfig = BUILD_ENV === "production" ? PRODUCTION_CONFIG : DEVELOPMENT_CONFIG;
|
|
443
|
-
let apiUrl = baseConfig.apiUrl;
|
|
444
|
-
if (BUILD_ENV === "development") {
|
|
445
|
-
if (process.env.NODE_ENV === "local" || process.env.NODE_ENV === "test") {
|
|
446
|
-
apiUrl = "http://localhost:3377";
|
|
447
|
-
} else if (process.env.NODE_ENV === "development") {
|
|
448
|
-
apiUrl = DEVELOPMENT_CONFIG.apiUrl;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
const getWorkOSAuthUrl = () => {
|
|
452
|
-
if (process.env.WORKOS_AUTH_URL) {
|
|
453
|
-
return process.env.WORKOS_AUTH_URL;
|
|
454
|
-
}
|
|
455
|
-
if (BUILD_ENV === "production") {
|
|
456
|
-
return PRODUCTION_CONFIG.workosAuthUrl;
|
|
457
|
-
}
|
|
458
|
-
const env = process.env.NODE_ENV || "development";
|
|
459
|
-
if (env === "local" || env === "test" || env === "development" || env === "staging") {
|
|
460
|
-
return DEVELOPMENT_CONFIG.workosAuthUrl;
|
|
461
|
-
}
|
|
462
|
-
return PRODUCTION_CONFIG.workosAuthUrl;
|
|
463
|
-
};
|
|
464
|
-
const getOAuthClientId = () => {
|
|
465
|
-
if (process.env.WORKOS_CLIENT_ID) {
|
|
466
|
-
return process.env.WORKOS_CLIENT_ID;
|
|
467
|
-
}
|
|
468
|
-
if (BUILD_ENV === "production") {
|
|
469
|
-
return PRODUCTION_CONFIG.workosClientId;
|
|
470
|
-
}
|
|
471
|
-
const env = process.env.NODE_ENV || "development";
|
|
472
|
-
if (env === "local" || env === "test" || env === "development" || env === "staging") {
|
|
473
|
-
return DEVELOPMENT_CONFIG.workosClientId;
|
|
474
|
-
}
|
|
475
|
-
return PRODUCTION_CONFIG.workosClientId;
|
|
476
|
-
};
|
|
477
|
-
const getWebAppUrl = () => {
|
|
478
|
-
if (process.env.BRAINGRID_WEB_URL) {
|
|
479
|
-
return process.env.BRAINGRID_WEB_URL;
|
|
480
|
-
}
|
|
481
|
-
if (BUILD_ENV === "production") {
|
|
482
|
-
return PRODUCTION_CONFIG.apiUrl;
|
|
483
|
-
}
|
|
484
|
-
const env = process.env.NODE_ENV || "development";
|
|
485
|
-
if (env === "local" || env === "test") {
|
|
486
|
-
return "http://localhost:3377";
|
|
487
|
-
}
|
|
488
|
-
return DEVELOPMENT_CONFIG.apiUrl;
|
|
489
|
-
};
|
|
490
|
-
return {
|
|
491
|
-
apiUrl: process.env.BRAINGRID_API_URL || apiUrl,
|
|
492
|
-
organizationId: process.env.BRAINGRID_ORG_ID,
|
|
493
|
-
clientId: process.env.BRAINGRID_CLIENT_ID || "braingrid-cli",
|
|
494
|
-
oauthClientId: getOAuthClientId(),
|
|
495
|
-
getWorkOSAuthUrl,
|
|
496
|
-
getWebAppUrl
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// src/services/oauth2-auth.ts
|
|
501
509
|
var logger2 = getLogger();
|
|
502
510
|
var OAuth2Handler = class {
|
|
503
511
|
/**
|
|
@@ -1000,6 +1008,7 @@ var KEYCHAIN_SERVICE = "braingrid-cli";
|
|
|
1000
1008
|
var KEYCHAIN_ACCOUNT = "session";
|
|
1001
1009
|
var GITHUB_KEYCHAIN_ACCOUNT = "github-token";
|
|
1002
1010
|
var BraingridAuth = class {
|
|
1011
|
+
// Cached session from env token
|
|
1003
1012
|
constructor(baseUrl) {
|
|
1004
1013
|
this.lastValidationTime = 0;
|
|
1005
1014
|
this.lastValidationResult = false;
|
|
@@ -1011,11 +1020,103 @@ var BraingridAuth = class {
|
|
|
1011
1020
|
this.oauthHandler = null;
|
|
1012
1021
|
// Store refresh token separately
|
|
1013
1022
|
this.logger = getLogger();
|
|
1023
|
+
this.envTokenSession = null;
|
|
1014
1024
|
const config = getConfig();
|
|
1015
1025
|
this.baseUrl = baseUrl || config.apiUrl || "https://app.braingrid.ai";
|
|
1016
1026
|
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Check if CLI is using BRAINGRID_API_TOKEN environment variable
|
|
1029
|
+
* This is used in sandbox environments where tokens are injected
|
|
1030
|
+
*/
|
|
1031
|
+
isUsingEnvToken() {
|
|
1032
|
+
return Boolean(BRAINGRID_API_TOKEN);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Get the env token value (for use in axios interceptor)
|
|
1036
|
+
*/
|
|
1037
|
+
getEnvToken() {
|
|
1038
|
+
return BRAINGRID_API_TOKEN;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Fetch user profile from server using the env token
|
|
1042
|
+
* This is used to populate session data when using BRAINGRID_API_TOKEN
|
|
1043
|
+
*/
|
|
1044
|
+
async fetchProfileFromServer() {
|
|
1045
|
+
if (!BRAINGRID_API_TOKEN) {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
this.logger.debug("[AUTH] Fetching profile using BRAINGRID_API_TOKEN");
|
|
1050
|
+
const response = await axiosWithRetry(
|
|
1051
|
+
{
|
|
1052
|
+
url: `${this.baseUrl}/api/v1/profile`,
|
|
1053
|
+
method: "POST",
|
|
1054
|
+
headers: {
|
|
1055
|
+
"Content-Type": "application/json",
|
|
1056
|
+
Authorization: `Bearer ${BRAINGRID_API_TOKEN}`
|
|
1057
|
+
},
|
|
1058
|
+
maxRedirects: 0,
|
|
1059
|
+
validateStatus: (status) => status < 500
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
maxRetries: 2,
|
|
1063
|
+
initialDelay: 500
|
|
1064
|
+
}
|
|
1065
|
+
);
|
|
1066
|
+
if (response.status !== 200) {
|
|
1067
|
+
this.logger.warn(`[AUTH] Profile fetch failed with status ${response.status}`);
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
const profileData = response.data;
|
|
1071
|
+
if (profileData.error || !profileData.user || !profileData.organization) {
|
|
1072
|
+
this.logger.warn("[AUTH] Profile response missing user or organization data");
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
const user = {
|
|
1076
|
+
object: "user",
|
|
1077
|
+
id: profileData.user.id,
|
|
1078
|
+
email: profileData.user.email,
|
|
1079
|
+
emailVerified: true,
|
|
1080
|
+
firstName: profileData.user.firstName || "",
|
|
1081
|
+
lastName: profileData.user.lastName || "",
|
|
1082
|
+
profilePictureUrl: profileData.user.avatar || "",
|
|
1083
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1084
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1085
|
+
lastSignInAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1086
|
+
externalId: null,
|
|
1087
|
+
metadata: {
|
|
1088
|
+
username: profileData.user.username,
|
|
1089
|
+
organizationId: profileData.organization.id,
|
|
1090
|
+
organizationName: profileData.organization.name
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
const session = {
|
|
1094
|
+
user,
|
|
1095
|
+
sealed_session: BRAINGRID_API_TOKEN,
|
|
1096
|
+
organization_id: profileData.organization.id,
|
|
1097
|
+
created_at: /* @__PURE__ */ new Date(),
|
|
1098
|
+
updated_at: /* @__PURE__ */ new Date(),
|
|
1099
|
+
login_time: /* @__PURE__ */ new Date()
|
|
1100
|
+
};
|
|
1101
|
+
this.envTokenSession = session;
|
|
1102
|
+
this.logger.debug("[AUTH] Successfully fetched profile from env token");
|
|
1103
|
+
return session;
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
this.logger.error("[AUTH] Error fetching profile with env token:", { error });
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1017
1109
|
async isAuthenticated(forceValidation = false) {
|
|
1018
1110
|
try {
|
|
1111
|
+
if (BRAINGRID_API_TOKEN) {
|
|
1112
|
+
this.logger.debug("[AUTH] Using BRAINGRID_API_TOKEN (sandbox mode)");
|
|
1113
|
+
if (isJWTExpired(BRAINGRID_API_TOKEN)) {
|
|
1114
|
+
this.logger.warn("[AUTH] Sandbox session expired");
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
const session2 = await this.getStoredSession();
|
|
1118
|
+
return session2 !== null;
|
|
1119
|
+
}
|
|
1019
1120
|
const session = await this.getStoredSession();
|
|
1020
1121
|
if (!session) {
|
|
1021
1122
|
this.logger.debug("[AUTH] No stored session found");
|
|
@@ -1149,6 +1250,12 @@ var BraingridAuth = class {
|
|
|
1149
1250
|
}
|
|
1150
1251
|
}
|
|
1151
1252
|
async getStoredSession() {
|
|
1253
|
+
if (BRAINGRID_API_TOKEN) {
|
|
1254
|
+
if (this.envTokenSession) {
|
|
1255
|
+
return this.envTokenSession;
|
|
1256
|
+
}
|
|
1257
|
+
return await this.fetchProfileFromServer();
|
|
1258
|
+
}
|
|
1152
1259
|
try {
|
|
1153
1260
|
const sessionData = await credentialStore.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
1154
1261
|
if (!sessionData) return null;
|
|
@@ -1177,12 +1284,23 @@ var BraingridAuth = class {
|
|
|
1177
1284
|
this.loginTime = nowMs;
|
|
1178
1285
|
}
|
|
1179
1286
|
async clearSession() {
|
|
1287
|
+
if (BRAINGRID_API_TOKEN) {
|
|
1288
|
+
this.logger.debug("[AUTH] Session managed by sandbox environment - clear is no-op");
|
|
1289
|
+
this.envTokenSession = null;
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1180
1292
|
await credentialStore.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
1181
1293
|
await credentialStore.deletePassword(KEYCHAIN_SERVICE, "refresh-token");
|
|
1182
1294
|
this.refreshTokenValue = void 0;
|
|
1183
1295
|
this.lastValidationTime = 0;
|
|
1184
1296
|
this.lastValidationResult = false;
|
|
1185
1297
|
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Get sandbox-specific error message for expired/invalid tokens
|
|
1300
|
+
*/
|
|
1301
|
+
getSandboxExpiredMessage() {
|
|
1302
|
+
return "Sandbox session expired. Your sandbox environment has a 5-hour limit.\nPlease create a new sandbox to continue.";
|
|
1303
|
+
}
|
|
1186
1304
|
async handleAuthenticationError() {
|
|
1187
1305
|
this.lastValidationTime = 0;
|
|
1188
1306
|
this.lastValidationResult = false;
|
|
@@ -2626,6 +2744,11 @@ function formatRequirementOutput(requirement2, options) {
|
|
|
2626
2744
|
}
|
|
2627
2745
|
message += `${chalk5.bold("Status:")} ${requirement2.status}
|
|
2628
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
|
+
}
|
|
2629
2752
|
if (requirement2.assignee) {
|
|
2630
2753
|
const assigneeName = requirement2.assignee.first_name || requirement2.assignee.last_name ? `${requirement2.assignee.first_name || ""} ${requirement2.assignee.last_name || ""}`.trim() : requirement2.assignee.email;
|
|
2631
2754
|
message += `${chalk5.bold("Assigned to:")} ${assigneeName} (${requirement2.assignee.email})
|
|
@@ -2772,15 +2895,16 @@ function formatRequirementListMarkdown(requirements, pagination) {
|
|
|
2772
2895
|
md += "_No requirements found._\n";
|
|
2773
2896
|
return md;
|
|
2774
2897
|
}
|
|
2775
|
-
md += "| Short ID | Status | Name | Branch | Progress |\n";
|
|
2776
|
-
md += "
|
|
2898
|
+
md += "| Short ID | Status | Name | Branch | Tags | Progress |\n";
|
|
2899
|
+
md += "|----------|--------|------|--------|------|----------|\n";
|
|
2777
2900
|
for (const req of requirements) {
|
|
2778
2901
|
const shortId = req.short_id || req.id.slice(0, 11);
|
|
2779
2902
|
const status = req.status;
|
|
2780
2903
|
const name = req.name;
|
|
2781
2904
|
const branch = req.branch || "N/A";
|
|
2905
|
+
const tags = req.tags && req.tags.length > 0 ? req.tags.map((t) => t.name).join(", ") : "-";
|
|
2782
2906
|
const progress = req.task_progress ? `${req.task_progress.progress_percentage}%` : "N/A";
|
|
2783
|
-
md += `| ${shortId} | ${status} | ${name} | ${branch} | ${progress} |
|
|
2907
|
+
md += `| ${shortId} | ${status} | ${name} | ${branch} | ${tags} | ${progress} |
|
|
2784
2908
|
`;
|
|
2785
2909
|
}
|
|
2786
2910
|
if (pagination) {
|
|
@@ -2853,6 +2977,15 @@ function formatRequirementListXml(requirements, pagination) {
|
|
|
2853
2977
|
`;
|
|
2854
2978
|
xml += " </assignee>\n";
|
|
2855
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
|
+
}
|
|
2856
2989
|
xml += ` <created_at>${escapeXml(req.created_at)}</created_at>
|
|
2857
2990
|
`;
|
|
2858
2991
|
xml += ` <updated_at>${escapeXml(req.updated_at)}</updated_at>
|
|
@@ -2969,6 +3102,12 @@ function formatRequirementBuildMarkdown(requirement2, options) {
|
|
|
2969
3102
|
md += `**Status:** ${requirement2.status}
|
|
2970
3103
|
|
|
2971
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
|
+
}
|
|
2972
3111
|
if (requirement2.assignee) {
|
|
2973
3112
|
const assigneeName = requirement2.assignee.first_name || requirement2.assignee.last_name ? `${requirement2.assignee.first_name || ""} ${requirement2.assignee.last_name || ""}`.trim() : requirement2.assignee.email;
|
|
2974
3113
|
md += `**Assigned to:** ${assigneeName} (${requirement2.assignee.email})
|
|
@@ -3105,6 +3244,15 @@ function formatRequirementBuildXml(requirement2) {
|
|
|
3105
3244
|
xml += ` <branch>${escapeXml(requirement2.branch)}</branch>
|
|
3106
3245
|
`;
|
|
3107
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
|
+
}
|
|
3108
3256
|
if (requirement2.assignee) {
|
|
3109
3257
|
xml += " <assignee>\n";
|
|
3110
3258
|
xml += ` <email>${escapeXml(requirement2.assignee.email)}</email>
|
|
@@ -3858,6 +4006,22 @@ var RequirementService = class {
|
|
|
3858
4006
|
}
|
|
3859
4007
|
};
|
|
3860
4008
|
|
|
4009
|
+
// src/utils/tag-validation.ts
|
|
4010
|
+
var MAX_TAGS = 5;
|
|
4011
|
+
function validateTags(tagsString) {
|
|
4012
|
+
if (!tagsString || tagsString.trim().length === 0) {
|
|
4013
|
+
return { valid: true, tags: [] };
|
|
4014
|
+
}
|
|
4015
|
+
const tags = tagsString.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
|
|
4016
|
+
if (tags.length > MAX_TAGS) {
|
|
4017
|
+
return {
|
|
4018
|
+
valid: false,
|
|
4019
|
+
error: `Maximum ${MAX_TAGS} tags allowed`
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
return { valid: true, tags };
|
|
4023
|
+
}
|
|
4024
|
+
|
|
3861
4025
|
// src/handlers/requirement.handlers.ts
|
|
3862
4026
|
function getServices2() {
|
|
3863
4027
|
const config = getConfig();
|
|
@@ -4085,11 +4249,23 @@ async function handleRequirementCreate(opts) {
|
|
|
4085
4249
|
};
|
|
4086
4250
|
}
|
|
4087
4251
|
}
|
|
4252
|
+
let validatedTags;
|
|
4253
|
+
if (opts.tags) {
|
|
4254
|
+
const tagResult = validateTags(opts.tags);
|
|
4255
|
+
if (!tagResult.valid) {
|
|
4256
|
+
return {
|
|
4257
|
+
success: false,
|
|
4258
|
+
message: chalk7.red(`\u274C ${tagResult.error}`)
|
|
4259
|
+
};
|
|
4260
|
+
}
|
|
4261
|
+
validatedTags = tagResult.tags;
|
|
4262
|
+
}
|
|
4088
4263
|
stopSpinner = showSpinner("Creating requirement", chalk7.gray);
|
|
4089
4264
|
const requirement2 = await requirementService.createProjectRequirement(projectId, {
|
|
4090
4265
|
name: opts.name,
|
|
4091
4266
|
content: opts.content || null,
|
|
4092
|
-
assigned_to: opts.assignedTo || null
|
|
4267
|
+
assigned_to: opts.assignedTo || null,
|
|
4268
|
+
tags: validatedTags
|
|
4093
4269
|
});
|
|
4094
4270
|
stopSpinner();
|
|
4095
4271
|
stopSpinner = null;
|
|
@@ -4162,9 +4338,21 @@ async function handleRequirementSpecify(opts) {
|
|
|
4162
4338
|
message: chalk7.red("\u274C Prompt must be no more than 5000 characters long")
|
|
4163
4339
|
};
|
|
4164
4340
|
}
|
|
4341
|
+
let validatedTags;
|
|
4342
|
+
if (opts.tags) {
|
|
4343
|
+
const tagResult = validateTags(opts.tags);
|
|
4344
|
+
if (!tagResult.valid) {
|
|
4345
|
+
return {
|
|
4346
|
+
success: false,
|
|
4347
|
+
message: chalk7.red(`\u274C ${tagResult.error}`)
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
validatedTags = tagResult.tags;
|
|
4351
|
+
}
|
|
4165
4352
|
stopSpinner = showSpinner("Specifying requirement...");
|
|
4166
4353
|
const requirement2 = await requirementService.specifyRequirement(projectId, {
|
|
4167
|
-
prompt: opts.prompt
|
|
4354
|
+
prompt: opts.prompt,
|
|
4355
|
+
tags: validatedTags
|
|
4168
4356
|
});
|
|
4169
4357
|
stopSpinner();
|
|
4170
4358
|
stopSpinner = null;
|
|
@@ -5317,6 +5505,14 @@ function getAuth() {
|
|
|
5317
5505
|
async function handleLogin() {
|
|
5318
5506
|
try {
|
|
5319
5507
|
const auth = getAuth();
|
|
5508
|
+
if (BRAINGRID_API_TOKEN) {
|
|
5509
|
+
return {
|
|
5510
|
+
success: true,
|
|
5511
|
+
message: chalk9.blue(
|
|
5512
|
+
"\u2139\uFE0F Using BRAINGRID_API_TOKEN - already authenticated via sandbox environment."
|
|
5513
|
+
)
|
|
5514
|
+
};
|
|
5515
|
+
}
|
|
5320
5516
|
console.log(chalk9.blue("\u{1F510} Starting OAuth2 authentication flow..."));
|
|
5321
5517
|
console.log(chalk9.dim("Your browser will open to complete authentication.\n"));
|
|
5322
5518
|
const gitUser = await getGitUser();
|
|
@@ -5350,6 +5546,12 @@ async function handleLogin() {
|
|
|
5350
5546
|
async function handleLogout() {
|
|
5351
5547
|
try {
|
|
5352
5548
|
const auth = getAuth();
|
|
5549
|
+
if (BRAINGRID_API_TOKEN) {
|
|
5550
|
+
return {
|
|
5551
|
+
success: true,
|
|
5552
|
+
message: chalk9.blue("\u2139\uFE0F Session managed by sandbox environment - logout not required.")
|
|
5553
|
+
};
|
|
5554
|
+
}
|
|
5353
5555
|
await auth.clearSession();
|
|
5354
5556
|
return {
|
|
5355
5557
|
success: true,
|
|
@@ -5367,6 +5569,12 @@ async function handleWhoami() {
|
|
|
5367
5569
|
const auth = getAuth();
|
|
5368
5570
|
const isAuthenticated = await auth.isAuthenticated();
|
|
5369
5571
|
if (!isAuthenticated) {
|
|
5572
|
+
if (BRAINGRID_API_TOKEN) {
|
|
5573
|
+
return {
|
|
5574
|
+
success: false,
|
|
5575
|
+
message: chalk9.red(auth.getSandboxExpiredMessage())
|
|
5576
|
+
};
|
|
5577
|
+
}
|
|
5370
5578
|
return {
|
|
5371
5579
|
success: false,
|
|
5372
5580
|
message: chalk9.yellow("\u26A0\uFE0F Not logged in. Run `braingrid login` to authenticate.")
|
|
@@ -5389,8 +5597,13 @@ async function handleWhoami() {
|
|
|
5389
5597
|
`;
|
|
5390
5598
|
output += `${chalk9.bold("Org ID:")} ${session.organization_id}
|
|
5391
5599
|
`;
|
|
5392
|
-
|
|
5600
|
+
if (BRAINGRID_API_TOKEN) {
|
|
5601
|
+
output += `${chalk9.bold("Auth:")} ${chalk9.cyan("Sandbox API Token")}
|
|
5393
5602
|
`;
|
|
5603
|
+
} else {
|
|
5604
|
+
output += `${chalk9.bold("Session:")} ${new Date(session.created_at).toLocaleString()}
|
|
5605
|
+
`;
|
|
5606
|
+
}
|
|
5394
5607
|
return {
|
|
5395
5608
|
success: true,
|
|
5396
5609
|
message: output,
|
|
@@ -7467,7 +7680,7 @@ program.command("update").description("Update BrainGrid CLI to the latest versio
|
|
|
7467
7680
|
program.command("specify").description("Create AI-refined requirement from prompt").option(
|
|
7468
7681
|
"-p, --project <id>",
|
|
7469
7682
|
"project ID (auto-detects from .braingrid/project.json if not provided)"
|
|
7470
|
-
).requiredOption("--prompt <prompt>", "requirement description (10-5000 characters)").option("--format <format>", "output format (table, json, xml, markdown)", "table").action(async (opts) => {
|
|
7683
|
+
).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) => {
|
|
7471
7684
|
const result = await handleRequirementSpecify(opts);
|
|
7472
7685
|
console.log(result.message);
|
|
7473
7686
|
if (!result.success) {
|
|
@@ -7552,7 +7765,7 @@ requirement.command("show [id]").description("Show requirement details (auto-det
|
|
|
7552
7765
|
requirement.command("create").description("Create a new requirement").option(
|
|
7553
7766
|
"-p, --project <id>",
|
|
7554
7767
|
"project ID (auto-detects from .braingrid/project.json if not provided)"
|
|
7555
|
-
).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) => {
|
|
7768
|
+
).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) => {
|
|
7556
7769
|
const result = await handleRequirementCreate(opts);
|
|
7557
7770
|
console.log(result.message);
|
|
7558
7771
|
if (!result.success) {
|