@braingrid/cli 0.2.24 → 0.2.26

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/dist/cli.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ execAsync
4
+ } from "./chunk-6GC3UJCM.js";
2
5
  import {
3
6
  checkInstalledCliTools,
4
7
  detectLinuxPackageManager,
@@ -20,6 +23,81 @@ import chalk6 from "chalk";
20
23
  // src/utils/axios-with-auth.ts
21
24
  import axios from "axios";
22
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.26" : "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
+
23
101
  // src/utils/logger.ts
24
102
  import fs from "fs";
25
103
  import path from "path";
@@ -208,6 +286,10 @@ function createAuthenticatedAxios(auth) {
208
286
  });
209
287
  instance.interceptors.request.use(
210
288
  async (config) => {
289
+ if (BRAINGRID_API_TOKEN) {
290
+ config.headers.Authorization = `Bearer ${BRAINGRID_API_TOKEN}`;
291
+ return config;
292
+ }
211
293
  const session = await auth.getStoredSession();
212
294
  if (session) {
213
295
  config.headers.Authorization = `Bearer ${session.sealed_session}`;
@@ -248,6 +330,11 @@ function createAuthenticatedAxios(auth) {
248
330
  const isRedirectToAuth = isAuthRedirect(error);
249
331
  if ((is401 || isRedirectToAuth) && !originalRequest._retry) {
250
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
+ }
251
338
  if (isRedirectToAuth) {
252
339
  logger.debug("[AUTH] Received redirect to auth endpoint - token likely expired");
253
340
  } else {
@@ -419,82 +506,6 @@ import { createHash, randomBytes } from "crypto";
419
506
  import { createServer } from "http";
420
507
  import open from "open";
421
508
  import axios3, { AxiosError as AxiosError2 } from "axios";
422
-
423
- // src/build-config.ts
424
- var BUILD_ENV = true ? "production" : process.env.NODE_ENV === "test" ? "development" : "production";
425
- var CLI_VERSION = true ? "0.2.24" : "0.0.0-test";
426
- var PRODUCTION_CONFIG = {
427
- apiUrl: "https://app.braingrid.ai",
428
- workosAuthUrl: "https://auth.braingrid.ai",
429
- workosClientId: "client_01K6H010C9K69HSDPM9CQM85S7"
430
- };
431
- var DEVELOPMENT_CONFIG = {
432
- apiUrl: "https://app.dev.braingrid.ai",
433
- workosAuthUrl: "https://balanced-celebration-78-staging.authkit.app",
434
- workosClientId: "client_01K6H04GF21T4JXNS3JDQM3YNE"
435
- };
436
-
437
- // src/utils/config.ts
438
- function getConfig() {
439
- const baseConfig = BUILD_ENV === "production" ? PRODUCTION_CONFIG : DEVELOPMENT_CONFIG;
440
- let apiUrl = baseConfig.apiUrl;
441
- if (BUILD_ENV === "development") {
442
- if (process.env.NODE_ENV === "local" || process.env.NODE_ENV === "test") {
443
- apiUrl = "http://localhost:3377";
444
- } else if (process.env.NODE_ENV === "development") {
445
- apiUrl = DEVELOPMENT_CONFIG.apiUrl;
446
- }
447
- }
448
- const getWorkOSAuthUrl = () => {
449
- if (process.env.WORKOS_AUTH_URL) {
450
- return process.env.WORKOS_AUTH_URL;
451
- }
452
- if (BUILD_ENV === "production") {
453
- return PRODUCTION_CONFIG.workosAuthUrl;
454
- }
455
- const env = process.env.NODE_ENV || "development";
456
- if (env === "local" || env === "test" || env === "development" || env === "staging") {
457
- return DEVELOPMENT_CONFIG.workosAuthUrl;
458
- }
459
- return PRODUCTION_CONFIG.workosAuthUrl;
460
- };
461
- const getOAuthClientId = () => {
462
- if (process.env.WORKOS_CLIENT_ID) {
463
- return process.env.WORKOS_CLIENT_ID;
464
- }
465
- if (BUILD_ENV === "production") {
466
- return PRODUCTION_CONFIG.workosClientId;
467
- }
468
- const env = process.env.NODE_ENV || "development";
469
- if (env === "local" || env === "test" || env === "development" || env === "staging") {
470
- return DEVELOPMENT_CONFIG.workosClientId;
471
- }
472
- return PRODUCTION_CONFIG.workosClientId;
473
- };
474
- const getWebAppUrl = () => {
475
- if (process.env.BRAINGRID_WEB_URL) {
476
- return process.env.BRAINGRID_WEB_URL;
477
- }
478
- if (BUILD_ENV === "production") {
479
- return PRODUCTION_CONFIG.apiUrl;
480
- }
481
- const env = process.env.NODE_ENV || "development";
482
- if (env === "local" || env === "test") {
483
- return "http://localhost:3377";
484
- }
485
- return DEVELOPMENT_CONFIG.apiUrl;
486
- };
487
- return {
488
- apiUrl: process.env.BRAINGRID_API_URL || apiUrl,
489
- organizationId: process.env.BRAINGRID_ORG_ID,
490
- clientId: process.env.BRAINGRID_CLIENT_ID || "braingrid-cli",
491
- oauthClientId: getOAuthClientId(),
492
- getWorkOSAuthUrl,
493
- getWebAppUrl
494
- };
495
- }
496
-
497
- // src/services/oauth2-auth.ts
498
509
  var logger2 = getLogger();
499
510
  var OAuth2Handler = class {
500
511
  /**
@@ -997,6 +1008,7 @@ var KEYCHAIN_SERVICE = "braingrid-cli";
997
1008
  var KEYCHAIN_ACCOUNT = "session";
998
1009
  var GITHUB_KEYCHAIN_ACCOUNT = "github-token";
999
1010
  var BraingridAuth = class {
1011
+ // Cached session from env token
1000
1012
  constructor(baseUrl) {
1001
1013
  this.lastValidationTime = 0;
1002
1014
  this.lastValidationResult = false;
@@ -1008,11 +1020,103 @@ var BraingridAuth = class {
1008
1020
  this.oauthHandler = null;
1009
1021
  // Store refresh token separately
1010
1022
  this.logger = getLogger();
1023
+ this.envTokenSession = null;
1011
1024
  const config = getConfig();
1012
1025
  this.baseUrl = baseUrl || config.apiUrl || "https://app.braingrid.ai";
1013
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
+ }
1014
1109
  async isAuthenticated(forceValidation = false) {
1015
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
+ }
1016
1120
  const session = await this.getStoredSession();
1017
1121
  if (!session) {
1018
1122
  this.logger.debug("[AUTH] No stored session found");
@@ -1146,6 +1250,12 @@ var BraingridAuth = class {
1146
1250
  }
1147
1251
  }
1148
1252
  async getStoredSession() {
1253
+ if (BRAINGRID_API_TOKEN) {
1254
+ if (this.envTokenSession) {
1255
+ return this.envTokenSession;
1256
+ }
1257
+ return await this.fetchProfileFromServer();
1258
+ }
1149
1259
  try {
1150
1260
  const sessionData = await credentialStore.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
1151
1261
  if (!sessionData) return null;
@@ -1174,12 +1284,23 @@ var BraingridAuth = class {
1174
1284
  this.loginTime = nowMs;
1175
1285
  }
1176
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
+ }
1177
1292
  await credentialStore.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
1178
1293
  await credentialStore.deletePassword(KEYCHAIN_SERVICE, "refresh-token");
1179
1294
  this.refreshTokenValue = void 0;
1180
1295
  this.lastValidationTime = 0;
1181
1296
  this.lastValidationResult = false;
1182
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
+ }
1183
1304
  async handleAuthenticationError() {
1184
1305
  this.lastValidationTime = 0;
1185
1306
  this.lastValidationResult = false;
@@ -1837,10 +1958,10 @@ var LocalProjectConfigSchema = z.object({
1837
1958
  // src/utils/git.ts
1838
1959
  import { exec } from "child_process";
1839
1960
  import { promisify } from "util";
1840
- var execAsync = promisify(exec);
1961
+ var execAsync2 = promisify(exec);
1841
1962
  async function isGitRepository() {
1842
1963
  try {
1843
- const { stdout } = await execAsync("git rev-parse --is-inside-work-tree");
1964
+ const { stdout } = await execAsync2("git rev-parse --is-inside-work-tree");
1844
1965
  return stdout.trim() === "true";
1845
1966
  } catch {
1846
1967
  return false;
@@ -1848,7 +1969,7 @@ async function isGitRepository() {
1848
1969
  }
1849
1970
  async function getRemoteUrl() {
1850
1971
  try {
1851
- const { stdout } = await execAsync("git config --get remote.origin.url");
1972
+ const { stdout } = await execAsync2("git config --get remote.origin.url");
1852
1973
  return stdout.trim() || null;
1853
1974
  } catch {
1854
1975
  return null;
@@ -1874,7 +1995,7 @@ function parseGitHubRepo(url) {
1874
1995
  }
1875
1996
  async function getCurrentBranch() {
1876
1997
  try {
1877
- const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD");
1998
+ const { stdout } = await execAsync2("git rev-parse --abbrev-ref HEAD");
1878
1999
  return stdout.trim() || null;
1879
2000
  } catch {
1880
2001
  return null;
@@ -1890,7 +2011,7 @@ function parseRequirementFromBranch(branchName) {
1890
2011
  }
1891
2012
  async function getGitRoot() {
1892
2013
  try {
1893
- const { stdout } = await execAsync("git rev-parse --show-toplevel");
2014
+ const { stdout } = await execAsync2("git rev-parse --show-toplevel");
1894
2015
  return stdout.trim() || null;
1895
2016
  } catch {
1896
2017
  return null;
@@ -1900,12 +2021,12 @@ async function getGitUser() {
1900
2021
  let name = null;
1901
2022
  let email = null;
1902
2023
  try {
1903
- const { stdout: nameStdout } = await execAsync("git config --get user.name");
2024
+ const { stdout: nameStdout } = await execAsync2("git config --get user.name");
1904
2025
  name = nameStdout.trim() || null;
1905
2026
  } catch {
1906
2027
  }
1907
2028
  try {
1908
- const { stdout: emailStdout } = await execAsync("git config --get user.email");
2029
+ const { stdout: emailStdout } = await execAsync2("git config --get user.email");
1909
2030
  email = emailStdout.trim() || null;
1910
2031
  } catch {
1911
2032
  }
@@ -3828,6 +3949,31 @@ var RequirementService = class {
3828
3949
  const response = await this.axios.post(url, data, { headers });
3829
3950
  return response.data;
3830
3951
  }
3952
+ async createGitBranch(projectId, requirementId, data) {
3953
+ const url = `${this.baseUrl}/api/v1/projects/${projectId}/requirements/${requirementId}/create-git-branch`;
3954
+ const headers = this.getHeaders();
3955
+ const response = await this.axios.post(url, data, { headers });
3956
+ return response.data;
3957
+ }
3958
+ async reviewAcceptance(projectId, requirementId, data, onChunk) {
3959
+ const url = `${this.baseUrl}/api/v1/projects/${projectId}/requirements/${requirementId}/review/acceptance`;
3960
+ const headers = this.getHeaders();
3961
+ const response = await this.axios.post(url, data, {
3962
+ headers,
3963
+ responseType: "stream"
3964
+ });
3965
+ return new Promise((resolve, reject) => {
3966
+ response.data.on("data", (chunk) => {
3967
+ onChunk(chunk.toString());
3968
+ });
3969
+ response.data.on("end", () => {
3970
+ resolve();
3971
+ });
3972
+ response.data.on("error", (error) => {
3973
+ reject(error);
3974
+ });
3975
+ });
3976
+ }
3831
3977
  };
3832
3978
 
3833
3979
  // src/handlers/requirement.handlers.ts
@@ -4464,6 +4610,190 @@ async function handleRequirementBuild(opts) {
4464
4610
  };
4465
4611
  }
4466
4612
  }
4613
+ function slugify(text) {
4614
+ return text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 40);
4615
+ }
4616
+ async function handleCreateGitBranch(opts) {
4617
+ let stopSpinner = null;
4618
+ try {
4619
+ const { requirementService, auth } = getServices2();
4620
+ const isAuthenticated = await auth.isAuthenticated();
4621
+ if (!isAuthenticated) {
4622
+ return {
4623
+ success: false,
4624
+ message: chalk7.red("\u274C Not authenticated. Please run `braingrid login` first.")
4625
+ };
4626
+ }
4627
+ const format = opts.format || "table";
4628
+ if (!["table", "json", "markdown"].includes(format)) {
4629
+ return {
4630
+ success: false,
4631
+ message: chalk7.red(
4632
+ `\u274C Invalid format: ${format}. Supported formats: table, json, markdown`
4633
+ )
4634
+ };
4635
+ }
4636
+ const requirementResult = await workspaceManager.getRequirement(opts.id);
4637
+ if (!requirementResult.success) {
4638
+ return {
4639
+ success: false,
4640
+ message: requirementResult.error
4641
+ };
4642
+ }
4643
+ const requirementId = requirementResult.requirementId;
4644
+ const workspace = await workspaceManager.getProject(opts.project);
4645
+ if (!workspace.success) {
4646
+ return {
4647
+ success: false,
4648
+ message: workspace.error
4649
+ };
4650
+ }
4651
+ const projectId = workspace.projectId;
4652
+ const normalizedId = normalizeRequirementId(requirementId);
4653
+ let branchName = opts.name;
4654
+ if (!branchName) {
4655
+ const user = await auth.getCurrentUser();
4656
+ if (!user) {
4657
+ return {
4658
+ success: false,
4659
+ message: chalk7.red("\u274C Could not get current user for branch name generation")
4660
+ };
4661
+ }
4662
+ const requirement2 = await requirementService.getProjectRequirement(projectId, normalizedId);
4663
+ const username = slugify(user.email.split("@")[0]);
4664
+ const reqShortId = requirement2.short_id || normalizedId;
4665
+ const sluggedName = slugify(requirement2.name);
4666
+ branchName = `${username}/${reqShortId}-${sluggedName}`;
4667
+ }
4668
+ stopSpinner = showSpinner("Creating GitHub branch...", chalk7.gray);
4669
+ const response = await requirementService.createGitBranch(projectId, normalizedId, {
4670
+ branchName,
4671
+ baseBranch: opts.base
4672
+ });
4673
+ stopSpinner();
4674
+ stopSpinner = null;
4675
+ let output;
4676
+ switch (format) {
4677
+ case "json": {
4678
+ output = JSON.stringify(response, null, 2);
4679
+ break;
4680
+ }
4681
+ case "markdown": {
4682
+ output = `# Branch Created
4683
+
4684
+ `;
4685
+ output += `**Branch:** \`${response.branch.name}\`
4686
+ `;
4687
+ output += `**SHA:** \`${response.branch.sha.substring(0, 7)}\`
4688
+
4689
+ `;
4690
+ output += `## Checkout Command
4691
+
4692
+ `;
4693
+ output += `\`\`\`bash
4694
+ git fetch origin && git checkout ${response.branch.name}
4695
+ \`\`\`
4696
+ `;
4697
+ break;
4698
+ }
4699
+ case "table":
4700
+ default: {
4701
+ output = chalk7.green(`\u2705 Created branch: ${response.branch.name}
4702
+
4703
+ `);
4704
+ output += `${chalk7.bold("SHA:")} ${response.branch.sha.substring(0, 7)}
4705
+ `;
4706
+ output += `
4707
+ ${chalk7.dim("To checkout:")} git fetch origin && git checkout ${response.branch.name}`;
4708
+ break;
4709
+ }
4710
+ }
4711
+ return {
4712
+ success: true,
4713
+ message: output,
4714
+ data: response
4715
+ };
4716
+ } catch (error) {
4717
+ if (stopSpinner) {
4718
+ stopSpinner();
4719
+ }
4720
+ return {
4721
+ success: false,
4722
+ message: formatError(error, getResourceContext("requirement", opts.id || "unknown"))
4723
+ };
4724
+ }
4725
+ }
4726
+ async function handleReviewAcceptance(opts) {
4727
+ try {
4728
+ const { requirementService, auth } = getServices2();
4729
+ const isAuthenticated = await auth.isAuthenticated();
4730
+ if (!isAuthenticated) {
4731
+ return {
4732
+ success: false,
4733
+ message: chalk7.red("\u274C Not authenticated. Please run `braingrid login` first.")
4734
+ };
4735
+ }
4736
+ const requirementResult = await workspaceManager.getRequirement(opts.id);
4737
+ if (!requirementResult.success) {
4738
+ return {
4739
+ success: false,
4740
+ message: requirementResult.error
4741
+ };
4742
+ }
4743
+ const requirementId = requirementResult.requirementId;
4744
+ const workspace = await workspaceManager.getProject(opts.project);
4745
+ if (!workspace.success) {
4746
+ return {
4747
+ success: false,
4748
+ message: workspace.error
4749
+ };
4750
+ }
4751
+ const projectId = workspace.projectId;
4752
+ const normalizedId = normalizeRequirementId(requirementId);
4753
+ let prNumber = opts.pr;
4754
+ if (!prNumber) {
4755
+ const { execAsync: execAsync5 } = await import("./command-execution-7JMH2DK4.js");
4756
+ try {
4757
+ const { stdout } = await execAsync5("gh pr view --json number -q .number");
4758
+ const detectedPrNumber = stdout.trim();
4759
+ if (!detectedPrNumber) {
4760
+ throw new Error("No PR number returned from gh CLI");
4761
+ }
4762
+ prNumber = detectedPrNumber;
4763
+ } catch {
4764
+ return {
4765
+ success: false,
4766
+ message: chalk7.red(
4767
+ "\u274C No PR specified and could not detect PR for current branch.\n Use --pr <number> or ensure you have an open PR for this branch."
4768
+ )
4769
+ };
4770
+ }
4771
+ }
4772
+ console.log(chalk7.bold("\n\u{1F50D} AI Code Review\n"));
4773
+ let fullOutput = "";
4774
+ await requirementService.reviewAcceptance(
4775
+ projectId,
4776
+ normalizedId,
4777
+ { pullRequest: prNumber },
4778
+ (chunk) => {
4779
+ process.stdout.write(chunk);
4780
+ fullOutput += chunk;
4781
+ }
4782
+ );
4783
+ console.log("\n");
4784
+ return {
4785
+ success: true,
4786
+ message: "",
4787
+ // Already printed via streaming
4788
+ data: { review: fullOutput }
4789
+ };
4790
+ } catch (error) {
4791
+ return {
4792
+ success: false,
4793
+ message: formatError(error, getResourceContext("requirement", opts.id || "unknown"))
4794
+ };
4795
+ }
4796
+ }
4467
4797
 
4468
4798
  // src/handlers/task.handlers.ts
4469
4799
  import chalk8 from "chalk";
@@ -4512,6 +4842,12 @@ var TaskService = class {
4512
4842
  const headers = this.getHeaders();
4513
4843
  await this.axios.delete(url, { headers });
4514
4844
  }
4845
+ async specifyTask(projectId, requirementId, data) {
4846
+ const url = `${this.baseUrl}/api/v1/projects/${projectId}/requirements/${requirementId}/tasks/specify`;
4847
+ const headers = this.getHeaders();
4848
+ const response = await this.axios.post(url, data, { headers });
4849
+ return response.data;
4850
+ }
4515
4851
  };
4516
4852
 
4517
4853
  // src/handlers/task.handlers.ts
@@ -4977,6 +5313,117 @@ async function handleTaskDelete(id, opts) {
4977
5313
  };
4978
5314
  }
4979
5315
  }
5316
+ async function handleTaskSpecify(opts) {
5317
+ let stopSpinner = null;
5318
+ try {
5319
+ const { taskService, auth } = getServices3();
5320
+ const isAuthenticated = await auth.isAuthenticated();
5321
+ if (!isAuthenticated) {
5322
+ return {
5323
+ success: false,
5324
+ message: chalk8.red("\u274C Not authenticated. Please run `braingrid login` first.")
5325
+ };
5326
+ }
5327
+ const format = opts.format || "table";
5328
+ if (!["table", "json", "markdown"].includes(format)) {
5329
+ return {
5330
+ success: false,
5331
+ message: chalk8.red(
5332
+ `\u274C Invalid format: ${format}. Supported formats: table, json, markdown`
5333
+ )
5334
+ };
5335
+ }
5336
+ const workspace = await workspaceManager.getProject(opts.project);
5337
+ if (!workspace.success) {
5338
+ return {
5339
+ success: false,
5340
+ message: workspace.error
5341
+ };
5342
+ }
5343
+ const projectId = workspace.projectId;
5344
+ const requirementResult = await workspaceManager.getRequirement(opts.requirement);
5345
+ if (!requirementResult.success) {
5346
+ return {
5347
+ success: false,
5348
+ message: requirementResult.error
5349
+ };
5350
+ }
5351
+ const requirementId = requirementResult.requirementId;
5352
+ if (opts.prompt.length < 10) {
5353
+ return {
5354
+ success: false,
5355
+ message: chalk8.red("\u274C Prompt must be at least 10 characters long")
5356
+ };
5357
+ }
5358
+ if (opts.prompt.length > 5e3) {
5359
+ return {
5360
+ success: false,
5361
+ message: chalk8.red("\u274C Prompt must be no more than 5000 characters long")
5362
+ };
5363
+ }
5364
+ stopSpinner = showSpinner("Creating task from prompt...", chalk8.gray);
5365
+ const response = await taskService.specifyTask(projectId, requirementId, {
5366
+ prompt: opts.prompt
5367
+ });
5368
+ stopSpinner();
5369
+ stopSpinner = null;
5370
+ const config = getConfig();
5371
+ let output;
5372
+ switch (format) {
5373
+ case "json": {
5374
+ output = JSON.stringify(response, null, 2);
5375
+ break;
5376
+ }
5377
+ case "markdown": {
5378
+ output = formatTaskSpecifyMarkdown(response, config.apiUrl);
5379
+ break;
5380
+ }
5381
+ case "table":
5382
+ default: {
5383
+ output = formatTaskSpecifyTable(response);
5384
+ break;
5385
+ }
5386
+ }
5387
+ return {
5388
+ success: true,
5389
+ message: output,
5390
+ data: response
5391
+ };
5392
+ } catch (error) {
5393
+ if (stopSpinner) {
5394
+ stopSpinner();
5395
+ }
5396
+ return {
5397
+ success: false,
5398
+ message: formatError(error, "specifying task")
5399
+ };
5400
+ }
5401
+ }
5402
+ function formatTaskSpecifyTable(response) {
5403
+ const task2 = response.task;
5404
+ const lines = [];
5405
+ lines.push(chalk8.green(`\u2705 Created task ${response.requirement_short_id}/TASK-${task2.number}`));
5406
+ lines.push("");
5407
+ lines.push(`${chalk8.bold("Title:")} ${task2.title}`);
5408
+ lines.push(`${chalk8.bold("Status:")} ${task2.status}`);
5409
+ return lines.join("\n");
5410
+ }
5411
+ function formatTaskSpecifyMarkdown(response, _apiUrl) {
5412
+ const task2 = response.task;
5413
+ const lines = [];
5414
+ lines.push(`# Task ${response.requirement_short_id}/TASK-${task2.number}: ${task2.title}`);
5415
+ lines.push("");
5416
+ lines.push(`**Status:** ${task2.status}`);
5417
+ lines.push(`**Requirement:** ${response.requirement_short_id}`);
5418
+ lines.push(`**Project:** ${response.project_short_id}`);
5419
+ lines.push("");
5420
+ if (task2.content) {
5421
+ lines.push("## Content");
5422
+ lines.push("");
5423
+ lines.push(task2.content);
5424
+ }
5425
+ return lines.join("\n");
5426
+ }
4980
5427
 
4981
5428
  // src/handlers/auth.handlers.ts
4982
5429
  import chalk9 from "chalk";
@@ -4988,6 +5435,14 @@ function getAuth() {
4988
5435
  async function handleLogin() {
4989
5436
  try {
4990
5437
  const auth = getAuth();
5438
+ if (BRAINGRID_API_TOKEN) {
5439
+ return {
5440
+ success: true,
5441
+ message: chalk9.blue(
5442
+ "\u2139\uFE0F Using BRAINGRID_API_TOKEN - already authenticated via sandbox environment."
5443
+ )
5444
+ };
5445
+ }
4991
5446
  console.log(chalk9.blue("\u{1F510} Starting OAuth2 authentication flow..."));
4992
5447
  console.log(chalk9.dim("Your browser will open to complete authentication.\n"));
4993
5448
  const gitUser = await getGitUser();
@@ -5021,6 +5476,12 @@ async function handleLogin() {
5021
5476
  async function handleLogout() {
5022
5477
  try {
5023
5478
  const auth = getAuth();
5479
+ if (BRAINGRID_API_TOKEN) {
5480
+ return {
5481
+ success: true,
5482
+ message: chalk9.blue("\u2139\uFE0F Session managed by sandbox environment - logout not required.")
5483
+ };
5484
+ }
5024
5485
  await auth.clearSession();
5025
5486
  return {
5026
5487
  success: true,
@@ -5038,6 +5499,12 @@ async function handleWhoami() {
5038
5499
  const auth = getAuth();
5039
5500
  const isAuthenticated = await auth.isAuthenticated();
5040
5501
  if (!isAuthenticated) {
5502
+ if (BRAINGRID_API_TOKEN) {
5503
+ return {
5504
+ success: false,
5505
+ message: chalk9.red(auth.getSandboxExpiredMessage())
5506
+ };
5507
+ }
5041
5508
  return {
5042
5509
  success: false,
5043
5510
  message: chalk9.yellow("\u26A0\uFE0F Not logged in. Run `braingrid login` to authenticate.")
@@ -5060,8 +5527,13 @@ async function handleWhoami() {
5060
5527
  `;
5061
5528
  output += `${chalk9.bold("Org ID:")} ${session.organization_id}
5062
5529
  `;
5063
- output += `${chalk9.bold("Session:")} ${new Date(session.created_at).toLocaleString()}
5530
+ if (BRAINGRID_API_TOKEN) {
5531
+ output += `${chalk9.bold("Auth:")} ${chalk9.cyan("Sandbox API Token")}
5532
+ `;
5533
+ } else {
5534
+ output += `${chalk9.bold("Session:")} ${new Date(session.created_at).toLocaleString()}
5064
5535
  `;
5536
+ }
5065
5537
  return {
5066
5538
  success: true,
5067
5539
  message: output,
@@ -5332,7 +5804,7 @@ var GitHubService = class {
5332
5804
  import { exec as exec2 } from "child_process";
5333
5805
  import { promisify as promisify2 } from "util";
5334
5806
  import chalk11 from "chalk";
5335
- var execAsync2 = promisify2(exec2);
5807
+ var execAsync3 = promisify2(exec2);
5336
5808
  async function isGitInstalled() {
5337
5809
  return isCliInstalled("git");
5338
5810
  }
@@ -5345,7 +5817,7 @@ async function getGitVersion() {
5345
5817
  async function installViaHomebrew() {
5346
5818
  console.log(chalk11.blue("\u{1F4E6} Installing Git via Homebrew..."));
5347
5819
  try {
5348
- await execAsync2("brew install git", {
5820
+ await execAsync3("brew install git", {
5349
5821
  timeout: 3e5
5350
5822
  // 5 minutes
5351
5823
  });
@@ -5373,7 +5845,7 @@ async function installViaXcodeSelect() {
5373
5845
  console.log(chalk11.blue("\u{1F4E6} Installing Git via Xcode Command Line Tools..."));
5374
5846
  console.log(chalk11.dim('A system dialog will appear - click "Install" to continue.\n'));
5375
5847
  try {
5376
- await execAsync2("xcode-select --install", {
5848
+ await execAsync3("xcode-select --install", {
5377
5849
  timeout: 6e5
5378
5850
  // 10 minutes (user interaction required)
5379
5851
  });
@@ -5414,7 +5886,7 @@ async function installGitWindows() {
5414
5886
  }
5415
5887
  console.log(chalk11.blue("\u{1F4E6} Installing Git via winget..."));
5416
5888
  try {
5417
- await execAsync2("winget install --id Git.Git -e --source winget --silent", {
5889
+ await execAsync3("winget install --id Git.Git -e --source winget --silent", {
5418
5890
  timeout: 3e5
5419
5891
  // 5 minutes
5420
5892
  });
@@ -5469,7 +5941,7 @@ async function installGitLinux() {
5469
5941
  message: chalk11.red(`\u274C Unsupported package manager: ${packageManager.name}`)
5470
5942
  };
5471
5943
  }
5472
- await execAsync2(installCommand, {
5944
+ await execAsync3(installCommand, {
5473
5945
  timeout: 3e5
5474
5946
  // 5 minutes
5475
5947
  });
@@ -5671,28 +6143,6 @@ import { select as select2 } from "@inquirer/prompts";
5671
6143
  import * as path4 from "path";
5672
6144
  import * as fs4 from "fs/promises";
5673
6145
 
5674
- // src/utils/command-execution.ts
5675
- import { exec as exec3, spawn } from "child_process";
5676
- import { promisify as promisify3 } from "util";
5677
- var execAsyncReal = promisify3(exec3);
5678
- async function execAsync3(command, options, isTestMode = false, mockExecHandler) {
5679
- if (isTestMode && mockExecHandler) {
5680
- return mockExecHandler(command);
5681
- }
5682
- const defaultOptions = {
5683
- maxBuffer: 1024 * 1024 * 10,
5684
- // 10MB default
5685
- timeout: 3e5,
5686
- // 5 minutes
5687
- ...options
5688
- };
5689
- if (command.includes("claude")) {
5690
- defaultOptions.maxBuffer = 1024 * 1024 * 50;
5691
- defaultOptions.timeout = 6e5;
5692
- }
5693
- return execAsyncReal(command, defaultOptions);
5694
- }
5695
-
5696
6146
  // src/services/setup-service.ts
5697
6147
  import * as fs3 from "fs/promises";
5698
6148
  import * as path3 from "path";
@@ -5743,7 +6193,7 @@ async function fetchFileFromGitHub(path6) {
5743
6193
  return withRetry(async () => {
5744
6194
  try {
5745
6195
  const command = `gh api repos/${GITHUB_OWNER}/${GITHUB_REPO}/contents/${path6}`;
5746
- const { stdout } = await execAsync3(command);
6196
+ const { stdout } = await execAsync(command);
5747
6197
  const response = JSON.parse(stdout);
5748
6198
  if (response.type !== "file") {
5749
6199
  throw new Error(`Path ${path6} is not a file`);
@@ -5766,7 +6216,7 @@ async function listGitHubDirectory(path6) {
5766
6216
  return withRetry(async () => {
5767
6217
  try {
5768
6218
  const command = `gh api repos/${GITHUB_OWNER}/${GITHUB_REPO}/contents/${path6}`;
5769
- const { stdout } = await execAsync3(command);
6219
+ const { stdout } = await execAsync(command);
5770
6220
  const response = JSON.parse(stdout);
5771
6221
  if (!Array.isArray(response)) {
5772
6222
  throw new Error(`Path ${path6} is not a directory`);
@@ -5899,7 +6349,7 @@ async function fileExists(filePath) {
5899
6349
  }
5900
6350
  async function checkPrerequisites() {
5901
6351
  try {
5902
- await execAsync3("gh --version");
6352
+ await execAsync("gh --version");
5903
6353
  } catch {
5904
6354
  return {
5905
6355
  success: false,
@@ -5907,7 +6357,7 @@ async function checkPrerequisites() {
5907
6357
  };
5908
6358
  }
5909
6359
  try {
5910
- await execAsync3("gh auth status");
6360
+ await execAsync("gh auth status");
5911
6361
  } catch {
5912
6362
  return {
5913
6363
  success: false,
@@ -6281,10 +6731,10 @@ async function checkAndShowUpdateWarning() {
6281
6731
  import { access as access3 } from "fs/promises";
6282
6732
 
6283
6733
  // src/utils/github-repo.ts
6284
- import { exec as exec4 } from "child_process";
6285
- import { promisify as promisify4 } from "util";
6734
+ import { exec as exec3 } from "child_process";
6735
+ import { promisify as promisify3 } from "util";
6286
6736
  import path5 from "path";
6287
- var execAsync4 = promisify4(exec4);
6737
+ var execAsync4 = promisify3(exec3);
6288
6738
  async function initGitRepo() {
6289
6739
  try {
6290
6740
  await execAsync4("git init");
@@ -7293,6 +7743,30 @@ requirement.command("build [id]").description(
7293
7743
  process.exit(1);
7294
7744
  }
7295
7745
  });
7746
+ requirement.command("create-branch [id]").description(
7747
+ "Create a GitHub branch for a requirement (auto-detects ID from git branch if not provided)"
7748
+ ).option(
7749
+ "-p, --project <id>",
7750
+ "project ID (auto-detects from .braingrid/project.json if not provided)"
7751
+ ).option("--name <name>", "branch name (auto-generates {username}/REQ-123-slug if not provided)").option("--base <branch>", "base branch to create from (defaults to repository default branch)").option("--format <format>", "output format (table, json, markdown)", "table").action(async (id, opts) => {
7752
+ const result = await handleCreateGitBranch({ ...opts, id });
7753
+ console.log(result.message);
7754
+ if (!result.success) {
7755
+ process.exit(1);
7756
+ }
7757
+ });
7758
+ requirement.command("review [id]").description("Run AI code review for a requirement PR (auto-detects ID and PR from git branch)").option(
7759
+ "-p, --project <id>",
7760
+ "project ID (auto-detects from .braingrid/project.json if not provided)"
7761
+ ).option("--pr <number>", "PR number or URL (auto-detects from current branch if not provided)").action(async (id, opts) => {
7762
+ const result = await handleReviewAcceptance({ ...opts, id });
7763
+ if (result.message) {
7764
+ console.log(result.message);
7765
+ }
7766
+ if (!result.success) {
7767
+ process.exit(1);
7768
+ }
7769
+ });
7296
7770
  var task = program.command("task").description("Manage tasks");
7297
7771
  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) => {
7298
7772
  const result = await handleTaskList(opts);
@@ -7336,6 +7810,16 @@ task.command("delete <id>").description("Delete a task").option("-r, --requireme
7336
7810
  process.exit(1);
7337
7811
  }
7338
7812
  });
7813
+ task.command("specify").description("Create a single task from a prompt using AI").option(
7814
+ "-r, --requirement <id>",
7815
+ "requirement ID (REQ-456, auto-detects from git branch if not provided)"
7816
+ ).option("-p, --project <id>", "project ID (PROJ-123, optional if project is initialized)").requiredOption("--prompt <prompt>", "task description (10-5000 characters)").option("--format <format>", "output format (table, json, markdown)", "table").action(async (opts) => {
7817
+ const result = await handleTaskSpecify(opts);
7818
+ console.log(result.message);
7819
+ if (!result.success) {
7820
+ process.exit(1);
7821
+ }
7822
+ });
7339
7823
  program.command("completion [shell]").description("Generate shell completion script (bash, zsh)").option("--setup", "automatically install completion for current shell").option("-s, --shell <shell>", "shell type (bash, zsh)").action(async (shell, opts) => {
7340
7824
  const result = await handleCompletion(shell, opts);
7341
7825
  console.log(result.message);