bdy 1.23.13 → 1.23.14-dev

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.
Files changed (38) hide show
  1. package/distTs/package.json +3 -1
  2. package/distTs/src/api/client.js +259 -24
  3. package/distTs/src/cliIndex.js +4 -0
  4. package/distTs/src/command/api/request.js +5 -4
  5. package/distTs/src/command/artifact/list.js +2 -6
  6. package/distTs/src/command/distro/list.js +1 -0
  7. package/distTs/src/command/distro/route/update.js +1 -1
  8. package/distTs/src/command/distro/update.js +9 -9
  9. package/distTs/src/command/environment/create.js +70 -0
  10. package/distTs/src/command/environment/delete.js +30 -0
  11. package/distTs/src/command/environment/get.js +48 -0
  12. package/distTs/src/command/environment/list.js +45 -0
  13. package/distTs/src/command/environment/resolve.js +39 -0
  14. package/distTs/src/command/environment/update.js +60 -0
  15. package/distTs/src/command/environment.js +20 -0
  16. package/distTs/src/command/project/git/credential.js +6 -4
  17. package/distTs/src/command/sandbox/exec/command.js +1 -1
  18. package/distTs/src/command/sandbox/exec/logs.js +1 -1
  19. package/distTs/src/command/target/create.js +33 -0
  20. package/distTs/src/command/target/delete.js +31 -0
  21. package/distTs/src/command/target/exec/command.js +101 -0
  22. package/distTs/src/command/target/exec/kill.js +31 -0
  23. package/distTs/src/command/target/exec/list.js +53 -0
  24. package/distTs/src/command/target/exec/logs.js +27 -0
  25. package/distTs/src/command/target/exec/status.js +44 -0
  26. package/distTs/src/command/target/exec.js +24 -0
  27. package/distTs/src/command/target/get.js +63 -0
  28. package/distTs/src/command/target/list.js +49 -0
  29. package/distTs/src/command/target/scope.js +138 -0
  30. package/distTs/src/command/target/update.js +32 -0
  31. package/distTs/src/command/target.js +22 -0
  32. package/distTs/src/index.js +14 -0
  33. package/distTs/src/input.js +65 -0
  34. package/distTs/src/output/pipeline.js +5 -3
  35. package/distTs/src/output.js +30 -2
  36. package/distTs/src/texts.js +251 -27
  37. package/distTs/src/utils.js +18 -2
  38. package/package.json +3 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bdy",
3
3
  "preferGlobal": false,
4
- "version": "1.23.13",
4
+ "version": "1.23.14-dev",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
7
7
  "homepage": "https://buddy.works/docs/cli",
@@ -27,6 +27,8 @@
27
27
  "e2e:tunnel": "vitest run --config e2e/vitest.config.ts e2e/modules/tunnel/",
28
28
  "e2e:agent": "vitest run --config e2e/vitest.config.ts e2e/modules/agent/",
29
29
  "e2e:distro": "vitest run --config e2e/vitest.config.ts e2e/modules/distro/",
30
+ "e2e:target": "vitest run --config e2e/vitest.config.ts e2e/modules/target/",
31
+ "e2e:environment": "vitest run --config e2e/vitest.config.ts e2e/modules/environment/",
30
32
  "unit": "vitest run --config vitest.config.ts"
31
33
  },
