bdy 1.23.15-dev-target-commands → 1.23.16-dev-target-commands

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bdy",
3
3
  "preferGlobal": false,
4
- "version": "1.23.15-dev-target-commands",
4
+ "version": "1.23.16-dev-target-commands",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
7
7
  "homepage": "https://buddy.works/docs/cli",
@@ -161,6 +161,10 @@ class AgentManagerClass {
161
161
  }
162
162
  const current = (0, utils_1.getVersion)();
163
163
  const latest = await (0, utils_1.getLatestVersion)();
164
+ // Equality only, deliberately not "is latest newer": this runs on an explicit remote
165
+ // update action, whose meaning is "match what the channel publishes". Refusing an
166
+ // older latest would take away the only rollback the fleet has - moving the channel
167
+ // pointer back after a bad release.
164
168
  if (!latest || latest === '0.0.0' || latest === current) {
165
169
  logger_1.default.info((0, texts_1.LOG_AGENT_UPDATE_NOT_NEEDED)(current));
166
170
  // Nothing to do is not a failure, and showing it as one would make an update of
@@ -174,12 +178,15 @@ class AgentManagerClass {
174
178
  // - this is the value that stays behind if the restart never lands.
175
179
  this.agent?.reportUpdateStatus(tunnel_1.TUNNEL_AGENT_UPDATE_STATUS.STARTED);
176
180
  this.agent?.flushUpdateStatus();
177
- const target = latest.match(/^\d+\.\d+\.\d+/);
181
+ // getLatestVersion already refuses a version it cannot parse, so this is belt and
182
+ // braces - kept because the download url is built from the triple, and shipping a
183
+ // malformed one would fail much further away from the cause.
184
+ const target = (0, utils_1.parseVersion)(latest);
178
185
  if (!target) {
179
186
  logger_1.default.error(texts_1.LOG_AGENT_UPDATE_FAILED);
180
187
  return report(tunnel_1.TUNNEL_AGENT_UPDATE_STATUS.FAILED);
181
188
  }
182
- const swapped = await this.system.swapBinaryToVersion(target[0]);
189
+ const swapped = await this.system.swapBinaryToVersion(target.version);
183
190
  if (!swapped) {
184
191
  logger_1.default.error((0, texts_1.LOG_AGENT_UPDATE_BINARY_INVALID)(latest));
185
192
  return report(tunnel_1.TUNNEL_AGENT_UPDATE_STATUS.FAILED);
@@ -249,6 +249,12 @@ class ApiClient {
249
249
  }
250
250
  }
251
251
  else if (status === 204) {
252
+ // A no-content success still honors the raw-output contracts: callers
253
+ // stream the (empty) body instead of tripping over a null.
254
+ if (rawResponse)
255
+ return response;
256
+ if (rawResponseBody)
257
+ return responseBody;
252
258
  await responseBody.dump();
253
259
  return null;
254
260
  }
@@ -1247,11 +1253,20 @@ class ApiClient {
1247
1253
  query += '&';
1248
1254
  query += encodeURIComponent(key) + '=' + encodeURIComponent(value);
1249
1255
  });
1250
- return await this.request({
1256
+ const resolved = await this.request({
1251
1257
  method: 'GET',
1252
1258
  path: `/workspaces/${encodeURIComponent(workspace)}/identifiers${query}`,
1253
1259
  parseResponseBody: true,
1254
1260
  });
1261
+ // A queried project must itself resolve: for some resources
1262
+ // (environments, artifacts, distributions) the endpoint otherwise falls
1263
+ // back to the workspace resource of the same identifier, silently
1264
+ // answering for a different scope than asked (TARGET_API_REQUIREMENTS.md
1265
+ // p6). project_identifier is present whenever the project exists.
1266
+ if (params.project && !resolved.project_identifier) {
1267
+ throw new Error(texts_1.ERR_PROJECT_NOT_FOUND);
1268
+ }
1269
+ return resolved;
1255
1270
  }
1256
1271
  async resolveIdentifiers(workspace, params) {
1257
1272
  let query = '';
@@ -159,14 +159,15 @@ const request = async (method, url, options) => {
159
159
  output_1.default.exitNormal('');
160
160
  }
161
161
  else {
162
- // setEncoding keeps multibyte characters intact across chunk boundaries,
163
- // and newLine=false keeps chunk boundaries out of the payload
162
+ // setEncoding keeps multibyte characters intact across chunk boundaries;
163
+ // the response is a 1:1 payload, so it bypasses terminal-kit (whose
164
+ // formatting corrupts % and ^ sequences, common in URL-encoded data)
164
165
  body.setEncoding('utf8');
165
166
  body.on('data', (data) => {
166
- output_1.default.normal(data, false);
167
+ output_1.default.data(data, false);
167
168
  });
168
169
  body.on('close', () => {
169
- output_1.default.normal('');
170
+ output_1.default.data('');
170
171
  output_1.default.exitNormal('');
171
172
  });
172
173
  }
@@ -18,7 +18,8 @@ commandEnvironmentDelete.addHelpText('after', `\nEXAMPLES:${texts_1.EXAMPLE_ENVI
18
18
  commandEnvironmentDelete.action(async (identifier, options) => {
19
19
  const workspace = input_1.default.restApiWorkspace(options.workspace);
20
20
  const client = input_1.default.restApiTokenClient();
21
- const environment_id = await (0, resolve_1.resolveEnvironmentId)(client, workspace, identifier, options);
21
+ const environment_id = await (0, resolve_1.resolveEnvironmentId)(client, workspace, identifier, options,
22
+ /* strictProject */ true);
22
23
  const confirmed = options.force ||
23
24
  (await output_1.default.confirm((0, texts_1.TXT_ENVIRONMENT_DELETE_CONFIRM)(identifier)));
24
25
  if (!confirmed)
@@ -11,19 +11,29 @@ const input_1 = __importDefault(require("../../input"));
11
11
  // project context from flags/env -> /identifiers -> hash id, exiting on a
12
12
  // miss. Without a project the identifier resolves as a workspace
13
13
  // environment; with one, along the project line.
14
- const resolveEnvironmentId = async (client, workspace, identifier, options) => {
14
+ //
15
+ // strictProject is for mutating commands (update/delete): an explicit -p is
16
+ // a statement "the environment of this project", so the resolved environment
17
+ // must actually live there. Without it /identifiers silently falls back to
18
+ // the workspace environment of the same identifier and the write lands on a
19
+ // workspace-wide resource. Reads keep the fallback - that is how workspace
20
+ // environments stay addressable from inside a project context.
21
+ const resolveEnvironmentId = async (client, workspace, identifier, options, strictProject = false) => {
15
22
  const project = input_1.default.restApiProject(options.project, true);
16
23
  const resolved = await client.getEnvironmentByIdentifier(workspace, identifier, project);
17
- // /identifiers falls back to the workspace environment even when the
18
- // requested project does not exist - deadly for delete/update, which
19
- // would then hit the workspace resource. A provided project must itself
20
- // resolve.
21
- if (project && !resolved.project_identifier) {
22
- output_1.default.exitError(texts_1.ERR_PROJECT_NOT_FOUND);
23
- }
24
+ // A nonexistent project is rejected by the API client
25
+ // (getResourceByIdentifier); here only the environment itself can miss.
24
26
  if (!resolved.environment_id) {
25
27
  output_1.default.exitError(texts_1.ERR_ENVIRONMENT_NOT_FOUND);
26
28
  }
29
+ if (strictProject && options.project) {
30
+ const environment = await client.getEnvironment(workspace, resolved.environment_id);
31
+ // a workspace environment carries no project, so this also fails the
32
+ // workspace-fallback case
33
+ if (environment.project?.name !== resolved.project_identifier) {
34
+ output_1.default.exitError(texts_1.ERR_ENVIRONMENT_NOT_FOUND);
35
+ }
36
+ }
27
37
  return resolved.environment_id;
28
38
  };
29
39
  exports.resolveEnvironmentId = resolveEnvironmentId;
@@ -52,7 +52,8 @@ commandEnvironmentUpdate.action(async (identifier, options) => {
52
52
  output_1.default.exitError(texts_1.ERR_COMMAND_NO_UPDATE);
53
53
  }
54
54
  const client = input_1.default.restApiTokenClient();
55
- const environment_id = await (0, resolve_1.resolveEnvironmentId)(client, workspace, identifier, options);
55
+ const environment_id = await (0, resolve_1.resolveEnvironmentId)(client, workspace, identifier, options,
56
+ /* strictProject */ true);
56
57
  const result = await client.updateEnvironment(workspace, environment_id, body);
57
58
  output_1.default.exitSuccess((0, texts_1.TXT_ENVIRONMENT_UPDATED)(result.identifier, result.html_url));
58
59
  });
@@ -46,14 +46,15 @@ const commandPre = async (_, command) => {
46
46
  if (!output_1.default.isTTY() || command.hideVersionUpdate || ['json', 'jsonl'].includes(command.opts()?.format)) {
47
47
  return;
48
48
  }
49
- const newCli = !!command.latestVersion && command.currentVersion !== command.latestVersion;
50
- const newAgent = !!command.agentStatus && !!command.latestVersion &&
51
- command.agentStatus.version !== command.latestVersion;
52
- if ((0, utils_1.isDocker)() && newCli && command.latestVersion) {
53
- output_1.default.newCliDockerVersion(command.latestVersion);
54
- }
55
- else if (newCli && command.latestVersion) {
56
- output_1.default.newCliVersion(command.latestVersion);
49
+ // No truthiness guards on the versions: an empty or unparsable one is never newer
50
+ const latest = command.latestVersion || '';
51
+ const newCli = (0, utils_1.isVersionNewer)(latest, command.currentVersion || '');
52
+ const newAgent = !!command.agentStatus && (0, utils_1.isVersionNewer)(latest, command.agentStatus.version);
53
+ if ((0, utils_1.isDocker)() && newCli) {
54
+ output_1.default.newCliDockerVersion(latest);
55
+ }
56
+ else if (newCli) {
57
+ output_1.default.newCliVersion(latest);
57
58
  }
58
59
  else if (newAgent) {
59
60
  output_1.default.newAgentVersion();
@@ -60,10 +60,12 @@ commandProjectGitCredential.action(async (action) => {
60
60
  projects.projects[0].html_url.includes(input.host);
61
61
  }
62
62
  if (myHost) {
63
- output_1.default.normal(`protocol=${input.protocol || 'https'}`);
64
- output_1.default.normal(`host=${input.host}`);
65
- output_1.default.normal(`username=${client.token}`);
66
- output_1.default.normal(`password=`);
63
+ // git credential protocol output - a machine-read payload, so it
64
+ // bypasses terminal-kit formatting
65
+ output_1.default.data(`protocol=${input.protocol || 'https'}`);
66
+ output_1.default.data(`host=${input.host}`);
67
+ output_1.default.data(`username=${client.token}`);
68
+ output_1.default.data(`password=`);
67
69
  }
68
70
  }
69
71
  catch {
@@ -43,7 +43,7 @@ commandSandboxExecCommand.action(async (identifier, command, options) => {
43
43
  str.forEach((s) => {
44
44
  const json = JSON.parse(s);
45
45
  if (json.data)
46
- output_1.default.normal(json.data);
46
+ output_1.default.data(json.data);
47
47
  });
48
48
  }
49
49
  catch {
@@ -31,7 +31,7 @@ commandSandboxExecLogs.action(async (identifier, commandId, options) => {
31
31
  str.forEach((s) => {
32
32
  const json = JSON.parse(s);
33
33
  if (json.data)
34
- output_1.default.normal(json.data);
34
+ output_1.default.data(json.data);
35
35
  });
36
36
  }
37
37
  catch {
@@ -20,7 +20,8 @@ commandTargetDelete.addHelpText('after', `\nEXAMPLES:${texts_1.EXAMPLE_TARGET_DE
20
20
  commandTargetDelete.action(async (identifier, options) => {
21
21
  const workspace = input_1.default.restApiWorkspace(options.workspace);
22
22
  const client = input_1.default.restApiTokenClient();
23
- const target_id = await (0, scope_1.resolveTargetId)(client, workspace, identifier, options);
23
+ const target_id = await (0, scope_1.resolveTargetId)(client, workspace, identifier, options,
24
+ /* strictLine */ true);
24
25
  const confirmed = options.force || (await output_1.default.confirm((0, texts_1.TXT_TARGET_DELETE_CONFIRM)(identifier)));
25
26
  if (!confirmed)
26
27
  output_1.default.exitNormal();
@@ -41,7 +41,8 @@ commandTargetExecCommand.action(async (identifier, command, options) => {
41
41
  }
42
42
  const workspace = input_1.default.restApiWorkspace(options.workspace);
43
43
  const client = input_1.default.restApiTokenClient();
44
- const target_id = await (0, scope_1.resolveTargetId)(client, workspace, identifier, options);
44
+ const target_id = await (0, scope_1.resolveTargetId)(client, workspace, identifier, options,
45
+ /* strictLine */ true);
45
46
  let exec = await client.executeTargetCommand(workspace, target_id, {
46
47
  command: cmd,
47
48
  });
@@ -10,14 +10,42 @@ const output_1 = __importDefault(require("../../output"));
10
10
  const input_1 = __importDefault(require("../../input"));
11
11
  // Shared resolve preamble for commands taking a target identifier: line
12
12
  // context from flags/env -> /identifiers -> hash id, exiting on a miss.
13
- const resolveTargetId = async (client, workspace, identifier, options) => {
13
+ //
14
+ // strictLine is for mutating commands (update/delete/exec command): an
15
+ // explicit line flag is a statement "the target of this project/pipeline/
16
+ // environment", yet /identifiers falls back to the workspace target of the
17
+ // same identifier when the requested line exists but does not hold it - the
18
+ // operation would silently land on a workspace-wide resource. Reads keep
19
+ // the fallback (workspace targets stay addressable from a line context).
20
+ const resolveTargetId = async (client, workspace, identifier, options, strictLine = false) => {
14
21
  const project = input_1.default.restApiProject(options.project, true);
15
22
  const line = input_1.default.targetLine(project, options.pipeline, options.environment);
16
- const { target_id } = await client.getTargetByIdentifier(workspace, identifier, line);
17
- if (!target_id) {
23
+ const resolved = await client.getTargetByIdentifier(workspace, identifier, line);
24
+ if (!resolved.target_id) {
18
25
  output_1.default.exitError(texts_1.ERR_TARGET_NOT_FOUND);
19
26
  }
20
- return target_id;
27
+ if (strictLine) {
28
+ const requested = options.pipeline
29
+ ? utils_1.TARGET_SCOPE.PIPELINE
30
+ : options.environment
31
+ ? utils_1.TARGET_SCOPE.ENVIRONMENT
32
+ : options.project
33
+ ? utils_1.TARGET_SCOPE.PROJECT
34
+ : null;
35
+ if (requested) {
36
+ const target = await client.getTarget(workspace, resolved.target_id);
37
+ const matches = requested === utils_1.TARGET_SCOPE.PIPELINE
38
+ ? String(target.pipeline?.id) === String(resolved.pipeline_id)
39
+ : requested === utils_1.TARGET_SCOPE.ENVIRONMENT
40
+ ? target.environment?.id === resolved.environment_id
41
+ : (0, exports.targetScopeOf)(target) === utils_1.TARGET_SCOPE.PROJECT &&
42
+ target.project?.name === resolved.project_identifier;
43
+ if (!matches) {
44
+ output_1.default.exitError(texts_1.ERR_TARGET_NOT_FOUND);
45
+ }
46
+ }
47
+ }
48
+ return resolved.target_id;
21
49
  };
22
50
  exports.resolveTargetId = resolveTargetId;
23
51
  // Lines the CLI requests per log fetch.
@@ -96,12 +124,7 @@ const resolveTargetScope = async (client, workspace, project, scope, options) =>
96
124
  if (project)
97
125
  query.project = project;
98
126
  const resolved = await client.getResourceByIdentifier(workspace, query);
99
- // /identifiers falls back to the workspace environment even when the
100
- // requested project does not exist; a provided project must itself
101
- // resolve or the scope would silently point elsewhere.
102
- if (project && !resolved.project_identifier) {
103
- output_1.default.exitError(texts_1.ERR_PROJECT_NOT_FOUND);
104
- }
127
+ // a nonexistent project is rejected by getResourceByIdentifier itself
105
128
  if (!resolved.environment_id) {
106
129
  output_1.default.exitError(texts_1.ERR_TARGET_ENVIRONMENT_NOT_FOUND);
107
130
  }
@@ -21,7 +21,8 @@ commandTargetUpdate.action(async (identifier, options) => {
21
21
  const yaml = input_1.default.restApiYaml(options.yaml);
22
22
  const workspace = input_1.default.restApiWorkspace(options.workspace);
23
23
  const client = input_1.default.restApiTokenClient();
24
- const target_id = await (0, scope_1.resolveTargetId)(client, workspace, identifier, options);
24
+ const target_id = await (0, scope_1.resolveTargetId)(client, workspace, identifier, options,
25
+ /* strictLine */ true);
25
26
  const body = {
26
27
  yaml: Buffer.from(yaml, 'utf8').toString('base64'),
27
28
  };
@@ -78,7 +78,7 @@ commandVersion.action(async () => {
78
78
  const currVersion = commandVersion.currentVersion || '';
79
79
  const lastVersion = commandVersion.latestVersion || '';
80
80
  const env = commandVersion.env || '';
81
- if (currVersion !== lastVersion) {
81
+ if ((0, utils_1.isVersionNewer)(lastVersion, currVersion)) {
82
82
  const platform = (0, utils_1.getPlatform)();
83
83
  const isMac = platform === 'darwin';
84
84
  const isWin = platform === 'win32';
@@ -8,11 +8,9 @@ const texts_1 = require("../texts");
8
8
  const pipeline_1 = __importDefault(require("./yaml/pipeline"));
9
9
  const actions_1 = __importDefault(require("./yaml/actions"));
10
10
  const validate_1 = __importDefault(require("./yaml/validate"));
11
- const init_1 = __importDefault(require("./yaml/init"));
12
11
  const commandYaml = (0, utils_1.newCommand)('yaml', texts_1.DESC_COMMAND_YAML);
13
12
  commandYaml.addCommand(pipeline_1.default);
14
13
  commandYaml.addCommand(actions_1.default);
15
14
  commandYaml.addCommand(validate_1.default);
16
- commandYaml.addCommand(init_1.default);
17
- commandYaml.addHelpText('after', texts_1.EXAMPLE_YAML);
15
+ commandYaml.addHelpText('after', `\nEXAMPLES:${texts_1.EXAMPLE_YAML}`);
18
16
  exports.default = commandYaml;
@@ -16,7 +16,14 @@ process.title = 'bdy';
16
16
  for (const s of [process.stdout, process.stderr]) {
17
17
  s._handle?.setBlocking?.(true);
18
18
  }
19
+ // The cost of blocking mode: a reader that closes early (`bdy ... | head`)
20
+ // turns further writes into EPIPE errors, which surface here as uncaught
21
+ // exceptions. That is the reader saying "got enough", not a failure - end
22
+ // quietly like any well-behaved pipe writer.
23
+ const isEpipe = (err) => err?.code === 'EPIPE' || err?.cause?.code === 'EPIPE';
19
24
  process.on('uncaughtException', (err) => {
25
+ if (isEpipe(err))
26
+ process.exit(0);
20
27
  logger_1.default.fatal(err);
21
28
  output_1.default.exitError(err);
22
29
  });
@@ -1191,7 +1191,9 @@ class OutputPipeline {
1191
1191
  this.jsonLog(logs[actionLogsStart]);
1192
1192
  }
1193
1193
  else {
1194
- output_1.default.normal(output_1.default.getTermKitDimColor(` ${logs[actionLogsStart]}`));
1194
+ // log lines are payload inside markup - escape so % and ^
1195
+ // sequences in build output render verbatim
1196
+ output_1.default.normal(output_1.default.getTermKitDimColor(` ${output_1.default.escapeTermKit(logs[actionLogsStart])}`));
1195
1197
  }
1196
1198
  }
1197
1199
  if (action &&
@@ -1221,9 +1223,9 @@ class OutputPipeline {
1221
1223
  let line = '';
1222
1224
  if (i > 0)
1223
1225
  line += ' ';
1224
- line += output_1.default.getTermKitMutedColor(v.key);
1226
+ line += output_1.default.getTermKitMutedColor(output_1.default.escapeTermKit(v.key));
1225
1227
  line += output_1.default.getTermKitDimColor(` = "`);
1226
- line += output_1.default.getTermKitBlueColor(v.value);
1228
+ line += output_1.default.getTermKitBlueColor(output_1.default.escapeTermKit(v.value));
1227
1229
  line += output_1.default.getTermKitDimColor('"');
1228
1230
  output_1.default.normal(line);
1229
1231
  });
@@ -136,9 +136,18 @@ class Output {
136
136
  }
137
137
  // Payload output (command results, logs, JSON): bypasses terminal-kit,
138
138
  // whose printf-style formatting and ^-markup corrupt data containing
139
- // % or ^ sequences.
140
- static data(txt) {
141
- process.stdout.write(`${txt}\n`);
139
+ // % or ^ sequences. newLine=false emits a mid-stream chunk, keeping
140
+ // chunk boundaries out of the payload.
141
+ static data(txt, newLine = true) {
142
+ process.stdout.write(newLine ? `${txt}\n` : txt);
143
+ }
144
+ // For payload embedded in terminal-kit markup (e.g. a dimmed log line):
145
+ // neutralizes the ^-markup and %-formatting the payload would otherwise
146
+ // trigger, so it renders verbatim while the surrounding markup still
147
+ // works. Only for strings printed via terminal() - ScreenBuffer.put does
148
+ // not printf-format, so there %% would render double.
149
+ static escapeTermKit(txt) {
150
+ return txt.replace(/[%^]/g, '$&$&');
142
151
  }
143
152
  static writeStderr(txt) {
144
153
  process.stderr.write(`${txt}\n`);
@@ -37,7 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.getRootDir = exports.TARGET_ONLY_PORT_REGEX = exports.TARGET_HTTP_REGEX = exports.TARGET_TCP_TLS_REGEX = exports.ApiErrorTunnelsDisabled = exports.ApiErrorWorkspaceFlagged = exports.ApiErrorTunnelLimitReached = exports.ApiErrorAgentLimitReached = exports.ApiErrorDomainRestricted = exports.ApiErrorTargetInvalid = exports.ApiErrorWrongToken = exports.ApiErrorFailedToConnect = exports.ApiErrorAgentNotFound = exports.ARTIFACT_AUTH_TYPE = exports.TARGET_EXEC_STATUS = exports.TARGET_SCOPE = exports.ARTIFACT_TYPE = exports.SANDBOX_SNAPSHOT_STATUS = exports.SANDBOX_APP_STATUS = exports.SANDBOX_SETUP_STATUS = exports.SANDBOX_EXEC_STATUS = exports.SANDBOX_EXEC_RUNTIME = exports.SANDBOX_STATUS = exports.REST_API_ENDPOINT = exports.REST_API_REGION = exports.DEFAULT_SANDBOX_CP_USER = exports.ARTIFACT_DEFAULT_VERSION = exports.SUGGESTED_BROWSER_VERSION = exports.AGENT_UPDATE_STATUS_TTL_MS = exports.AGENT_UPDATE_PROBATION_MS = exports.AGENT_UPDATE_MAX_ATTEMPTS = exports.AGENT_FETCH_RETRY_MAX = exports.AGENT_FETCH_RETRY_MIN = exports.SSH_HANDSHAKE_TIMEOUT = exports.DEFAULT_TIMEOUT = exports.getSshIdent = exports.TUNNEL_SSH_IDENT_SHARED_H2 = exports.TUNNEL_CLIENT_IP_HEADER = exports.TUNNEL_HTTP2_SHED_LOG_INTERVAL = exports.TUNNEL_HTTP2_MAX_SESSIONS = exports.TUNNEL_HTTP2_SESSION_IDLE_TIMEOUT = exports.TUNNEL_HTTP_RETRY_AFTER = exports.TUNNEL_HTTP_MAX_INFLIGHT = exports.TUNNEL_HTTP_CB_MIN_REQUESTS = exports.TUNNEL_HTTP_CB_WINDOW = exports.TUNNEL_MAX_REQUEST_SIZE_TO_SYNC = exports.TUNNEL_HTTP_LOG_MAX_REQUESTS = exports.TUNNEL_HTTP_LOG_MAX_BODY = exports.TUNNEL_HTTP_RATE_WINDOW = exports.TUNNEL_HTTP_RATE_LIMIT = void 0;
40
- exports.getGitName = exports.getGitEmail = exports.MCP_TOOL_CATEGORIES = exports.getBasicCommandTls = exports.getBasicCommandHttp = exports.getBasicCommandSandboxEndpoint = exports.getBasicCommandTcp = exports.getCurrentUser = exports.getServiceUser = exports.getRealTargetHost = exports.isWindows = exports.isLinux = exports.isOsx = exports.isDocker = exports.truncate = exports.sleep = exports.getLatestVersion = exports.getVersionEnv = exports.getCurrentVersionWithoutEnv = exports.getVersionWithoutEnv = exports.getVersion = exports.isInstalledByChoco = exports.isInstalledByApt = exports.isInstalledByBrew = exports.isInstalledByNpm = exports.isNpmGlobalWritable = exports.isPathWritable = exports.tryGetEmail = exports.execLocally = exports.getHomeDirectory = exports.HOME_DIR_NAME = exports.chownToHomeOwner = exports.isDomainAvailable = exports.newCommand = exports.formatBytes = exports.formatHelp = exports.getPlatform = exports.getHostname = exports.isStringRegExp = exports.getWorkingDir = void 0;
40
+ exports.getGitName = exports.getGitEmail = exports.MCP_TOOL_CATEGORIES = exports.getBasicCommandTls = exports.getBasicCommandHttp = exports.getBasicCommandSandboxEndpoint = exports.getBasicCommandTcp = exports.getCurrentUser = exports.getServiceUser = exports.getRealTargetHost = exports.isWindows = exports.isLinux = exports.isOsx = exports.isDocker = exports.truncate = exports.sleep = exports.getLatestVersion = exports.getVersionEnv = exports.getCurrentVersionWithoutEnv = exports.isVersionNewer = exports.getVersionWithoutEnv = exports.parseVersion = exports.getVersion = exports.isInstalledByChoco = exports.isInstalledByApt = exports.isInstalledByBrew = exports.isInstalledByNpm = exports.isNpmGlobalWritable = exports.isPathWritable = exports.tryGetEmail = exports.execLocally = exports.getHomeDirectory = exports.HOME_DIR_NAME = exports.chownToHomeOwner = exports.isDomainAvailable = exports.newCommand = exports.formatBytes = exports.formatHelp = exports.getPlatform = exports.getHostname = exports.isStringRegExp = exports.getWorkingDir = void 0;
41
41
  exports.apiErrorCodeToClass = apiErrorCodeToClass;
42
42
  exports.isFile = isFile;
43
43
  exports.getAppWorkspaceUrl = getAppWorkspaceUrl;
@@ -599,19 +599,44 @@ const getVersion = () => {
599
599
  return cachedVersion;
600
600
  };
601
601
  exports.getVersion = getVersion;
602
+ // The one place that knows what a version string looks like: `x.y.z` optionally followed by
603
+ // a channel suffix. AgentSystem.verifyBinary keeps a regex of its own on purpose - it fishes
604
+ // a version out of selftest output rather than parsing one, so it must not be anchored.
605
+ const parseVersion = (version) => {
606
+ const m = (version || '').match(/^(\d+)\.(\d+)\.(\d+)/);
607
+ if (!m)
608
+ return null;
609
+ return {
610
+ version: m[0],
611
+ major: Number(m[1]),
612
+ minor: Number(m[2]),
613
+ patch: Number(m[3]),
614
+ };
615
+ };
616
+ exports.parseVersion = parseVersion;
602
617
  const getVersionWithoutEnv = (version) => {
603
- const m = version.match(/^\d+\.\d+\.\d+/);
604
- if (m)
605
- return m[0];
606
- return '0.0.0';
618
+ return (0, exports.parseVersion)(version)?.version || '0.0.0';
607
619
  };
608
620
  exports.getVersionWithoutEnv = getVersionWithoutEnv;
621
+ // An update is only an update when the other side is actually ahead. Comparing the strings
622
+ // for inequality also fired on a build that is newer than what the channel publishes - a
623
+ // local dev build, or a machine that got the release before es.buddy.works did - and told
624
+ // the user to "update" to an older version. Both sides carry the same env suffix (latest is
625
+ // fetched per env), so the numeric triple decides.
626
+ const isVersionNewer = (version, than) => {
627
+ const a = (0, exports.parseVersion)(version);
628
+ const b = (0, exports.parseVersion)(than);
629
+ if (!a || !b)
630
+ return false;
631
+ if (a.major !== b.major)
632
+ return a.major > b.major;
633
+ if (a.minor !== b.minor)
634
+ return a.minor > b.minor;
635
+ return a.patch > b.patch;
636
+ };
637
+ exports.isVersionNewer = isVersionNewer;
609
638
  const getCurrentVersionWithoutEnv = () => {
610
- const v = (0, exports.getVersion)();
611
- const m = v.match(/^\d+\.\d+\.\d+/);
612
- if (m)
613
- return m[0];
614
- return '0.0.0';
639
+ return (0, exports.getVersionWithoutEnv)((0, exports.getVersion)());
615
640
  };
616
641
  exports.getCurrentVersionWithoutEnv = getCurrentVersionWithoutEnv;
617
642
  const getVersionEnv = () => {
@@ -640,10 +665,20 @@ const getLatestVersion = async () => {
640
665
  });
641
666
  if (response.status === 200) {
642
667
  const data = await response.text();
643
- const v = data.trim();
668
+ // A 200 whose body is not a version - a padded value, an error page from something in
669
+ // front of the bucket - is nothing a caller can act on, so it fails here rather than
670
+ // downstream, where every comparison would read it as "no update". The parsed triple
671
+ // is what gets returned, not the body: the regex is unanchored at the end, so a body
672
+ // that merely starts with a version would otherwise carry its tail into the banners,
673
+ // the update log and the equality test in AgentManager.remoteUpdate.
674
+ // Only the agent's remote update surfaces this - `commandPre` swallows it, so the CLI
675
+ // stays quiet about a channel it could not read.
676
+ const parsed = (0, exports.parseVersion)(data.trim());
677
+ if (!parsed)
678
+ throw new Error(texts_1.ERR_FETCH_VERSION);
644
679
  if (env === 'prod')
645
- return v;
646
- return `${v}-${env}`;
680
+ return parsed.version;
681
+ return `${parsed.version}-${env}`;
647
682
  }
648
683
  else {
649
684
  return '0.0.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bdy",
3
3
  "preferGlobal": false,
4
- "version": "1.23.15-dev-target-commands",
4
+ "version": "1.23.16-dev-target-commands",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
7
7
  "homepage": "https://buddy.works/docs/cli",