@heroku/skynet 2.0.4 → 2.2.0
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/commands/allowlists.js +2 -1
- package/dist/commands/categories/get.js +2 -1
- package/dist/commands/deprovision.js +2 -1
- package/dist/commands/suspend/app-owner.js +5 -5
- package/dist/commands/suspend/apps.js +3 -2
- package/dist/commands/suspend/user.js +26 -11
- package/dist/commands/suspensions.js +2 -1
- package/dist/commands/unsuspend/apps.js +2 -2
- package/dist/commands/unsuspend/user.js +2 -2
- package/dist/lib/utils.d.ts +12 -0
- package/dist/lib/utils.js +89 -8
- package/oclif.manifest.json +1 -1
- package/package.json +3 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import SkynetAPI from '../lib/skynet.js';
|
|
2
2
|
import { Command } from '@heroku-cli/command';
|
|
3
3
|
import { hux } from '@heroku/heroku-cli-util';
|
|
4
|
+
import { parseResponse } from '../lib/utils.js';
|
|
4
5
|
export default class Allowlists extends Command {
|
|
5
6
|
static id = 'skynet:allowlists';
|
|
6
7
|
static aliases = ['skynet:allowlists'];
|
|
@@ -12,7 +13,7 @@ export default class Allowlists extends Command {
|
|
|
12
13
|
async run() {
|
|
13
14
|
const skynet = new SkynetAPI(this.heroku.auth);
|
|
14
15
|
let response = await skynet.allowlists();
|
|
15
|
-
response =
|
|
16
|
+
response = parseResponse(response);
|
|
16
17
|
if (Object.keys(response).length > 0) {
|
|
17
18
|
hux.table(response, {
|
|
18
19
|
Value: {},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import SkynetAPI from '../../lib/skynet.js';
|
|
2
2
|
import { Command } from '@heroku-cli/command';
|
|
3
3
|
import { hux } from '@heroku/heroku-cli-util';
|
|
4
|
+
import { parseResponse } from '../../lib/utils.js';
|
|
4
5
|
export default class GetCategories extends Command {
|
|
5
6
|
static id = 'skynet:categories';
|
|
6
7
|
static aliases = ['skynet:categories'];
|
|
@@ -12,7 +13,7 @@ export default class GetCategories extends Command {
|
|
|
12
13
|
async run() {
|
|
13
14
|
const skynet = new SkynetAPI(this.heroku.auth);
|
|
14
15
|
let response = await skynet.categories();
|
|
15
|
-
response =
|
|
16
|
+
response = parseResponse(response);
|
|
16
17
|
hux.table(response, {
|
|
17
18
|
Category: {},
|
|
18
19
|
Description: {}
|
|
@@ -3,6 +3,7 @@ import { requireSudo } from '../lib/sudo.js';
|
|
|
3
3
|
import { Command } from '@heroku-cli/command';
|
|
4
4
|
import { color } from '@heroku-cli/color';
|
|
5
5
|
import { Flags, ux } from '@oclif/core';
|
|
6
|
+
import { parseResponse } from '../lib/utils.js';
|
|
6
7
|
export default class Deprovision extends Command {
|
|
7
8
|
static id = 'skynet:deprovision';
|
|
8
9
|
static aliases = ['skynet:deprovision'];
|
|
@@ -76,7 +77,7 @@ export default class Deprovision extends Command {
|
|
|
76
77
|
ux.action.start(`Deprovisioning ${color.cyan(user)}`);
|
|
77
78
|
let response = await skynet.deprovision(user, notes, category, notify, force);
|
|
78
79
|
ux.action.stop();
|
|
79
|
-
response =
|
|
80
|
+
response = parseResponse(response);
|
|
80
81
|
ux.log(`${color.cyan(response.status)}. ${response.message}. Notification ${notificationStatus}`);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import SkynetAPI from '../../lib/skynet.js';
|
|
2
|
-
import * as utils from '../../lib/utils.js';
|
|
3
2
|
import { requireSudo } from '../../lib/sudo.js';
|
|
4
3
|
import { Command } from '@heroku-cli/command';
|
|
5
4
|
import { color } from '@heroku-cli/color';
|
|
6
5
|
import { Flags, ux } from '@oclif/core';
|
|
7
|
-
import { processSuspensionResult } from '../../lib/utils.js';
|
|
6
|
+
import { processSuspensionResult, parseResponse, readlines, arrayChunks } from '../../lib/utils.js';
|
|
8
7
|
const chunkSize = 5;
|
|
9
8
|
export default class SuspendAppOwner extends Command {
|
|
10
9
|
static id = 'skynet:suspend:app-owner';
|
|
@@ -71,8 +70,8 @@ export default class SuspendAppOwner extends Command {
|
|
|
71
70
|
ux.error('Either --app or --infile must be passed, but not both');
|
|
72
71
|
}
|
|
73
72
|
if (file) {
|
|
74
|
-
const apps = await
|
|
75
|
-
const chunks =
|
|
73
|
+
const apps = await readlines(file);
|
|
74
|
+
const chunks = arrayChunks(apps, chunkSize);
|
|
76
75
|
for (const chunk of chunks) {
|
|
77
76
|
ux.log('Suspending app owners: ' + chunk.join());
|
|
78
77
|
const response = await skynet.bulkSuspendAppOwner(apps.join(), notes, category, deprovision);
|
|
@@ -82,7 +81,8 @@ export default class SuspendAppOwner extends Command {
|
|
|
82
81
|
else {
|
|
83
82
|
ux.action.start(`Suspending app owner of ${color.cyan(app)}`);
|
|
84
83
|
let response = await skynet.suspendAppOwner(app, notes, category, force, deprovision, flags.async);
|
|
85
|
-
response =
|
|
84
|
+
response = parseResponse(response);
|
|
85
|
+
ux.action.stop();
|
|
86
86
|
processSuspensionResult(app, response);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -3,7 +3,7 @@ import { requireSudo } from '../../lib/sudo.js';
|
|
|
3
3
|
import { Command } from '@heroku-cli/command';
|
|
4
4
|
import { color } from '@heroku-cli/color';
|
|
5
5
|
import { Flags, ux } from '@oclif/core';
|
|
6
|
-
import { processSuspensionResult } from '../../lib/utils.js';
|
|
6
|
+
import { processSuspensionResult, parseResponse } from '../../lib/utils.js';
|
|
7
7
|
export default class SuspendApp extends Command {
|
|
8
8
|
static id = 'skynet:suspend:app';
|
|
9
9
|
static aliases = ['skynet:suspend:app'];
|
|
@@ -50,7 +50,8 @@ export default class SuspendApp extends Command {
|
|
|
50
50
|
const skynet = new SkynetAPI(this.heroku.auth);
|
|
51
51
|
ux.action.start(color.blue.bold(`Suspending the app ${color.cyan(flags.app)}...`));
|
|
52
52
|
let response = await skynet.suspendApp(flags.app, flags.notes, flags.category, flags.bypass || process.env.HEROKU_FORCE === '1', flags.async);
|
|
53
|
-
response =
|
|
53
|
+
response = parseResponse(response);
|
|
54
|
+
ux.action.stop();
|
|
54
55
|
processSuspensionResult(flags.app, response);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -2,7 +2,7 @@ import SkynetAPI from '../../lib/skynet.js';
|
|
|
2
2
|
import { requireSudo } from '../../lib/sudo.js';
|
|
3
3
|
import { Command } from '@heroku-cli/command';
|
|
4
4
|
import { Flags, ux } from '@oclif/core';
|
|
5
|
-
import {
|
|
5
|
+
import { processSuspensionResult, parseResponse, handleApiError } from '../../lib/utils.js';
|
|
6
6
|
import { color } from '@heroku-cli/color';
|
|
7
7
|
export default class SuspendUser extends Command {
|
|
8
8
|
static id = 'skynet:suspend:user';
|
|
@@ -92,21 +92,36 @@ export default class SuspendUser extends Command {
|
|
|
92
92
|
}
|
|
93
93
|
if (file) {
|
|
94
94
|
ux.action.start(color.blue.bold('Starting bulk suspend...'));
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
ux.action.stop(
|
|
95
|
+
try {
|
|
96
|
+
let response = await skynet.bulkSuspendUsers(file, notes, category, notify, force, deprovision);
|
|
97
|
+
response = parseResponse(response);
|
|
98
|
+
ux.action.stop();
|
|
99
|
+
// For bulk operations, use a custom message if no message is in response
|
|
100
|
+
if ((response?.status === 'OK' || response?.statusCode === 200) && !response?.message && !response?.Message) {
|
|
101
|
+
response.message = `Bulk suspended users from ${file}. Please check slack for update on processing.`;
|
|
102
|
+
}
|
|
103
|
+
processSuspensionResult(file, response);
|
|
99
104
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
catch (error) {
|
|
106
|
+
ux.action.stop();
|
|
107
|
+
handleApiError(error, `bulk suspend users from ${file}`);
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
else {
|
|
106
111
|
ux.action.start(color.blue.bold(`Suspending user ${color.cyan(user)}...`));
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
try {
|
|
113
|
+
let response = await skynet.suspendUser(user, notes, category, notify, force, deprovision, flags.async);
|
|
114
|
+
response = parseResponse(response);
|
|
115
|
+
ux.action.stop();
|
|
116
|
+
processSuspensionResult(user, response);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// got throws errors for non-2xx status codes (like 409)
|
|
120
|
+
ux.action.stop();
|
|
121
|
+
handleApiError(error, `suspend ${user}`, false);
|
|
122
|
+
// Don't re-throw - we've handled the error response
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
110
125
|
}
|
|
111
126
|
}
|
|
112
127
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import SkynetAPI from '../lib/skynet.js';
|
|
2
2
|
import { Command } from '@heroku-cli/command';
|
|
3
3
|
import { Flags, ux } from '@oclif/core';
|
|
4
|
+
import { parseResponse } from '../lib/utils.js';
|
|
4
5
|
export default class Suspensions extends Command {
|
|
5
6
|
static id = 'skynet:suspensions';
|
|
6
7
|
static aliases = ['skynet:suspensions'];
|
|
@@ -21,7 +22,7 @@ export default class Suspensions extends Command {
|
|
|
21
22
|
const { flags } = await this.parse(Suspensions);
|
|
22
23
|
const skynet = new SkynetAPI(this.heroku.auth);
|
|
23
24
|
let response = await skynet.suspensions(flags.account);
|
|
24
|
-
response =
|
|
25
|
+
response = parseResponse(response);
|
|
25
26
|
ux.log(response);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -2,7 +2,7 @@ import SkynetAPI from '../../lib/skynet.js';
|
|
|
2
2
|
import { Command } from '@heroku-cli/command';
|
|
3
3
|
import { color } from '@heroku-cli/color';
|
|
4
4
|
import { Flags, ux } from '@oclif/core';
|
|
5
|
-
import { processUnsuspendResult } from '../../lib/utils.js';
|
|
5
|
+
import { processUnsuspendResult, parseResponse } from '../../lib/utils.js';
|
|
6
6
|
export default class UnsuspendApp extends Command {
|
|
7
7
|
static id = 'skynet:unsuspend:app';
|
|
8
8
|
static aliases = ['skynet:unsuspend:app'];
|
|
@@ -44,7 +44,7 @@ export default class UnsuspendApp extends Command {
|
|
|
44
44
|
}
|
|
45
45
|
ux.action.start(`Unsuspending app ${color.cyan(app)}`);
|
|
46
46
|
let response = await skynet.unsuspendApp(app, category, notes);
|
|
47
|
-
response =
|
|
47
|
+
response = parseResponse(response);
|
|
48
48
|
processUnsuspendResult(app, response);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -3,7 +3,7 @@ import { requireSudo } from '../../lib/sudo.js';
|
|
|
3
3
|
import { Command } from '@heroku-cli/command';
|
|
4
4
|
import { color } from '@heroku-cli/color';
|
|
5
5
|
import { Flags, ux } from '@oclif/core';
|
|
6
|
-
import { processUnsuspendResult } from '../../lib/utils.js';
|
|
6
|
+
import { processUnsuspendResult, parseResponse } from '../../lib/utils.js';
|
|
7
7
|
export default class UnsuspendUser extends Command {
|
|
8
8
|
static id = 'skynet:unsuspend:user';
|
|
9
9
|
static aliases = ['skynet:unsuspend:user'];
|
|
@@ -45,7 +45,7 @@ export default class UnsuspendUser extends Command {
|
|
|
45
45
|
}
|
|
46
46
|
ux.action.start(`Unsuspending ${color.cyan(user)}`);
|
|
47
47
|
let response = await skynet.unsuspendUser(user, category, notes);
|
|
48
|
-
response =
|
|
48
|
+
response = parseResponse(response);
|
|
49
49
|
processUnsuspendResult(user, response);
|
|
50
50
|
}
|
|
51
51
|
}
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
export declare function readlines(file: string): Promise<string[]>;
|
|
2
2
|
export declare function arrayChunks<T>(array: T[], chunkSize: number): T[][];
|
|
3
|
+
/**
|
|
4
|
+
* Parses a response that may be a string (JSON) or already an object
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseResponse(response: any): any;
|
|
7
|
+
/**
|
|
8
|
+
* Extracts and normalizes error information from an API error
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractErrorBody(error: any): any;
|
|
11
|
+
/**
|
|
12
|
+
* Handles API errors consistently, logging them and optionally throwing
|
|
13
|
+
*/
|
|
14
|
+
export declare function handleApiError(error: any, operation: string, throwError?: boolean): void;
|
|
3
15
|
export declare function processSuspensionResult(suspendObject: any, response: any): void;
|
|
4
16
|
export declare function processUnsuspendResult(unsuspendObject: any, response: any): void;
|
|
5
17
|
export declare function logError(response: any): void;
|
package/dist/lib/utils.js
CHANGED
|
@@ -32,18 +32,74 @@ export function arrayChunks(array, chunkSize) {
|
|
|
32
32
|
}
|
|
33
33
|
return chunks;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Parses a response that may be a string (JSON) or already an object
|
|
37
|
+
*/
|
|
38
|
+
export function parseResponse(response) {
|
|
39
|
+
return typeof response === 'string' ? JSON.parse(response) : response;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extracts and normalizes error information from an API error
|
|
43
|
+
*/
|
|
44
|
+
export function extractErrorBody(error) {
|
|
45
|
+
if (!error?.response?.body) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const errorBody = typeof error.response.body === 'string'
|
|
50
|
+
? JSON.parse(error.response.body)
|
|
51
|
+
: error.response.body;
|
|
52
|
+
// Add status code from the HTTP response if not in body
|
|
53
|
+
if (error.response.statusCode && !errorBody.statusCode) {
|
|
54
|
+
errorBody.statusCode = error.response.statusCode;
|
|
55
|
+
}
|
|
56
|
+
return errorBody;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Handles API errors consistently, logging them and optionally throwing
|
|
64
|
+
*/
|
|
65
|
+
export function handleApiError(error, operation, throwError = true) {
|
|
66
|
+
const errorBody = extractErrorBody(error);
|
|
67
|
+
if (errorBody) {
|
|
68
|
+
logError(errorBody);
|
|
69
|
+
}
|
|
70
|
+
else if (!error?.response?.body) {
|
|
71
|
+
// Only log error message if we don't have a response body (already logged by logError)
|
|
72
|
+
ux.log(color.red(`Error: ${error?.message || error}`));
|
|
73
|
+
}
|
|
74
|
+
ux.log(color.bgRed(`Failed to ${operation}`));
|
|
75
|
+
if (throwError) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
35
79
|
export function processSuspensionResult(suspendObject, response) {
|
|
36
|
-
if (response
|
|
37
|
-
|
|
80
|
+
if (response?.success === 'ok' || response?.status === 'OK' || response?.statusCode === 200) {
|
|
81
|
+
const message = response?.message || response?.Message;
|
|
82
|
+
if (message) {
|
|
83
|
+
ux.log(color.green(message));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
ux.log(color.green(`Suspended ${suspendObject}`));
|
|
87
|
+
}
|
|
38
88
|
}
|
|
39
89
|
else {
|
|
40
90
|
logError(response);
|
|
41
|
-
ux.
|
|
91
|
+
ux.log(color.bgRed(`Failed to suspend ${suspendObject}`));
|
|
42
92
|
}
|
|
43
93
|
}
|
|
44
94
|
export function processUnsuspendResult(unsuspendObject, response) {
|
|
45
|
-
if (response
|
|
46
|
-
|
|
95
|
+
if (response?.success === 'ok' || response?.status === 'OK' || response?.statusCode === 200) {
|
|
96
|
+
const message = response?.message || response?.Message;
|
|
97
|
+
if (message) {
|
|
98
|
+
ux.action.stop(color.green(message));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
ux.action.stop(color.green(`Unsuspended ${unsuspendObject}`));
|
|
102
|
+
}
|
|
47
103
|
}
|
|
48
104
|
else {
|
|
49
105
|
logError(response);
|
|
@@ -51,7 +107,32 @@ export function processUnsuspendResult(unsuspendObject, response) {
|
|
|
51
107
|
}
|
|
52
108
|
}
|
|
53
109
|
export function logError(response) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
110
|
+
const parts = [];
|
|
111
|
+
// Handle API response format: { Status: "error", StatusCode: 409, Message: "...", Error: "..." }
|
|
112
|
+
if (response?.statusCode || response?.StatusCode) {
|
|
113
|
+
parts.push(`Status: ${response.statusCode || response.StatusCode}`);
|
|
114
|
+
}
|
|
115
|
+
if (response?.Status && response.Status !== 'OK') {
|
|
116
|
+
parts.push(response.Status);
|
|
117
|
+
}
|
|
118
|
+
if (response?.Message) {
|
|
119
|
+
parts.push(response.Message);
|
|
120
|
+
}
|
|
121
|
+
else if (response?.message) {
|
|
122
|
+
parts.push(response.message);
|
|
123
|
+
}
|
|
124
|
+
if (response?.Error) {
|
|
125
|
+
parts.push(response.Error);
|
|
126
|
+
}
|
|
127
|
+
else if (response?.error) {
|
|
128
|
+
parts.push(response.error);
|
|
129
|
+
}
|
|
130
|
+
// If we have parts, combine them into a single line
|
|
131
|
+
if (parts.length > 0) {
|
|
132
|
+
ux.log(color.red(parts.join(' - ')));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// If response doesn't have expected error format, log the whole thing
|
|
136
|
+
ux.log(color.red(`Error: ${JSON.stringify(response)}`));
|
|
137
|
+
}
|
|
57
138
|
}
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroku/skynet",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "use Skynet from Heroku CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": "heroku/heroku-skynet-cli",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"@heroku-cli/color": "^2.0.4",
|
|
21
21
|
"@heroku-cli/command": "^11.8.0",
|
|
22
22
|
"@heroku/heroku-cli-util": "^10.1.2",
|
|
23
|
+
"@oclif/core": "^2.16.0",
|
|
23
24
|
"@oclif/plugin-help": "^6.2.32",
|
|
24
25
|
"form-data": "^4.0.4",
|
|
25
26
|
"got": "^14.6.0",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"build:dev": "rm -rf dist && tsc -b --sourcemap",
|
|
67
68
|
"build:full": "rm -rf dist && tsc -b && npm run manifest && npm run readme",
|
|
68
69
|
"manifest": "oclif manifest",
|
|
70
|
+
"release:prepare": "np --no-yarn",
|
|
69
71
|
"readme": "echo 'README generation skipped for legacy plugin format'",
|
|
70
72
|
"release": "np --no-yarn --any-branch",
|
|
71
73
|
"lint": "eslint . --ext .ts --config .eslintrc.json",
|