32
34
  "files": [
@@ -16,6 +16,25 @@ const cfg_1 = __importDefault(require("../tunnel/cfg"));
16
16
  * echo back the escaped form - callers comparing a response against the names
17
17
  * they passed in must apply the same mapping.
18
18
  */
19
+ const RETRIABLE_NETWORK_ERROR = /ECONNREFUSED|ECONNRESET|ETIMEDOUT/;
20
+ // undici headers can be string | string[]; callers want the first value.
21
+ const headerValue = (h) => Array.isArray(h) ? h[0] : h;
22
+ // How long to wait before retrying a transient status on an idempotent
23
+ // request, or null when a retry makes no sense (non-retriable status, or a
24
+ // rate limit window that resets too far in the future to wait for).
25
+ const retryDelay = (status, attempt, rateLimitReset) => {
26
+ if (status === 429) {
27
+ if (!rateLimitReset)
28
+ return null;
29
+ const ms = parseInt(rateLimitReset, 10) * 1000 - Date.now();
30
+ if (ms <= 0)
31
+ return 1000;
32
+ return ms > 15000 ? null : ms + 500;
33
+ }
34
+ return [502, 503, 504].includes(status) ? attempt * 1000 : null;
35
+ };
36
+ const RATE_LIMIT_WARN_THRESHOLD = 25;
37
+ let rateLimitWarned = false;
19
38
  const formDataFileName = (name) => name.replace(/["\r\n]/g, (c) => ({ '"': '%22', '\r': '%0D', '\n': '%0A' })[c]);
20
39
  exports.formDataFileName = formDataFileName;
21
40
  class ApiClient {
@@ -67,9 +86,8 @@ class ApiClient {
67
86
  }
68
87
  return false;
69
88
  }
70
- async request({ method = 'GET', path, query, body = null, headers = {}, parseResponseBody = false, rawResponseBody = false, rawResponse = false, httpUrlEncoded = false, tryRefreshingToken = true, }) {
71
- if (!headers)
72
- headers = {};
89
+ async request(args) {
90
+ const { method = 'GET', path, query, body = null, headers = {}, parseResponseBody = false, rawResponseBody = false, rawResponse = false, httpUrlEncoded = false, tryRefreshingToken = true, } = args;
73
91
  if (this.token && !headers.authorization) {
74
92
  headers.authorization = `Bearer ${this.token}`;
75
93
  }
@@ -112,20 +130,41 @@ class ApiClient {
112
130
  logger_1.default.debug(headers);
113
131
  logger_1.default.debug('BODY');
114
132
  logger_1.default.debug(bodyParsed);
115
- try {
116
- response = await this.client.request(opts);
117
- rateLimitReset = response.headers['x-rate-limit-reset'];
133
+ // Transient failures (rate limit with a nearby reset, 5xx, connection
134
+ // resets) are retried with a backoff, but only for idempotent requests -
135
+ // retrying a POST could duplicate the operation.
136
+ const maxAttempts = method === 'GET' ? 3 : 1;
137
+ for (let attempt = 1;; attempt += 1) {
138
+ try {
139
+ response = await this.client.request(opts);
140
+ }
141
+ catch (err) {
142
+ logger_1.default.debug('API CLIENT RESPONSE ERROR');
143
+ logger_1.default.debug(err);
144
+ const retriable = RETRIABLE_NETWORK_ERROR.test(err.message);
145
+ if (retriable && attempt < maxAttempts) {
146
+ await (0, utils_1.sleep)(attempt * 1000);
147
+ continue;
148
+ }
149
+ throw new Error(retriable ? texts_1.ERR_REST_API_CONNECT_ERROR : texts_1.ERR_REST_API_GENERAL_ERROR, { cause: err });
150
+ }
151
+ rateLimitReset = headerValue(response.headers['x-rate-limit-reset']);
118
152
  status = response.statusCode;
119
153
  responseBody = response.body;
120
154
  logger_1.default.debug(`API CLIENT RESPONSE STATUS: ${status}`);
155
+ const delay = attempt < maxAttempts
156
+ ? retryDelay(status, attempt, rateLimitReset)
157
+ : null;
158
+ if (delay === null)
159
+ break;
160
+ await responseBody.dump();
161
+ logger_1.default.debug(`API CLIENT RETRY IN ${delay}ms`);
162
+ await (0, utils_1.sleep)(delay);
121
163
  }
122
- catch (err) {
123
- logger_1.default.debug('API CLIENT RESPONSE ERROR');
124
- logger_1.default.debug(err);
125
- if (/ECONNREFUSED|ECONNRESET|ETIMEDOUT/.test(err.message)) {
126
- throw new Error(texts_1.ERR_REST_API_CONNECT_ERROR, { cause: err });
127
- }
128
- throw new Error(texts_1.ERR_REST_API_GENERAL_ERROR, { cause: err });
164
+ this.warnRateLimitLow(headerValue(response.headers['x-rate-limit-remaining']), rateLimitReset);
165
+ if (status === 429) {
166
+ await responseBody.dump();
167
+ throw new Error((0, texts_1.ERR_REST_API_RATE_LIMIT)(rateLimitReset));
129
168
  }
130
169
  if (status === 401) {
131
170
  await responseBody.dump();
@@ -136,15 +175,12 @@ class ApiClient {
136
175
  const success = await this.tryRefreshToken();
137
176
  if (success) {
138
177
  delete headers.authorization;
178
+ // Retry with the complete original args (an explicit field list
179
+ // once silently dropped `query` here); only the mutated headers
180
+ // and the refresh guard are overridden.
139
181
  return await this.request({
140
- method,
141
- path,
142
- body,
182
+ ...args,
143
183
  headers,
144
- parseResponseBody,
145
- rawResponseBody,
146
- rawResponse,
147
- httpUrlEncoded,
148
184
  tryRefreshingToken: false,
149
185
  });
150
186
  }
@@ -185,7 +221,7 @@ class ApiClient {
185
221
  if (status === 404)
186
222
  throw new Error(texts_1.ERR_REST_API_RESOURCE_NOT_FOUND);
187
223
  else
188
- throw new Error(texts_1.ERR_REST_API_GENERAL_ERROR);
224
+ throw new Error((0, texts_1.ERR_REST_API_HTTP_ERROR)(status));
189
225
  }
190
226
  if ([200, 201].includes(status)) {
191
227
  if (parseResponseBody) {
@@ -213,6 +249,12 @@ class ApiClient {
213
249
  }
214
250
  }
215
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;
216
258
  await responseBody.dump();
217
259
  return null;
218
260
  }
@@ -220,10 +262,38 @@ class ApiClient {
220
262
  throw new Error(texts_1.ERR_REST_API_NOT_RESPONDING);
221
263
  }
222
264
  else {
223
- await responseBody.dump();
224
- throw new Error(texts_1.ERR_REST_API_GENERAL_ERROR);
265
+ // Any other error status (409, 422, 5xx...): surface the API's own
266
+ // message when the body carries one (e.g. "Concurrency limit reached")
267
+ // instead of reducing it to a bare HTTP code.
268
+ let json;
269
+ try {
270
+ json = await responseBody.json();
271
+ }
272
+ catch {
273
+ throw new Error((0, texts_1.ERR_REST_API_HTTP_ERROR)(status));
274
+ }
275
+ logger_1.default.debug('API CLIENT PARSED RESPONSE:');
276
+ logger_1.default.debug(json);
277
+ if (json.errors && json.errors[0] && json.errors[0].message) {
278
+ throw new Error((0, texts_1.ERR_API_MESSAGE_REPLACER)(json.errors[0].message, path, this.baseUrl, rateLimitReset));
279
+ }
280
+ throw new Error((0, texts_1.ERR_REST_API_HTTP_ERROR)(status));
225
281
  }
226
282
  }
283
+ // Set by the CLI bootstrap (Input.restApiTokenClient); the transport layer
284
+ // only detects the condition and leaves rendering to the caller, so
285
+ // non-interactive consumers (MCP proxy) stay silent.
286
+ static onRateLimitLow;
287
+ // One-time notification when the rate limit window is nearly exhausted.
288
+ warnRateLimitLow(remaining, reset) {
289
+ if (rateLimitWarned || !remaining || !ApiClient.onRateLimitLow)
290
+ return;
291
+ const left = parseInt(remaining, 10);
292
+ if (Number.isNaN(left) || left > RATE_LIMIT_WARN_THRESHOLD)
293
+ return;
294
+ rateLimitWarned = true;
295
+ ApiClient.onRateLimitLow(remaining, reset);
296
+ }
227
297
  async getPipelineRunActionLogs(workspace, project, pipelineId, executionId, actionExecutionId, offset, limit) {
228
298
  const query = {
229
299
  offset: String(offset),
@@ -563,6 +633,162 @@ class ApiClient {
563
633
  q.project = project;
564
634
  return await this.getResourceByIdentifier(workspace, q);
565
635
  }
636
+ // Target methods
637
+ targetScopeQuery(scope) {
638
+ const query = {};
639
+ if (scope.project)
640
+ query.project_name = scope.project;
641
+ if (scope.pipelineId)
642
+ query.pipeline_id = scope.pipelineId.toString();
643
+ if (scope.environmentId)
644
+ query.environment_id = scope.environmentId;
645
+ return query;
646
+ }
647
+ async getTargets(workspace, filters = {}) {
648
+ return await this.request({
649
+ method: 'GET',
650
+ path: `/workspaces/${encodeURIComponent(workspace)}/targets`,
651
+ query: this.targetScopeQuery(filters),
652
+ parseResponseBody: true,
653
+ });
654
+ }
655
+ async getTarget(workspace, targetId) {
656
+ return await this.request({
657
+ method: 'GET',
658
+ path: `/workspaces/${encodeURIComponent(workspace)}/targets/${encodeURIComponent(targetId)}`,
659
+ parseResponseBody: true,
660
+ });
661
+ }
662
+ async createTargetByYaml(workspace, scope, body) {
663
+ return await this.request({
664
+ method: 'POST',
665
+ path: `/workspaces/${encodeURIComponent(workspace)}/targets/yaml`,
666
+ query: this.targetScopeQuery(scope),
667
+ body,
668
+ parseResponseBody: true,
669
+ });
670
+ }
671
+ async updateTargetByYaml(workspace, targetId, body) {
672
+ return await this.request({
673
+ method: 'PATCH',
674
+ path: `/workspaces/${encodeURIComponent(workspace)}/targets/${encodeURIComponent(targetId)}/yaml`,
675
+ body,
676
+ parseResponseBody: true,
677
+ });
678
+ }
679
+ async deleteTarget(workspace, targetId) {
680
+ return await this.request({
681
+ method: 'DELETE',
682
+ path: `/workspaces/${encodeURIComponent(workspace)}/targets/${encodeURIComponent(targetId)}`,
683
+ parseResponseBody: false,
684
+ });
685
+ }
686
+ async executeTargetCommand(workspace, targetId, body) {
687
+ return await this.request({
688
+ method: 'POST',
689
+ path: `/workspaces/${encodeURIComponent(workspace)}/targets/${encodeURIComponent(targetId)}/execs`,
690
+ body,
691
+ parseResponseBody: true,
692
+ });
693
+ }
694
+ async getTargetExecs(workspace, targetId, limit) {
695
+ const query = {};
696
+ if (limit)
697
+ query.limit = limit.toString();
698
+ return await this.request({
699
+ method: 'GET',
700
+ path: `/workspaces/${encodeURIComponent(workspace)}/targets/${encodeURIComponent(targetId)}/execs`,
701
+ query,
702
+ parseResponseBody: true,
703
+ });
704
+ }
705
+ async getTargetExec(workspace, execId, wait) {
706
+ const query = {};
707
+ if (wait)
708
+ query.wait = wait.toString();
709
+ return await this.request({
710
+ method: 'GET',
711
+ path: `/workspaces/${encodeURIComponent(workspace)}/targets/execs/${encodeURIComponent(execId)}`,
712
+ query,
713
+ parseResponseBody: true,
714
+ });
715
+ }
716
+ async cancelTargetExec(workspace, execId) {
717
+ return await this.request({
718
+ method: 'DELETE',
719
+ path: `/workspaces/${encodeURIComponent(workspace)}/targets/execs/${encodeURIComponent(execId)}`,
720
+ parseResponseBody: false,
721
+ });
722
+ }
723
+ async getTargetExecLog(workspace, execId, offset, limit) {
724
+ const query = {};
725
+ if (offset)
726
+ query.offset = offset.toString();
727
+ if (limit)
728
+ query.limit = limit.toString();
729
+ return await this.request({
730
+ method: 'GET',
731
+ path: `/workspaces/${encodeURIComponent(workspace)}/targets/execs/${encodeURIComponent(execId)}/log`,
732
+ query,
733
+ parseResponseBody: true,
734
+ });
735
+ }
736
+ async getTargetByIdentifier(workspace, identifier, line = {}) {
737
+ return await this.getResourceByIdentifier(workspace, {
738
+ target: identifier,
739
+ ...line,
740
+ });
741
+ }
742
+ // Environment methods
743
+ async getEnvironments(workspace, project) {
744
+ const query = {};
745
+ if (project)
746
+ query.project_name = project;
747
+ return await this.request({
748
+ method: 'GET',
749
+ path: `/workspaces/${encodeURIComponent(workspace)}/environments`,
750
+ query,
751
+ parseResponseBody: true,
752
+ });
753
+ }
754
+ async getEnvironment(workspace, environmentId) {
755
+ return await this.request({
756
+ method: 'GET',
757
+ path: `/workspaces/${encodeURIComponent(workspace)}/environments/${encodeURIComponent(environmentId)}`,
758
+ parseResponseBody: true,
759
+ });
760
+ }
761
+ async createEnvironment(workspace, body) {
762
+ return await this.request({
763
+ method: 'POST',
764
+ path: `/workspaces/${encodeURIComponent(workspace)}/environments`,
765
+ body,
766
+ parseResponseBody: true,
767
+ });
768
+ }
769
+ async updateEnvironment(workspace, environmentId, body) {
770
+ return await this.request({
771
+ method: 'PATCH',
772
+ path: `/workspaces/${encodeURIComponent(workspace)}/environments/${encodeURIComponent(environmentId)}`,
773
+ body,
774
+ parseResponseBody: true,
775
+ });
776
+ }
777
+ async deleteEnvironment(workspace, environmentId) {
778
+ return await this.request({
779
+ method: 'DELETE',
780
+ path: `/workspaces/${encodeURIComponent(workspace)}/environments/${encodeURIComponent(environmentId)}`,
781
+ parseResponseBody: false,
782
+ });
783
+ }
784
+ async getEnvironmentByIdentifier(workspace, identifier, project) {
785
+ const q = {
786
+ environment: identifier,
787
+ };
788
+ if (project)
789
+ q.project = project;
790
+ return await this.getResourceByIdentifier(workspace, q);
791
+ }
566
792
  // Sandbox methods
567
793
  async createSandbox(workspace, project, body) {
568
794
  return await this.request({
@@ -1027,11 +1253,20 @@ class ApiClient {
1027
1253
  query += '&';
1028
1254
  query += encodeURIComponent(key) + '=' + encodeURIComponent(value);
1029
1255
  });
1030
- return await this.request({
1256
+ const resolved = await this.request({
1031
1257
  method: 'GET',
1032
1258
  path: `/workspaces/${encodeURIComponent(workspace)}/identifiers${query}`,
1033
1259
  parseResponseBody: true,
1034
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;
1035
1270
  }
1036
1271
  async resolveIdentifiers(workspace, params) {
1037
1272
  let query = '';
@@ -24,6 +24,8 @@ const artifact_1 = __importDefault(require("./command/artifact"));
24
24
  const api_1 = __importDefault(require("./command/api"));
25
25
  const domain_1 = __importDefault(require("./command/domain"));
26
26
  const distro_1 = __importDefault(require("./command/distro"));
27
+ const target_1 = __importDefault(require("./command/target"));
28
+ const environment_1 = __importDefault(require("./command/environment"));
27
29
  const register_1 = __importDefault(require("./command/register"));
28
30
  const mcp_1 = __importDefault(require("./command/mcp"));
29
31
  function cliIndex() {
@@ -47,6 +49,8 @@ function cliIndex() {
47
49
  program.addCommand(sandbox_1.default);
48
50
  program.addCommand(domain_1.default);
49
51
  program.addCommand(distro_1.default);
52
+ program.addCommand(target_1.default);
53
+ program.addCommand(environment_1.default);
50
54
  program.addCommand(login_1.default);
51
55
  program.addCommand(register_1.default);
52
56
  program.addCommand(whoami_1.default);
@@ -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,14 +18,12 @@ commandArtifactList.action(async (options) => {
18
18
  const workspace = input_1.default.restApiWorkspace(options.workspace);
19
19
  const project = input_1.default.restApiProject(options.project, true);
20
20
  const scope = input_1.default.scope(project, options.scope);
21
+ input_1.default.projectScopeNote(options, project, scope, 'artifacts');
21
22
  const client = input_1.default.restApiTokenClient();
22
23
  const response = await client.getArtifacts(workspace, scope === distro_1.SCOPE.PROJECT ? project : null);
23
24
  const artifacts = response.artifacts || [];
24
25
  if (options.format === 'json') {
25
- const filtered = scope === distro_1.SCOPE.PROJECT
26
- ? artifacts.filter((a) => a.scope !== distro_1.SCOPE.WORKSPACE)
27
- : artifacts;
28
- output_1.default.json(filtered);
26
+ output_1.default.json(artifacts);
29
27
  }
30
28
  else {
31
29
  if (artifacts.length === 0) {
@@ -33,8 +31,6 @@ commandArtifactList.action(async (options) => {
33
31
  }
34
32
  const data = [['NAME', 'IDENTIFIER', 'TYPE', 'URL']];
35
33
  for (const artifact of artifacts) {
36
- if (scope === distro_1.SCOPE.PROJECT && artifact.scope === distro_1.SCOPE.WORKSPACE)
37
- continue;
38
34
  data.push([
39
35
  artifact.name,
40
36
  artifact.identifier,
@@ -19,6 +19,7 @@ commandDistroList.action(async (options) => {
19
19
  const workspace = input_1.default.restApiWorkspace(options.workspace);
20
20
  const project = input_1.default.restApiProject(options.project, true);
21
21
  const scope = input_1.default.scope(project, options.scope);
22
+ input_1.default.projectScopeNote(options, project, scope, 'distributions');
22
23
  const client = input_1.default.restApiTokenClient();
23
24
  const r = await client.getDistributions(workspace, scope === distro_1.SCOPE.PROJECT ? project : null);
24
25
  const distributions = r.distributions || [];
@@ -77,7 +77,7 @@ commandDistroRouteUpdate.action(async (identifier, routeId, options) => {
77
77
  changed = true;
78
78
  }
79
79
  if (!changed) {
80
- output_1.default.exitError(texts_1.ERR_COMMAND_ROUTE_NO_UPDATE);
80
+ output_1.default.exitError(texts_1.ERR_COMMAND_NO_UPDATE);
81
81
  }
82
82
  const result = await client.updateRoute(workspace, scope === distro_1.SCOPE.PROJECT ? project : null, data.distribution_id, routeId, body);
83
83
  output_1.default.id(result.id);
@@ -27,14 +27,6 @@ commandDistroUpdate.action(async (identifier, options) => {
27
27
  const workspace = input_1.default.restApiWorkspace(options.workspace);
28
28
  const project = input_1.default.restApiProject(options.project, true);
29
29
  const scope = input_1.default.scope(project, options.scope);
30
- const client = input_1.default.restApiTokenClient();
31
- const data = await client.getDistributionByIdentifier(workspace, scope === distro_1.SCOPE.PROJECT ? project : null, identifier);
32
- if (!data || !data.domain) {
33
- output_1.default.exitError(texts_1.ERR_WORKSPACE_NOT_FOUND);
34
- }
35
- if (!data || !data.distribution_id) {
36
- output_1.default.exitError(texts_1.ERR_COMMAND_DISTRO_NOT_FOUND);
37
- }
38
30
  const body = {};
39
31
  let changed = false;
40
32
  if (options.name) {
@@ -54,7 +46,15 @@ commandDistroUpdate.action(async (identifier, options) => {
54
46
  changed = true;
55
47
  }
56
48
  if (!changed) {
57
- output_1.default.exitError(texts_1.ERR_COMMAND_DISTRO_NO_UPDATE);
49
+ output_1.default.exitError(texts_1.ERR_COMMAND_NO_UPDATE);
50
+ }
51
+ const client = input_1.default.restApiTokenClient();
52
+ const data = await client.getDistributionByIdentifier(workspace, scope === distro_1.SCOPE.PROJECT ? project : null, identifier);
53
+ if (!data || !data.domain) {
54
+ output_1.default.exitError(texts_1.ERR_WORKSPACE_NOT_FOUND);
55
+ }
56
+ if (!data || !data.distribution_id) {
57
+ output_1.default.exitError(texts_1.ERR_COMMAND_DISTRO_NOT_FOUND);
58
58
  }
59
59
  const result = await client.updateDistribution(workspace, scope === distro_1.SCOPE.PROJECT ? project : null, data.distribution_id, body);
60
60
  output_1.default.identifier(result.identifier);
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const utils_1 = require("../../utils");
7
+ const texts_1 = require("../../texts");
8
+ const input_1 = __importDefault(require("../../input"));
9
+ const output_1 = __importDefault(require("../../output"));
10
+ const distro_1 = require("../../types/distro");
11
+ const commandEnvironmentCreate = (0, utils_1.newCommand)('create', texts_1.DESC_COMMAND_ENVIRONMENT_CREATE);
12
+ commandEnvironmentCreate.alias('add');
13
+ commandEnvironmentCreate.option('-i, --identifier <identifier>', texts_1.OPTION_ENVIRONMENT_CREATE_IDENTIFIER);
14
+ commandEnvironmentCreate.option('-w, --workspace <domain>', texts_1.OPTION_REST_API_WORKSPACE);
15
+ commandEnvironmentCreate.option('-p, --project <name>', texts_1.OPTION_REST_API_PROJECT);
16
+ commandEnvironmentCreate.option('--scope <scope>', texts_1.OPT_COMMAND_SCOPE);
17
+ commandEnvironmentCreate.option('--tags <tags>', texts_1.OPTION_ENVIRONMENT_TAGS);
18
+ commandEnvironmentCreate.option('--public-url <url>', texts_1.OPTION_ENVIRONMENT_PUBLIC_URL);
19
+ commandEnvironmentCreate.option('--icon <icon>', texts_1.OPTION_ENVIRONMENT_ICON);
20
+ commandEnvironmentCreate.option('--note <note>', texts_1.OPT_COMMAND_NOTE);
21
+ commandEnvironmentCreate.option('--agent-note <note>', texts_1.OPT_COMMAND_AGENT_NOTE);
22
+ commandEnvironmentCreate.argument('[name]', texts_1.OPTION_ENVIRONMENT_NAME);
23
+ commandEnvironmentCreate.addHelpText('after', `\nEXAMPLES:${texts_1.EXAMPLE_ENVIRONMENT_CREATE}`);
24
+ commandEnvironmentCreate.action(async (name, options) => {
25
+ const humanId = require('human-id').default;
26
+ const workspace = input_1.default.restApiWorkspace(options.workspace);
27
+ const project = input_1.default.restApiProject(options.project, true);
28
+ const scope = input_1.default.scope(project, options.scope);
29
+ if (!name) {
30
+ if (options.identifier)
31
+ name = options.identifier;
32
+ else
33
+ name = humanId({ separator: '-', capitalize: false });
34
+ }
35
+ // identifier is required by the API; derive a slug from the name when
36
+ // -i is not given (hyphens cannot appear at the start or end)
37
+ const slug = name
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9_-]+/g, '-')
40
+ .replace(/^-+|-+$/g, '');
41
+ const data = {
42
+ name,
43
+ identifier: options.identifier || slug,
44
+ scope,
45
+ };
46
+ if (project && scope === distro_1.SCOPE.PROJECT) {
47
+ data.project = {
48
+ name: project,
49
+ };
50
+ }
51
+ if (options.tags) {
52
+ data.tags = input_1.default.tags(options.tags);
53
+ }
54
+ if (options.publicUrl) {
55
+ data.public_url = options.publicUrl;
56
+ }
57
+ if (options.icon) {
58
+ data.icon = options.icon;
59
+ }
60
+ if (options.note) {
61
+ data.note = options.note;
62
+ }
63
+ if (options.agentNote) {
64
+ data.agent_note = options.agentNote;
65
+ }
66
+ const client = input_1.default.restApiTokenClient();
67
+ const result = await client.createEnvironment(workspace, data);
68
+ output_1.default.exitSuccess((0, texts_1.TXT_ENVIRONMENT_CREATED)(result.identifier, result.html_url));
69
+ });
70
+ exports.default = commandEnvironmentCreate;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const utils_1 = require("../../utils");
7
+ const texts_1 = require("../../texts");
8
+ const output_1 = __importDefault(require("../../output"));
9
+ const input_1 = __importDefault(require("../../input"));
10
+ const resolve_1 = require("./resolve");
11
+ const commandEnvironmentDelete = (0, utils_1.newCommand)('delete', texts_1.DESC_COMMAND_ENVIRONMENT_DELETE);
12
+ commandEnvironmentDelete.alias('rm');
13
+ commandEnvironmentDelete.option('-w, --workspace <domain>', texts_1.OPTION_REST_API_WORKSPACE);
14
+ commandEnvironmentDelete.option('-p, --project <name>', texts_1.OPTION_REST_API_PROJECT);
15
+ commandEnvironmentDelete.option('-f, --force', texts_1.OPTION_CONFIRM_FORCE);
16
+ commandEnvironmentDelete.argument('<identifier>', texts_1.OPTION_ENVIRONMENT_IDENTIFIER);
17
+ commandEnvironmentDelete.addHelpText('after', `\nEXAMPLES:${texts_1.EXAMPLE_ENVIRONMENT_DELETE}`);
18
+ commandEnvironmentDelete.action(async (identifier, options) => {
19
+ const workspace = input_1.default.restApiWorkspace(options.workspace);
20
+ const client = input_1.default.restApiTokenClient();
21
+ const environment_id = await (0, resolve_1.resolveEnvironmentId)(client, workspace, identifier, options,
22
+ /* strictProject */ true);
23
+ const confirmed = options.force ||
24
+ (await output_1.default.confirm((0, texts_1.TXT_ENVIRONMENT_DELETE_CONFIRM)(identifier)));
25
+ if (!confirmed)
26
+ output_1.default.exitNormal();
27
+ await client.deleteEnvironment(workspace, environment_id);
28
+ output_1.default.exitSuccess((0, texts_1.TXT_ENVIRONMENT_DELETED)(identifier));
29
+ });
30
+ exports.default = commandEnvironmentDelete;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const utils_1 = require("../../utils");
7
+ const texts_1 = require("../../texts");
8
+ const output_1 = __importDefault(require("../../output"));
9
+ const input_1 = __importDefault(require("../../input"));
10
+ const resolve_1 = require("./resolve");
11
+ const commandEnvironmentGet = (0, utils_1.newCommand)('get', texts_1.DESC_COMMAND_ENVIRONMENT_GET);
12
+ commandEnvironmentGet.option('-w, --workspace <domain>', texts_1.OPTION_REST_API_WORKSPACE);
13
+ commandEnvironmentGet.option('-p, --project <name>', texts_1.OPTION_REST_API_PROJECT);
14
+ commandEnvironmentGet.option('--format <text|json>', texts_1.OPTION_FORMAT);
15
+ commandEnvironmentGet.argument('<identifier>', texts_1.OPTION_ENVIRONMENT_IDENTIFIER);
16
+ commandEnvironmentGet.addHelpText('after', `\nEXAMPLES:${texts_1.EXAMPLE_ENVIRONMENT_GET}`);
17
+ commandEnvironmentGet.action(async (identifier, options) => {
18
+ const workspace = input_1.default.restApiWorkspace(options.workspace);
19
+ const client = input_1.default.restApiTokenClient();
20
+ const environment_id = await (0, resolve_1.resolveEnvironmentId)(client, workspace, identifier, options);
21
+ const environment = await client.getEnvironment(workspace, environment_id);
22
+ if (options.format === 'json') {
23
+ output_1.default.json(environment);
24
+ }
25
+ else {
26
+ const data = [
27
+ ['Field', 'Value'],
28
+ ['ID', environment.id || '-'],
29
+ ['Identifier', environment.identifier || '-'],
30
+ ['Name', environment.name || '-'],
31
+ ['Scope', environment.scope || '-'],
32
+ ['Tags', (environment.tags || []).join(', ') || '-'],
33
+ ['Public URL', environment.public_url || '-'],
34
+ ['Icon', environment.icon || '-'],
35
+ ['Note', environment.note || '-'],
36
+ ['Agent note', environment.agent_note || '-'],
37
+ ];
38
+ if (environment.project) {
39
+ data.push(['Project', environment.project.name || '-']);
40
+ }
41
+ if (environment.html_url) {
42
+ data.push(['URL', environment.html_url]);
43
+ }
44
+ output_1.default.table(data);
45
+ }
46
+ output_1.default.exitNormal();
47
+ });
48
+ exports.default = commandEnvironmentGet;