@supercheck/cli 0.1.0-beta.3 → 0.1.0-beta.4
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/README.md +78 -26
- package/dist/bin/supercheck.js +500 -405
- package/dist/bin/supercheck.js.map +1 -1
- package/dist/chunk-W54DX2I7.js +21 -0
- package/dist/chunk-W54DX2I7.js.map +1 -0
- package/dist/prompt-BPDPYRS7.js +8 -0
- package/dist/prompt-BPDPYRS7.js.map +1 -0
- package/package.json +1 -1
package/dist/bin/supercheck.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
confirmPrompt
|
|
4
|
+
} from "../chunk-W54DX2I7.js";
|
|
2
5
|
|
|
3
6
|
// src/bin/supercheck.ts
|
|
4
7
|
import { Command as Command18 } from "commander";
|
|
@@ -210,7 +213,7 @@ function requireTriggerKey() {
|
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
// src/version.ts
|
|
213
|
-
var CLI_VERSION = true ? "0.1.0-beta.
|
|
216
|
+
var CLI_VERSION = true ? "0.1.0-beta.4" : "0.0.0-dev";
|
|
214
217
|
|
|
215
218
|
// src/api/client.ts
|
|
216
219
|
import { ProxyAgent } from "undici";
|
|
@@ -407,7 +410,7 @@ var ApiClient = class {
|
|
|
407
410
|
return this.request("DELETE", path);
|
|
408
411
|
}
|
|
409
412
|
sleep(ms) {
|
|
410
|
-
return new Promise((
|
|
413
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
411
414
|
}
|
|
412
415
|
};
|
|
413
416
|
var defaultClient = null;
|
|
@@ -760,6 +763,203 @@ function safeTokenPreview(token) {
|
|
|
760
763
|
return `${token.substring(0, 12)}...${token.substring(token.length - 4)}`;
|
|
761
764
|
}
|
|
762
765
|
|
|
766
|
+
// src/utils/spinner.ts
|
|
767
|
+
import ora from "ora";
|
|
768
|
+
|
|
769
|
+
// src/output/formatter.ts
|
|
770
|
+
import Table from "cli-table3";
|
|
771
|
+
import pc2 from "picocolors";
|
|
772
|
+
var currentFormat = "table";
|
|
773
|
+
function setOutputFormat(format) {
|
|
774
|
+
currentFormat = format;
|
|
775
|
+
}
|
|
776
|
+
function getOutputFormat() {
|
|
777
|
+
return currentFormat;
|
|
778
|
+
}
|
|
779
|
+
function output(data, options) {
|
|
780
|
+
switch (currentFormat) {
|
|
781
|
+
case "json":
|
|
782
|
+
logger.output(JSON.stringify(data, null, 2));
|
|
783
|
+
break;
|
|
784
|
+
case "quiet":
|
|
785
|
+
if (Array.isArray(data)) {
|
|
786
|
+
for (const item of data) {
|
|
787
|
+
if ("id" in item) logger.output(String(item.id));
|
|
788
|
+
}
|
|
789
|
+
} else if ("id" in data) {
|
|
790
|
+
logger.output(String(data.id));
|
|
791
|
+
}
|
|
792
|
+
break;
|
|
793
|
+
case "table":
|
|
794
|
+
default:
|
|
795
|
+
outputTable(data, options?.columns);
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function outputTable(data, columns) {
|
|
800
|
+
if (data === void 0 || data === null) {
|
|
801
|
+
logger.info("No results found.");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const rawItems = Array.isArray(data) ? data : [data];
|
|
805
|
+
const items = rawItems.filter((item) => item !== void 0 && item !== null);
|
|
806
|
+
if (items.length === 0) {
|
|
807
|
+
logger.info("No results found.");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const cols = columns ?? Object.keys(items[0]).map((key) => ({
|
|
811
|
+
key,
|
|
812
|
+
header: key.charAt(0).toUpperCase() + key.slice(1)
|
|
813
|
+
}));
|
|
814
|
+
const table = new Table({
|
|
815
|
+
head: cols.map((c) => c.header),
|
|
816
|
+
style: {
|
|
817
|
+
head: ["cyan"],
|
|
818
|
+
border: ["gray"]
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
for (const item of items) {
|
|
822
|
+
table.push(cols.map((c) => formatValue(item?.[c.key], c, item)));
|
|
823
|
+
}
|
|
824
|
+
logger.output(table.toString());
|
|
825
|
+
}
|
|
826
|
+
var STATUS_COLORS = {
|
|
827
|
+
// Success states (green)
|
|
828
|
+
up: "success",
|
|
829
|
+
ok: "success",
|
|
830
|
+
success: "success",
|
|
831
|
+
passed: "success",
|
|
832
|
+
active: "success",
|
|
833
|
+
sent: "success",
|
|
834
|
+
enabled: "success",
|
|
835
|
+
healthy: "success",
|
|
836
|
+
running: "success",
|
|
837
|
+
completed: "success",
|
|
838
|
+
// Error states (red)
|
|
839
|
+
down: "error",
|
|
840
|
+
failed: "error",
|
|
841
|
+
error: "error",
|
|
842
|
+
blocked: "error",
|
|
843
|
+
unhealthy: "error",
|
|
844
|
+
cancelled: "error",
|
|
845
|
+
canceled: "error",
|
|
846
|
+
// Warning states (yellow)
|
|
847
|
+
paused: "warning",
|
|
848
|
+
pending: "warning",
|
|
849
|
+
disabled: "warning",
|
|
850
|
+
degraded: "warning",
|
|
851
|
+
unknown: "warning",
|
|
852
|
+
queued: "warning",
|
|
853
|
+
warning: "warning",
|
|
854
|
+
skipped: "warning",
|
|
855
|
+
inactive: "warning"
|
|
856
|
+
};
|
|
857
|
+
function formatStatus(status) {
|
|
858
|
+
const normalizedStatus = status.toLowerCase();
|
|
859
|
+
const colorType = STATUS_COLORS[normalizedStatus];
|
|
860
|
+
if (!colorType) {
|
|
861
|
+
return status;
|
|
862
|
+
}
|
|
863
|
+
switch (colorType) {
|
|
864
|
+
case "success":
|
|
865
|
+
return pc2.green(`\u25CF ${status}`);
|
|
866
|
+
case "error":
|
|
867
|
+
return pc2.red(`\u25CF ${status}`);
|
|
868
|
+
case "warning":
|
|
869
|
+
return pc2.yellow(`\u25CF ${status}`);
|
|
870
|
+
default:
|
|
871
|
+
return status;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function formatValue(value, column, row) {
|
|
875
|
+
if (column?.format && row) {
|
|
876
|
+
return column.format(value, row);
|
|
877
|
+
}
|
|
878
|
+
if (value === null || value === void 0) return pc2.dim("-");
|
|
879
|
+
if (typeof value === "boolean") {
|
|
880
|
+
return value ? pc2.green("\u2713") : pc2.red("\u2717");
|
|
881
|
+
}
|
|
882
|
+
if (value instanceof Date) return formatDate(value);
|
|
883
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
884
|
+
if (typeof value === "number") {
|
|
885
|
+
const dateForNumber = maybeFormatDate(value, column?.key);
|
|
886
|
+
if (dateForNumber) return dateForNumber;
|
|
887
|
+
return String(value);
|
|
888
|
+
}
|
|
889
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
890
|
+
const strValue = String(value);
|
|
891
|
+
const dateForString = maybeFormatDate(strValue, column?.key);
|
|
892
|
+
if (dateForString) return dateForString;
|
|
893
|
+
if (STATUS_COLORS[strValue.toLowerCase()]) {
|
|
894
|
+
return formatStatus(strValue);
|
|
895
|
+
}
|
|
896
|
+
return strValue;
|
|
897
|
+
}
|
|
898
|
+
var DATE_KEY_PATTERN = /(At|Date|Time|Timestamp)$/i;
|
|
899
|
+
function maybeFormatDate(value, key) {
|
|
900
|
+
if (!key && typeof value !== "string") return null;
|
|
901
|
+
if (key && !DATE_KEY_PATTERN.test(key) && typeof value !== "string") return null;
|
|
902
|
+
const date = new Date(value);
|
|
903
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
904
|
+
if (typeof value === "string") {
|
|
905
|
+
const isIsoLike = value.includes("T") || value.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(value);
|
|
906
|
+
if (!isIsoLike) return null;
|
|
907
|
+
return formatDate(date);
|
|
908
|
+
}
|
|
909
|
+
return formatDate(date);
|
|
910
|
+
}
|
|
911
|
+
function formatDate(date) {
|
|
912
|
+
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
913
|
+
}
|
|
914
|
+
function outputDetail(data) {
|
|
915
|
+
if (currentFormat === "json") {
|
|
916
|
+
logger.output(JSON.stringify(data, null, 2));
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (currentFormat === "quiet") {
|
|
920
|
+
if ("id" in data) logger.output(String(data.id));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const keys = Object.keys(data);
|
|
924
|
+
if (keys.length === 0) {
|
|
925
|
+
logger.info("No details available.");
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const maxKeyLen = Math.max(...keys.map((k) => k.length));
|
|
929
|
+
for (const [key, value] of Object.entries(data)) {
|
|
930
|
+
const label = key.padEnd(maxKeyLen + 2);
|
|
931
|
+
logger.output(` ${label}${formatValue(value, { key, header: key }, data)}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/utils/spinner.ts
|
|
936
|
+
function createSpinner(text) {
|
|
937
|
+
const format = getOutputFormat();
|
|
938
|
+
const isInteractive = format === "table";
|
|
939
|
+
const spinner = ora({
|
|
940
|
+
text,
|
|
941
|
+
// Disable spinner in non-interactive modes
|
|
942
|
+
isSilent: !isInteractive,
|
|
943
|
+
// Use dots style for a clean look
|
|
944
|
+
spinner: "dots"
|
|
945
|
+
});
|
|
946
|
+
return spinner;
|
|
947
|
+
}
|
|
948
|
+
async function withSpinner(text, fn, options) {
|
|
949
|
+
const spinner = createSpinner(text);
|
|
950
|
+
spinner.start();
|
|
951
|
+
try {
|
|
952
|
+
const result = await fn();
|
|
953
|
+
const successMessage = typeof options?.successText === "function" ? options.successText(result) : options?.successText ?? text.replace(/\.\.\.?$/, "");
|
|
954
|
+
spinner.succeed(successMessage);
|
|
955
|
+
return result;
|
|
956
|
+
} catch (error) {
|
|
957
|
+
const failMessage = options?.failText ?? text.replace(/\.\.\.?$/, " failed");
|
|
958
|
+
spinner.fail(failMessage);
|
|
959
|
+
throw error;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
763
963
|
// src/commands/login.ts
|
|
764
964
|
var loginCommand = new Command("login").description("Authenticate with Supercheck").option("--token <token>", "Provide a CLI token directly (for CI/CD)").option("--url <url>", "Supercheck API URL (for self-hosted instances)").action(async (options) => {
|
|
765
965
|
if (options.token) {
|
|
@@ -775,7 +975,11 @@ var loginCommand = new Command("login").description("Authenticate with Superchec
|
|
|
775
975
|
token
|
|
776
976
|
});
|
|
777
977
|
try {
|
|
778
|
-
await
|
|
978
|
+
await withSpinner(
|
|
979
|
+
"Verifying token...",
|
|
980
|
+
() => client.get("/api/cli-tokens"),
|
|
981
|
+
{ successText: "Token verified" }
|
|
982
|
+
);
|
|
779
983
|
} catch {
|
|
780
984
|
throw new CLIError(
|
|
781
985
|
"Token verification failed. Please check your token and try again.",
|
|
@@ -821,7 +1025,14 @@ var whoamiCommand = new Command("whoami").description("Show current authenticati
|
|
|
821
1025
|
const baseUrl = getStoredBaseUrl();
|
|
822
1026
|
const client = getApiClient({ token, baseUrl: baseUrl ?? void 0 });
|
|
823
1027
|
try {
|
|
824
|
-
const
|
|
1028
|
+
const data = await withSpinner(
|
|
1029
|
+
"Fetching context...",
|
|
1030
|
+
async () => {
|
|
1031
|
+
const { data: data2 } = await client.get("/api/cli-tokens");
|
|
1032
|
+
return data2;
|
|
1033
|
+
},
|
|
1034
|
+
{ successText: "Context loaded" }
|
|
1035
|
+
);
|
|
825
1036
|
const tokenPreview = safeTokenPreview(token);
|
|
826
1037
|
const activeToken = data.tokens?.find((t) => t.start && token.startsWith(t.start.replace(/\.+$/, "")));
|
|
827
1038
|
logger.newline();
|
|
@@ -1069,214 +1280,48 @@ function validateNoSecrets(config) {
|
|
|
1069
1280
|
"Config file contains what appears to be an API token. Tokens must be stored in environment variables (SUPERCHECK_TOKEN) or the OS keychain, never in config files.",
|
|
1070
1281
|
3 /* ConfigError */
|
|
1071
1282
|
);
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
async function loadConfig(options = {}) {
|
|
1076
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1077
|
-
const configPath = resolveConfigPath(cwd, options.configPath);
|
|
1078
|
-
if (!configPath) {
|
|
1079
|
-
throw new CLIError(
|
|
1080
|
-
"No supercheck.config.ts found. Run `supercheck init` to create one.",
|
|
1081
|
-
3 /* ConfigError */
|
|
1082
|
-
);
|
|
1083
|
-
}
|
|
1084
|
-
let config = await loadConfigFile(configPath);
|
|
1085
|
-
const localConfigPath = resolveLocalConfigPath(cwd);
|
|
1086
|
-
if (localConfigPath) {
|
|
1087
|
-
const localConfig = await loadConfigFile(localConfigPath);
|
|
1088
|
-
config = deepmerge(config, localConfig);
|
|
1089
|
-
}
|
|
1090
|
-
config = applyEnvOverrides(config);
|
|
1091
|
-
validateNoSecrets(config);
|
|
1092
|
-
const parsed = supercheckConfigSchema.safeParse(config);
|
|
1093
|
-
if (!parsed.success) {
|
|
1094
|
-
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
1095
|
-
throw new CLIError(
|
|
1096
|
-
`Invalid configuration:
|
|
1097
|
-
${issues}`,
|
|
1098
|
-
3 /* ConfigError */
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
return {
|
|
1102
|
-
config: parsed.data,
|
|
1103
|
-
configPath
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
async function tryLoadConfig(options = {}) {
|
|
1107
|
-
try {
|
|
1108
|
-
return await loadConfig(options);
|
|
1109
|
-
} catch (err) {
|
|
1110
|
-
if (err instanceof CLIError && err.message.includes("No supercheck.config.ts found")) {
|
|
1111
|
-
return null;
|
|
1112
|
-
}
|
|
1113
|
-
throw err;
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// src/output/formatter.ts
|
|
1118
|
-
import Table from "cli-table3";
|
|
1119
|
-
import pc2 from "picocolors";
|
|
1120
|
-
var currentFormat = "table";
|
|
1121
|
-
function setOutputFormat(format) {
|
|
1122
|
-
currentFormat = format;
|
|
1123
|
-
}
|
|
1124
|
-
function getOutputFormat() {
|
|
1125
|
-
return currentFormat;
|
|
1126
|
-
}
|
|
1127
|
-
function output(data, options) {
|
|
1128
|
-
switch (currentFormat) {
|
|
1129
|
-
case "json":
|
|
1130
|
-
logger.output(JSON.stringify(data, null, 2));
|
|
1131
|
-
break;
|
|
1132
|
-
case "quiet":
|
|
1133
|
-
if (Array.isArray(data)) {
|
|
1134
|
-
for (const item of data) {
|
|
1135
|
-
if ("id" in item) logger.output(String(item.id));
|
|
1136
|
-
}
|
|
1137
|
-
} else if ("id" in data) {
|
|
1138
|
-
logger.output(String(data.id));
|
|
1139
|
-
}
|
|
1140
|
-
break;
|
|
1141
|
-
case "table":
|
|
1142
|
-
default:
|
|
1143
|
-
outputTable(data, options?.columns);
|
|
1144
|
-
break;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
function outputTable(data, columns) {
|
|
1148
|
-
if (data === void 0 || data === null) {
|
|
1149
|
-
logger.info("No results found.");
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
const rawItems = Array.isArray(data) ? data : [data];
|
|
1153
|
-
const items = rawItems.filter((item) => item !== void 0 && item !== null);
|
|
1154
|
-
if (items.length === 0) {
|
|
1155
|
-
logger.info("No results found.");
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
const cols = columns ?? Object.keys(items[0]).map((key) => ({
|
|
1159
|
-
key,
|
|
1160
|
-
header: key.charAt(0).toUpperCase() + key.slice(1)
|
|
1161
|
-
}));
|
|
1162
|
-
const table = new Table({
|
|
1163
|
-
head: cols.map((c) => c.header),
|
|
1164
|
-
style: {
|
|
1165
|
-
head: ["cyan"],
|
|
1166
|
-
border: ["gray"]
|
|
1167
|
-
}
|
|
1168
|
-
});
|
|
1169
|
-
for (const item of items) {
|
|
1170
|
-
table.push(cols.map((c) => formatValue(item?.[c.key], c, item)));
|
|
1171
|
-
}
|
|
1172
|
-
logger.output(table.toString());
|
|
1173
|
-
}
|
|
1174
|
-
var STATUS_COLORS = {
|
|
1175
|
-
// Success states (green)
|
|
1176
|
-
up: "success",
|
|
1177
|
-
ok: "success",
|
|
1178
|
-
success: "success",
|
|
1179
|
-
passed: "success",
|
|
1180
|
-
active: "success",
|
|
1181
|
-
sent: "success",
|
|
1182
|
-
enabled: "success",
|
|
1183
|
-
healthy: "success",
|
|
1184
|
-
running: "success",
|
|
1185
|
-
completed: "success",
|
|
1186
|
-
// Error states (red)
|
|
1187
|
-
down: "error",
|
|
1188
|
-
failed: "error",
|
|
1189
|
-
error: "error",
|
|
1190
|
-
blocked: "error",
|
|
1191
|
-
unhealthy: "error",
|
|
1192
|
-
cancelled: "error",
|
|
1193
|
-
canceled: "error",
|
|
1194
|
-
// Warning states (yellow)
|
|
1195
|
-
paused: "warning",
|
|
1196
|
-
pending: "warning",
|
|
1197
|
-
disabled: "warning",
|
|
1198
|
-
degraded: "warning",
|
|
1199
|
-
unknown: "warning",
|
|
1200
|
-
queued: "warning",
|
|
1201
|
-
warning: "warning",
|
|
1202
|
-
skipped: "warning",
|
|
1203
|
-
inactive: "warning"
|
|
1204
|
-
};
|
|
1205
|
-
function formatStatus(status) {
|
|
1206
|
-
const normalizedStatus = status.toLowerCase();
|
|
1207
|
-
const colorType = STATUS_COLORS[normalizedStatus];
|
|
1208
|
-
if (!colorType) {
|
|
1209
|
-
return status;
|
|
1210
|
-
}
|
|
1211
|
-
switch (colorType) {
|
|
1212
|
-
case "success":
|
|
1213
|
-
return pc2.green(`\u25CF ${status}`);
|
|
1214
|
-
case "error":
|
|
1215
|
-
return pc2.red(`\u25CF ${status}`);
|
|
1216
|
-
case "warning":
|
|
1217
|
-
return pc2.yellow(`\u25CF ${status}`);
|
|
1218
|
-
default:
|
|
1219
|
-
return status;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
function formatValue(value, column, row) {
|
|
1223
|
-
if (column?.format && row) {
|
|
1224
|
-
return column.format(value, row);
|
|
1225
|
-
}
|
|
1226
|
-
if (value === null || value === void 0) return pc2.dim("-");
|
|
1227
|
-
if (typeof value === "boolean") {
|
|
1228
|
-
return value ? pc2.green("\u2713") : pc2.red("\u2717");
|
|
1229
|
-
}
|
|
1230
|
-
if (value instanceof Date) return formatDate(value);
|
|
1231
|
-
if (Array.isArray(value)) return value.join(", ");
|
|
1232
|
-
if (typeof value === "number") {
|
|
1233
|
-
const dateForNumber = maybeFormatDate(value, column?.key);
|
|
1234
|
-
if (dateForNumber) return dateForNumber;
|
|
1235
|
-
return String(value);
|
|
1236
|
-
}
|
|
1237
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
1238
|
-
const strValue = String(value);
|
|
1239
|
-
const dateForString = maybeFormatDate(strValue, column?.key);
|
|
1240
|
-
if (dateForString) return dateForString;
|
|
1241
|
-
if (STATUS_COLORS[strValue.toLowerCase()]) {
|
|
1242
|
-
return formatStatus(strValue);
|
|
1243
|
-
}
|
|
1244
|
-
return strValue;
|
|
1245
|
-
}
|
|
1246
|
-
var DATE_KEY_PATTERN = /(At|Date|Time|Timestamp)$/i;
|
|
1247
|
-
function maybeFormatDate(value, key) {
|
|
1248
|
-
if (!key && typeof value !== "string") return null;
|
|
1249
|
-
if (key && !DATE_KEY_PATTERN.test(key) && typeof value !== "string") return null;
|
|
1250
|
-
const date = new Date(value);
|
|
1251
|
-
if (Number.isNaN(date.getTime())) return null;
|
|
1252
|
-
if (typeof value === "string") {
|
|
1253
|
-
const isIsoLike = value.includes("T") || value.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(value);
|
|
1254
|
-
if (!isIsoLike) return null;
|
|
1255
|
-
return formatDate(date);
|
|
1256
|
-
}
|
|
1257
|
-
return formatDate(date);
|
|
1258
|
-
}
|
|
1259
|
-
function formatDate(date) {
|
|
1260
|
-
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
1261
|
-
}
|
|
1262
|
-
function outputDetail(data) {
|
|
1263
|
-
if (currentFormat === "json") {
|
|
1264
|
-
logger.output(JSON.stringify(data, null, 2));
|
|
1265
|
-
return;
|
|
1283
|
+
}
|
|
1266
1284
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1285
|
+
}
|
|
1286
|
+
async function loadConfig(options = {}) {
|
|
1287
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1288
|
+
const configPath = resolveConfigPath(cwd, options.configPath);
|
|
1289
|
+
if (!configPath) {
|
|
1290
|
+
throw new CLIError(
|
|
1291
|
+
"No supercheck.config.ts found. Run `supercheck init` to create one.",
|
|
1292
|
+
3 /* ConfigError */
|
|
1293
|
+
);
|
|
1270
1294
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1295
|
+
let config = await loadConfigFile(configPath);
|
|
1296
|
+
const localConfigPath = resolveLocalConfigPath(cwd);
|
|
1297
|
+
if (localConfigPath) {
|
|
1298
|
+
const localConfig = await loadConfigFile(localConfigPath);
|
|
1299
|
+
config = deepmerge(config, localConfig);
|
|
1275
1300
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1301
|
+
config = applyEnvOverrides(config);
|
|
1302
|
+
validateNoSecrets(config);
|
|
1303
|
+
const parsed = supercheckConfigSchema.safeParse(config);
|
|
1304
|
+
if (!parsed.success) {
|
|
1305
|
+
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
1306
|
+
throw new CLIError(
|
|
1307
|
+
`Invalid configuration:
|
|
1308
|
+
${issues}`,
|
|
1309
|
+
3 /* ConfigError */
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
config: parsed.data,
|
|
1314
|
+
configPath
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
async function tryLoadConfig(options = {}) {
|
|
1318
|
+
try {
|
|
1319
|
+
return await loadConfig(options);
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
if (err instanceof CLIError && err.message.includes("No supercheck.config.ts found")) {
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
throw err;
|
|
1280
1325
|
}
|
|
1281
1326
|
}
|
|
1282
1327
|
|
|
@@ -1286,7 +1331,14 @@ var healthCommand = new Command2("health").description("Check Supercheck API hea
|
|
|
1286
1331
|
const baseUrl = options.url ?? getStoredBaseUrl() ?? configResult?.config.api?.baseUrl ?? "https://app.supercheck.io";
|
|
1287
1332
|
const client = getApiClient({ baseUrl });
|
|
1288
1333
|
try {
|
|
1289
|
-
const
|
|
1334
|
+
const data = await withSpinner(
|
|
1335
|
+
`Checking API health at ${baseUrl}...`,
|
|
1336
|
+
async () => {
|
|
1337
|
+
const { data: data2 } = await client.get("/api/health");
|
|
1338
|
+
return data2;
|
|
1339
|
+
},
|
|
1340
|
+
{ successText: "Health check complete" }
|
|
1341
|
+
);
|
|
1290
1342
|
const checks = data.checks ?? {};
|
|
1291
1343
|
const displayData = {
|
|
1292
1344
|
status: data.status,
|
|
@@ -1322,8 +1374,14 @@ var locationsCommand = new Command2("locations").description("List available exe
|
|
|
1322
1374
|
const configResult = await tryLoadConfig();
|
|
1323
1375
|
const baseUrl = getStoredBaseUrl() ?? configResult?.config.api?.baseUrl ?? "https://app.supercheck.io";
|
|
1324
1376
|
const client = getApiClient({ baseUrl });
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1377
|
+
const locations = await withSpinner(
|
|
1378
|
+
"Fetching execution locations...",
|
|
1379
|
+
async () => {
|
|
1380
|
+
const { data } = await client.get("/api/locations");
|
|
1381
|
+
return "locations" in data ? data.locations : data.data ?? [];
|
|
1382
|
+
},
|
|
1383
|
+
{ successText: "Locations loaded" }
|
|
1384
|
+
);
|
|
1327
1385
|
output(locations, {
|
|
1328
1386
|
columns: [
|
|
1329
1387
|
{ key: "code", header: "Code" },
|
|
@@ -1349,35 +1407,91 @@ configCommand.command("print").description("Print the resolved configuration").a
|
|
|
1349
1407
|
|
|
1350
1408
|
// src/commands/init.ts
|
|
1351
1409
|
import { Command as Command4 } from "commander";
|
|
1352
|
-
import { existsSync as
|
|
1410
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2, appendFileSync } from "fs";
|
|
1411
|
+
import { resolve as resolve4 } from "path";
|
|
1412
|
+
|
|
1413
|
+
// src/utils/package-manager.ts
|
|
1414
|
+
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
1353
1415
|
import { resolve as resolve3, basename as basename2 } from "path";
|
|
1416
|
+
function detectPackageManager(cwd) {
|
|
1417
|
+
if (existsSync2(resolve3(cwd, "bun.lockb")) || existsSync2(resolve3(cwd, "bun.lock"))) return "bun";
|
|
1418
|
+
if (existsSync2(resolve3(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1419
|
+
if (existsSync2(resolve3(cwd, "yarn.lock"))) return "yarn";
|
|
1420
|
+
return "npm";
|
|
1421
|
+
}
|
|
1422
|
+
function getInstallCommand(pm, packages) {
|
|
1423
|
+
const pkgs = packages.join(" ");
|
|
1424
|
+
switch (pm) {
|
|
1425
|
+
case "bun":
|
|
1426
|
+
return `bun add -d ${pkgs}`;
|
|
1427
|
+
case "pnpm":
|
|
1428
|
+
return `pnpm add -D ${pkgs}`;
|
|
1429
|
+
case "yarn":
|
|
1430
|
+
return `yarn add -D ${pkgs}`;
|
|
1431
|
+
case "npm":
|
|
1432
|
+
return `npm install --save-dev ${pkgs}`;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function ensurePackageJson(cwd) {
|
|
1436
|
+
const pkgPath = resolve3(cwd, "package.json");
|
|
1437
|
+
if (existsSync2(pkgPath)) return false;
|
|
1438
|
+
const projectName = basename2(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1439
|
+
const minimalPkg = {
|
|
1440
|
+
name: projectName,
|
|
1441
|
+
private: true,
|
|
1442
|
+
type: "module",
|
|
1443
|
+
scripts: {
|
|
1444
|
+
"supercheck:deploy": "supercheck deploy",
|
|
1445
|
+
"supercheck:diff": "supercheck diff",
|
|
1446
|
+
"supercheck:pull": "supercheck pull",
|
|
1447
|
+
"supercheck:test": "supercheck test"
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
writeFileSync(pkgPath, JSON.stringify(minimalPkg, null, 2) + "\n", "utf-8");
|
|
1451
|
+
return true;
|
|
1452
|
+
}
|
|
1453
|
+
async function installDependencies(cwd, pm, opts = {}) {
|
|
1454
|
+
if (opts.skipInstall) {
|
|
1455
|
+
logger.info("Skipping dependency installation");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const devDeps = opts.packages ?? ["@supercheck/cli", "typescript", "@types/node"];
|
|
1459
|
+
const cmd = getInstallCommand(pm, devDeps);
|
|
1460
|
+
logger.info(`Installing dependencies with ${pm}...`);
|
|
1461
|
+
logger.debug(`Running: ${cmd}`);
|
|
1462
|
+
await withSpinner(
|
|
1463
|
+
`Installing ${devDeps.length} packages...`,
|
|
1464
|
+
async () => {
|
|
1465
|
+
const { execSync } = await import("child_process");
|
|
1466
|
+
try {
|
|
1467
|
+
execSync(cmd, {
|
|
1468
|
+
cwd,
|
|
1469
|
+
stdio: "pipe",
|
|
1470
|
+
timeout: 12e4,
|
|
1471
|
+
// 2 minute timeout
|
|
1472
|
+
env: { ...process.env, NODE_ENV: "development" }
|
|
1473
|
+
});
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1476
|
+
throw new CLIError(
|
|
1477
|
+
`Failed to install dependencies. Run manually:
|
|
1478
|
+
${cmd}
|
|
1354
1479
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
// Disable spinner in non-interactive modes
|
|
1363
|
-
isSilent: !isInteractive,
|
|
1364
|
-
// Use dots style for a clean look
|
|
1365
|
-
spinner: "dots"
|
|
1366
|
-
});
|
|
1367
|
-
return spinner;
|
|
1480
|
+
Error: ${message}`,
|
|
1481
|
+
1 /* GeneralError */
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
},
|
|
1485
|
+
{ successText: "Dependencies installed" }
|
|
1486
|
+
);
|
|
1368
1487
|
}
|
|
1369
|
-
async function
|
|
1370
|
-
const
|
|
1371
|
-
spinner.start();
|
|
1488
|
+
async function checkK6Binary() {
|
|
1489
|
+
const { execSync } = await import("child_process");
|
|
1372
1490
|
try {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
return
|
|
1377
|
-
} catch (error) {
|
|
1378
|
-
const failMessage = options?.failText ?? text.replace(/\.\.\.?$/, " failed");
|
|
1379
|
-
spinner.fail(failMessage);
|
|
1380
|
-
throw error;
|
|
1491
|
+
execSync("k6 version", { stdio: "ignore" });
|
|
1492
|
+
return true;
|
|
1493
|
+
} catch {
|
|
1494
|
+
return false;
|
|
1381
1495
|
}
|
|
1382
1496
|
}
|
|
1383
1497
|
|
|
@@ -1454,84 +1568,10 @@ supercheck.config.local.ts
|
|
|
1454
1568
|
supercheck.config.local.js
|
|
1455
1569
|
supercheck.config.local.mjs
|
|
1456
1570
|
`;
|
|
1457
|
-
function detectPackageManager(cwd) {
|
|
1458
|
-
if (existsSync2(resolve3(cwd, "bun.lockb")) || existsSync2(resolve3(cwd, "bun.lock"))) return "bun";
|
|
1459
|
-
if (existsSync2(resolve3(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1460
|
-
if (existsSync2(resolve3(cwd, "yarn.lock"))) return "yarn";
|
|
1461
|
-
return "npm";
|
|
1462
|
-
}
|
|
1463
|
-
function getInstallCommand(pm, packages) {
|
|
1464
|
-
const pkgs = packages.join(" ");
|
|
1465
|
-
switch (pm) {
|
|
1466
|
-
case "bun":
|
|
1467
|
-
return `bun add -d ${pkgs}`;
|
|
1468
|
-
case "pnpm":
|
|
1469
|
-
return `pnpm add -D ${pkgs}`;
|
|
1470
|
-
case "yarn":
|
|
1471
|
-
return `yarn add -D ${pkgs}`;
|
|
1472
|
-
case "npm":
|
|
1473
|
-
return `npm install --save-dev ${pkgs}`;
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
function ensurePackageJson(cwd) {
|
|
1477
|
-
const pkgPath = resolve3(cwd, "package.json");
|
|
1478
|
-
if (existsSync2(pkgPath)) return false;
|
|
1479
|
-
const projectName = basename2(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1480
|
-
const minimalPkg = {
|
|
1481
|
-
name: projectName,
|
|
1482
|
-
private: true,
|
|
1483
|
-
type: "module",
|
|
1484
|
-
scripts: {
|
|
1485
|
-
"supercheck:deploy": "supercheck deploy",
|
|
1486
|
-
"supercheck:diff": "supercheck diff",
|
|
1487
|
-
"supercheck:pull": "supercheck pull"
|
|
1488
|
-
}
|
|
1489
|
-
};
|
|
1490
|
-
writeFileSync(pkgPath, JSON.stringify(minimalPkg, null, 2) + "\n", "utf-8");
|
|
1491
|
-
return true;
|
|
1492
|
-
}
|
|
1493
|
-
async function installDependencies(cwd, pm, opts) {
|
|
1494
|
-
if (opts.skipInstall) {
|
|
1495
|
-
logger.info("Skipping dependency installation (--skip-install)");
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1498
|
-
const devDeps = ["@supercheck/cli", "typescript", "@types/node"];
|
|
1499
|
-
if (opts.playwright) {
|
|
1500
|
-
devDeps.push("@playwright/test");
|
|
1501
|
-
}
|
|
1502
|
-
const cmd = getInstallCommand(pm, devDeps);
|
|
1503
|
-
logger.info(`Installing dependencies with ${pm}...`);
|
|
1504
|
-
logger.debug(`Running: ${cmd}`);
|
|
1505
|
-
await withSpinner(
|
|
1506
|
-
`Installing ${devDeps.length} packages...`,
|
|
1507
|
-
async () => {
|
|
1508
|
-
const { execSync } = await import("child_process");
|
|
1509
|
-
try {
|
|
1510
|
-
execSync(cmd, {
|
|
1511
|
-
cwd,
|
|
1512
|
-
stdio: "pipe",
|
|
1513
|
-
timeout: 12e4,
|
|
1514
|
-
// 2 minute timeout
|
|
1515
|
-
env: { ...process.env, NODE_ENV: "development" }
|
|
1516
|
-
});
|
|
1517
|
-
} catch (err) {
|
|
1518
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1519
|
-
throw new CLIError(
|
|
1520
|
-
`Failed to install dependencies. Run manually:
|
|
1521
|
-
${cmd}
|
|
1522
|
-
|
|
1523
|
-
Error: ${message}`,
|
|
1524
|
-
1 /* GeneralError */
|
|
1525
|
-
);
|
|
1526
|
-
}
|
|
1527
|
-
},
|
|
1528
|
-
{ successText: "Dependencies installed" }
|
|
1529
|
-
);
|
|
1530
|
-
}
|
|
1531
1571
|
var initCommand = new Command4("init").description("Initialize a new Supercheck project with config and example tests").option("--force", "Overwrite existing config file").option("--skip-install", "Skip automatic dependency installation").option("--skip-examples", "Skip creating example test files").option("--pm <manager>", "Package manager to use (npm, yarn, pnpm, bun)").action(async (options) => {
|
|
1532
1572
|
const cwd = process.cwd();
|
|
1533
|
-
const configPath =
|
|
1534
|
-
if (
|
|
1573
|
+
const configPath = resolve4(cwd, "supercheck.config.ts");
|
|
1574
|
+
if (existsSync3(configPath) && !options.force) {
|
|
1535
1575
|
throw new CLIError(
|
|
1536
1576
|
"supercheck.config.ts already exists. Use --force to overwrite.",
|
|
1537
1577
|
3 /* ConfigError */
|
|
@@ -1540,11 +1580,11 @@ var initCommand = new Command4("init").description("Initialize a new Supercheck
|
|
|
1540
1580
|
logger.newline();
|
|
1541
1581
|
logger.header("Initializing Supercheck project...");
|
|
1542
1582
|
logger.newline();
|
|
1543
|
-
|
|
1583
|
+
writeFileSync2(configPath, CONFIG_TEMPLATE, "utf-8");
|
|
1544
1584
|
logger.success("Created supercheck.config.ts");
|
|
1545
|
-
const tsconfigPath =
|
|
1546
|
-
if (!
|
|
1547
|
-
|
|
1585
|
+
const tsconfigPath = resolve4(cwd, "tsconfig.supercheck.json");
|
|
1586
|
+
if (!existsSync3(tsconfigPath) || options.force) {
|
|
1587
|
+
writeFileSync2(tsconfigPath, SUPERCHECK_TSCONFIG, "utf-8");
|
|
1548
1588
|
logger.success("Created tsconfig.supercheck.json (IDE IntelliSense)");
|
|
1549
1589
|
}
|
|
1550
1590
|
const dirs = [
|
|
@@ -1553,32 +1593,32 @@ var initCommand = new Command4("init").description("Initialize a new Supercheck
|
|
|
1553
1593
|
"_supercheck_/status-pages"
|
|
1554
1594
|
];
|
|
1555
1595
|
for (const dir of dirs) {
|
|
1556
|
-
const dirPath =
|
|
1557
|
-
if (!
|
|
1596
|
+
const dirPath = resolve4(cwd, dir);
|
|
1597
|
+
if (!existsSync3(dirPath)) {
|
|
1558
1598
|
mkdirSync(dirPath, { recursive: true });
|
|
1559
1599
|
}
|
|
1560
1600
|
if (dir !== "_supercheck_/tests") {
|
|
1561
|
-
const gitkeepPath =
|
|
1562
|
-
if (!
|
|
1563
|
-
|
|
1601
|
+
const gitkeepPath = resolve4(dirPath, ".gitkeep");
|
|
1602
|
+
if (!existsSync3(gitkeepPath)) {
|
|
1603
|
+
writeFileSync2(gitkeepPath, "", "utf-8");
|
|
1564
1604
|
}
|
|
1565
1605
|
}
|
|
1566
1606
|
}
|
|
1567
1607
|
logger.success("Created _supercheck_/ directory structure");
|
|
1568
1608
|
if (!options.skipExamples) {
|
|
1569
|
-
const pwTestPath =
|
|
1570
|
-
if (!
|
|
1571
|
-
|
|
1609
|
+
const pwTestPath = resolve4(cwd, "_supercheck_/tests/homepage.pw.ts");
|
|
1610
|
+
if (!existsSync3(pwTestPath)) {
|
|
1611
|
+
writeFileSync2(pwTestPath, EXAMPLE_PW_TEST, "utf-8");
|
|
1572
1612
|
logger.success("Created _supercheck_/tests/homepage.pw.ts (Playwright example)");
|
|
1573
1613
|
}
|
|
1574
|
-
const k6TestPath =
|
|
1575
|
-
if (!
|
|
1576
|
-
|
|
1614
|
+
const k6TestPath = resolve4(cwd, "_supercheck_/tests/load-test.k6.ts");
|
|
1615
|
+
if (!existsSync3(k6TestPath)) {
|
|
1616
|
+
writeFileSync2(k6TestPath, EXAMPLE_K6_TEST, "utf-8");
|
|
1577
1617
|
logger.success("Created _supercheck_/tests/load-test.k6.ts (k6 example)");
|
|
1578
1618
|
}
|
|
1579
1619
|
}
|
|
1580
|
-
const gitignorePath =
|
|
1581
|
-
if (
|
|
1620
|
+
const gitignorePath = resolve4(cwd, ".gitignore");
|
|
1621
|
+
if (existsSync3(gitignorePath)) {
|
|
1582
1622
|
const content = readFileSync2(gitignorePath, "utf-8");
|
|
1583
1623
|
if (!content.includes("supercheck.config.local")) {
|
|
1584
1624
|
appendFileSync(gitignorePath, GITIGNORE_ADDITIONS, "utf-8");
|
|
@@ -1592,7 +1632,7 @@ var initCommand = new Command4("init").description("Initialize a new Supercheck
|
|
|
1592
1632
|
logger.success("Created package.json");
|
|
1593
1633
|
}
|
|
1594
1634
|
await installDependencies(cwd, pm, {
|
|
1595
|
-
|
|
1635
|
+
packages: options.skipExamples ? ["@supercheck/cli", "typescript", "@types/node"] : ["@supercheck/cli", "typescript", "@types/node", "@playwright/test"],
|
|
1596
1636
|
skipInstall: options.skipInstall ?? false
|
|
1597
1637
|
});
|
|
1598
1638
|
logger.newline();
|
|
@@ -1748,13 +1788,9 @@ jobCommand.command("update <id>").description("Update job configuration").option
|
|
|
1748
1788
|
});
|
|
1749
1789
|
jobCommand.command("delete <id>").description("Delete a job").option("--force", "Skip confirmation").action(async (id, options) => {
|
|
1750
1790
|
if (!options.force) {
|
|
1751
|
-
const {
|
|
1752
|
-
const
|
|
1753
|
-
|
|
1754
|
-
rl.question(`Are you sure you want to delete job ${id}? (y/N) `, resolve5);
|
|
1755
|
-
});
|
|
1756
|
-
rl.close();
|
|
1757
|
-
if (answer.toLowerCase() !== "y") {
|
|
1791
|
+
const { confirmPrompt: confirmPrompt2 } = await import("../prompt-BPDPYRS7.js");
|
|
1792
|
+
const confirmed = await confirmPrompt2(`Delete job ${id}?`, { default: false });
|
|
1793
|
+
if (!confirmed) {
|
|
1758
1794
|
logger.info("Aborted");
|
|
1759
1795
|
return;
|
|
1760
1796
|
}
|
|
@@ -1811,7 +1847,7 @@ jobCommand.command("trigger <id>").description("Trigger a job run").option("--wa
|
|
|
1811
1847
|
const timeoutMs = parseIntStrict(options.timeout, "--timeout", { min: 1 }) * 1e3;
|
|
1812
1848
|
const startTime = Date.now();
|
|
1813
1849
|
while (Date.now() - startTime < timeoutMs) {
|
|
1814
|
-
await new Promise((
|
|
1850
|
+
await new Promise((resolve6) => setTimeout(resolve6, 3e3));
|
|
1815
1851
|
const { data: runData } = await statusClient.get(
|
|
1816
1852
|
`/api/runs/${data.runId}`
|
|
1817
1853
|
);
|
|
@@ -2136,8 +2172,8 @@ testCommand.command("get <id>").description("Get test details").option("--includ
|
|
|
2136
2172
|
});
|
|
2137
2173
|
testCommand.command("create").description("Create a new test").requiredOption("--title <title>", "Test title").requiredOption("--file <path>", "Path to the test script file").option("--type <type>", "Test type (playwright, k6)", "playwright").option("--description <description>", "Test description").action(async (options) => {
|
|
2138
2174
|
const { readFileSync: readFileSync4 } = await import("fs");
|
|
2139
|
-
const { resolve:
|
|
2140
|
-
const filePath =
|
|
2175
|
+
const { resolve: resolve6 } = await import("path");
|
|
2176
|
+
const filePath = resolve6(process.cwd(), options.file);
|
|
2141
2177
|
let script;
|
|
2142
2178
|
try {
|
|
2143
2179
|
script = readFileSync4(filePath, "utf-8");
|
|
@@ -2164,8 +2200,8 @@ testCommand.command("update <id>").description("Update a test").option("--title
|
|
|
2164
2200
|
if (options.description !== void 0) body.description = options.description;
|
|
2165
2201
|
if (options.file) {
|
|
2166
2202
|
const { readFileSync: readFileSync4 } = await import("fs");
|
|
2167
|
-
const { resolve:
|
|
2168
|
-
const filePath =
|
|
2203
|
+
const { resolve: resolve6 } = await import("path");
|
|
2204
|
+
const filePath = resolve6(process.cwd(), options.file);
|
|
2169
2205
|
try {
|
|
2170
2206
|
const raw = readFileSync4(filePath, "utf-8");
|
|
2171
2207
|
body.script = Buffer2.from(raw, "utf-8").toString("base64");
|
|
@@ -2183,13 +2219,9 @@ testCommand.command("update <id>").description("Update a test").option("--title
|
|
|
2183
2219
|
});
|
|
2184
2220
|
testCommand.command("delete <id>").description("Delete a test").option("--force", "Skip confirmation").action(async (id, options) => {
|
|
2185
2221
|
if (!options.force) {
|
|
2186
|
-
const {
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
rl.question(`Are you sure you want to delete test ${id}? (y/N) `, resolve5);
|
|
2190
|
-
});
|
|
2191
|
-
rl.close();
|
|
2192
|
-
if (answer.toLowerCase() !== "y") {
|
|
2222
|
+
const { confirmPrompt: confirmPrompt2 } = await import("../prompt-BPDPYRS7.js");
|
|
2223
|
+
const confirmed = await confirmPrompt2(`Delete test ${id}?`, { default: false });
|
|
2224
|
+
if (!confirmed) {
|
|
2193
2225
|
logger.info("Aborted");
|
|
2194
2226
|
return;
|
|
2195
2227
|
}
|
|
@@ -2218,8 +2250,8 @@ testCommand.command("tags <id>").description("Get test tags").action(async (id)
|
|
|
2218
2250
|
});
|
|
2219
2251
|
testCommand.command("validate").description("Validate a test script").requiredOption("--file <path>", "Path to the test script file").option("--type <type>", "Test type (playwright, k6)", "playwright").action(async (options) => {
|
|
2220
2252
|
const { readFileSync: readFileSync4 } = await import("fs");
|
|
2221
|
-
const { resolve:
|
|
2222
|
-
const filePath =
|
|
2253
|
+
const { resolve: resolve6 } = await import("path");
|
|
2254
|
+
const filePath = resolve6(process.cwd(), options.file);
|
|
2223
2255
|
let script;
|
|
2224
2256
|
try {
|
|
2225
2257
|
script = readFileSync4(filePath, "utf-8");
|
|
@@ -2421,13 +2453,9 @@ monitorCommand.command("update <id>").description("Update a monitor").option("--
|
|
|
2421
2453
|
});
|
|
2422
2454
|
monitorCommand.command("delete <id>").description("Delete a monitor").option("--force", "Skip confirmation").action(async (id, options) => {
|
|
2423
2455
|
if (!options.force) {
|
|
2424
|
-
const {
|
|
2425
|
-
const
|
|
2426
|
-
|
|
2427
|
-
rl.question(`Are you sure you want to delete monitor ${id}? (y/N) `, resolve5);
|
|
2428
|
-
});
|
|
2429
|
-
rl.close();
|
|
2430
|
-
if (answer.toLowerCase() !== "y") {
|
|
2456
|
+
const { confirmPrompt: confirmPrompt2 } = await import("../prompt-BPDPYRS7.js");
|
|
2457
|
+
const confirmed = await confirmPrompt2(`Delete monitor ${id}?`, { default: false });
|
|
2458
|
+
if (!confirmed) {
|
|
2431
2459
|
logger.info("Aborted");
|
|
2432
2460
|
return;
|
|
2433
2461
|
}
|
|
@@ -2483,13 +2511,21 @@ varCommand.command("set <key> <value>").description("Create or update a variable
|
|
|
2483
2511
|
logger.success(`Variable "${key}" created`);
|
|
2484
2512
|
}
|
|
2485
2513
|
});
|
|
2486
|
-
varCommand.command("delete <key>").description("Delete a variable").action(async (key) => {
|
|
2514
|
+
varCommand.command("delete <key>").description("Delete a variable").option("--force", "Skip confirmation").action(async (key, options) => {
|
|
2487
2515
|
const client = createAuthenticatedClient();
|
|
2488
2516
|
const { data: variables } = await client.get("/api/variables");
|
|
2489
2517
|
const variable = variables.find((v) => v.key === key);
|
|
2490
2518
|
if (!variable) {
|
|
2491
2519
|
throw new CLIError(`Variable "${key}" not found`, 1 /* GeneralError */);
|
|
2492
2520
|
}
|
|
2521
|
+
if (!options.force) {
|
|
2522
|
+
const { confirmPrompt: confirmPrompt2 } = await import("../prompt-BPDPYRS7.js");
|
|
2523
|
+
const confirmed = await confirmPrompt2(`Delete variable "${key}"?`, { default: false });
|
|
2524
|
+
if (!confirmed) {
|
|
2525
|
+
logger.info("Aborted");
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2493
2529
|
await client.delete(`/api/variables/${variable.id}`);
|
|
2494
2530
|
logger.success(`Variable "${key}" deleted`);
|
|
2495
2531
|
});
|
|
@@ -2516,7 +2552,15 @@ tagCommand.command("create <name>").description("Create a new tag").option("--co
|
|
|
2516
2552
|
});
|
|
2517
2553
|
logger.success(`Tag "${name}" created (${data.id})`);
|
|
2518
2554
|
});
|
|
2519
|
-
tagCommand.command("delete <id>").description("Delete a tag").action(async (id) => {
|
|
2555
|
+
tagCommand.command("delete <id>").description("Delete a tag").option("--force", "Skip confirmation").action(async (id, options) => {
|
|
2556
|
+
if (!options.force) {
|
|
2557
|
+
const { confirmPrompt: confirmPrompt2 } = await import("../prompt-BPDPYRS7.js");
|
|
2558
|
+
const confirmed = await confirmPrompt2(`Delete tag ${id}?`, { default: false });
|
|
2559
|
+
if (!confirmed) {
|
|
2560
|
+
logger.info("Aborted");
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2520
2564
|
const client = createAuthenticatedClient();
|
|
2521
2565
|
await client.delete(`/api/tags/${id}`);
|
|
2522
2566
|
logger.success(`Tag ${id} deleted`);
|
|
@@ -2668,10 +2712,16 @@ var diffCommand = new Command11("diff").description("Preview changes between loc
|
|
|
2668
2712
|
const cwd = process.cwd();
|
|
2669
2713
|
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
2670
2714
|
const client = createAuthenticatedClient();
|
|
2671
|
-
logger.info("Comparing local config with remote project...");
|
|
2672
2715
|
logger.newline();
|
|
2673
|
-
const localResources =
|
|
2674
|
-
|
|
2716
|
+
const { localResources, remoteResources } = await withSpinner(
|
|
2717
|
+
"Comparing local and remote resources...",
|
|
2718
|
+
async () => {
|
|
2719
|
+
const local = buildLocalResources(config, cwd);
|
|
2720
|
+
const remote = await fetchRemoteResources(client);
|
|
2721
|
+
return { localResources: local, remoteResources: remote };
|
|
2722
|
+
},
|
|
2723
|
+
{ successText: "Resources compared" }
|
|
2724
|
+
);
|
|
2675
2725
|
logger.debug(`Local: ${localResources.length} resources, Remote: ${remoteResources.length} resources`);
|
|
2676
2726
|
const changes = reconcile(localResources, remoteResources);
|
|
2677
2727
|
formatChangePlan(changes);
|
|
@@ -2710,12 +2760,20 @@ async function applyChange(client, change) {
|
|
|
2710
2760
|
}
|
|
2711
2761
|
var deployCommand = new Command12("deploy").description("Push local config resources to the Supercheck project").option("--config <path>", "Path to config file").option("--dry-run", "Show what would change without applying").option("--force", "Skip confirmation prompt").option("--no-delete", "Do not delete remote resources missing from config").action(async (options) => {
|
|
2712
2762
|
const cwd = process.cwd();
|
|
2713
|
-
const { config
|
|
2763
|
+
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
2714
2764
|
const client = createAuthenticatedClient();
|
|
2715
|
-
logger.info(`Deploying from ${configPath ?? "config"}...`);
|
|
2716
2765
|
logger.newline();
|
|
2717
|
-
|
|
2718
|
-
|
|
2766
|
+
logger.header("Deploying from config...");
|
|
2767
|
+
logger.newline();
|
|
2768
|
+
const { localResources, remoteResources } = await withSpinner(
|
|
2769
|
+
"Comparing local and remote resources...",
|
|
2770
|
+
async () => {
|
|
2771
|
+
const local = buildLocalResources(config, cwd);
|
|
2772
|
+
const remote = await fetchRemoteResources(client);
|
|
2773
|
+
return { localResources: local, remoteResources: remote };
|
|
2774
|
+
},
|
|
2775
|
+
{ successText: "Resources compared" }
|
|
2776
|
+
);
|
|
2719
2777
|
let changes = reconcile(localResources, remoteResources);
|
|
2720
2778
|
if (options.delete === false) {
|
|
2721
2779
|
changes = changes.filter((c) => c.action !== "delete");
|
|
@@ -2731,13 +2789,8 @@ var deployCommand = new Command12("deploy").description("Push local config resou
|
|
|
2731
2789
|
return;
|
|
2732
2790
|
}
|
|
2733
2791
|
if (!options.force) {
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
const answer = await new Promise((resolve5) => {
|
|
2737
|
-
rl.question("Do you want to apply these changes? (y/N) ", resolve5);
|
|
2738
|
-
});
|
|
2739
|
-
rl.close();
|
|
2740
|
-
if (!answer || answer.toLowerCase() !== "y") {
|
|
2792
|
+
const confirmed = await confirmPrompt("Apply these changes?", { default: false });
|
|
2793
|
+
if (!confirmed) {
|
|
2741
2794
|
logger.info("Deploy aborted.");
|
|
2742
2795
|
return;
|
|
2743
2796
|
}
|
|
@@ -2893,9 +2946,12 @@ var destroyCommand = new Command13("destroy").description("Tear down managed res
|
|
|
2893
2946
|
const cwd = process.cwd();
|
|
2894
2947
|
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
2895
2948
|
const client = createAuthenticatedClient();
|
|
2896
|
-
logger.info("Scanning for managed resources to destroy...");
|
|
2897
2949
|
logger.newline();
|
|
2898
|
-
const managed = await
|
|
2950
|
+
const managed = await withSpinner(
|
|
2951
|
+
"Scanning for managed resources...",
|
|
2952
|
+
() => fetchManagedResources(client, config),
|
|
2953
|
+
{ successText: "Scan complete" }
|
|
2954
|
+
);
|
|
2899
2955
|
if (managed.length === 0) {
|
|
2900
2956
|
logger.success("No managed resources found on the remote project.");
|
|
2901
2957
|
return;
|
|
@@ -2949,8 +3005,8 @@ var destroyCommand = new Command13("destroy").description("Tear down managed res
|
|
|
2949
3005
|
|
|
2950
3006
|
// src/commands/pull.ts
|
|
2951
3007
|
import { Command as Command14 } from "commander";
|
|
2952
|
-
import { existsSync as
|
|
2953
|
-
import { resolve as
|
|
3008
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync3 } from "fs";
|
|
3009
|
+
import { resolve as resolve5, dirname as dirname2 } from "path";
|
|
2954
3010
|
import pc6 from "picocolors";
|
|
2955
3011
|
function mapTestType(apiType) {
|
|
2956
3012
|
if (apiType === "performance" || apiType === "k6") return "k6";
|
|
@@ -2964,15 +3020,15 @@ function testFilename(testId, testType) {
|
|
|
2964
3020
|
return `${testId}${ext}`;
|
|
2965
3021
|
}
|
|
2966
3022
|
function writeIfChanged(filePath, content) {
|
|
2967
|
-
if (
|
|
3023
|
+
if (existsSync4(filePath)) {
|
|
2968
3024
|
const existing = readFileSync3(filePath, "utf-8");
|
|
2969
3025
|
if (existing === content) return false;
|
|
2970
3026
|
}
|
|
2971
3027
|
const dir = dirname2(filePath);
|
|
2972
|
-
if (!
|
|
3028
|
+
if (!existsSync4(dir)) {
|
|
2973
3029
|
mkdirSync2(dir, { recursive: true });
|
|
2974
3030
|
}
|
|
2975
|
-
|
|
3031
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
2976
3032
|
return true;
|
|
2977
3033
|
}
|
|
2978
3034
|
function decodeScript(script) {
|
|
@@ -3056,8 +3112,8 @@ async function fetchStatusPages(client) {
|
|
|
3056
3112
|
}
|
|
3057
3113
|
}
|
|
3058
3114
|
function pullTests(tests, cwd, summary) {
|
|
3059
|
-
const testsDir =
|
|
3060
|
-
if (!
|
|
3115
|
+
const testsDir = resolve5(cwd, "_supercheck_/tests");
|
|
3116
|
+
if (!existsSync4(testsDir)) {
|
|
3061
3117
|
mkdirSync2(testsDir, { recursive: true });
|
|
3062
3118
|
}
|
|
3063
3119
|
for (const test of tests) {
|
|
@@ -3071,7 +3127,7 @@ function pullTests(tests, cwd, summary) {
|
|
|
3071
3127
|
summary.skipped++;
|
|
3072
3128
|
continue;
|
|
3073
3129
|
}
|
|
3074
|
-
const filePath =
|
|
3130
|
+
const filePath = resolve5(cwd, relPath);
|
|
3075
3131
|
const written = writeIfChanged(filePath, script);
|
|
3076
3132
|
if (written) {
|
|
3077
3133
|
logger.info(pc6.green(` + ${relPath}`));
|
|
@@ -3246,6 +3302,26 @@ function generateConfigContent(opts) {
|
|
|
3246
3302
|
}
|
|
3247
3303
|
parts.push("})");
|
|
3248
3304
|
parts.push("");
|
|
3305
|
+
parts.push(`/**`);
|
|
3306
|
+
parts.push(` * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
3307
|
+
parts.push(` * Getting Started`);
|
|
3308
|
+
parts.push(` * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
3309
|
+
parts.push(` *`);
|
|
3310
|
+
parts.push(` * 1. Install dependencies:`);
|
|
3311
|
+
parts.push(` * npm install`);
|
|
3312
|
+
parts.push(` * npx playwright install`);
|
|
3313
|
+
parts.push(` *`);
|
|
3314
|
+
parts.push(` * 2. Run tests:`);
|
|
3315
|
+
parts.push(` * npx supercheck test`);
|
|
3316
|
+
parts.push(` *`);
|
|
3317
|
+
parts.push(` * 3. Useful commands:`);
|
|
3318
|
+
parts.push(` * supercheck diff Preview changes against the cloud`);
|
|
3319
|
+
parts.push(` * supercheck deploy Push local config to Supercheck`);
|
|
3320
|
+
parts.push(` * supercheck pull Sync cloud resources locally`);
|
|
3321
|
+
parts.push(` *`);
|
|
3322
|
+
parts.push(` * Documentation: https://docs.supercheck.io`);
|
|
3323
|
+
parts.push(` */`);
|
|
3324
|
+
parts.push("");
|
|
3249
3325
|
return parts.join("\n");
|
|
3250
3326
|
}
|
|
3251
3327
|
function formatArray(arr, baseIndent) {
|
|
@@ -3374,13 +3450,9 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3374
3450
|
logger.info(`Found ${pc6.bold(String(totalResources))} resources to pull.`);
|
|
3375
3451
|
logger.info("This will write test scripts and update supercheck.config.ts.");
|
|
3376
3452
|
logger.newline();
|
|
3377
|
-
const {
|
|
3378
|
-
const
|
|
3379
|
-
|
|
3380
|
-
rl.question("Continue? (y/N) ", resolvePrompt);
|
|
3381
|
-
});
|
|
3382
|
-
rl.close();
|
|
3383
|
-
if (!answer || answer.toLowerCase() !== "y") {
|
|
3453
|
+
const { confirmPrompt: confirmPrompt2 } = await import("../prompt-BPDPYRS7.js");
|
|
3454
|
+
const confirmed = await confirmPrompt2("Continue?", { default: true });
|
|
3455
|
+
if (!confirmed) {
|
|
3384
3456
|
logger.info("Pull aborted.");
|
|
3385
3457
|
return;
|
|
3386
3458
|
}
|
|
@@ -3452,7 +3524,7 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3452
3524
|
tags: tagDefs,
|
|
3453
3525
|
statusPages: statusPageDefs
|
|
3454
3526
|
});
|
|
3455
|
-
const configPath =
|
|
3527
|
+
const configPath = resolve5(cwd, "supercheck.config.ts");
|
|
3456
3528
|
const configChanged = writeIfChanged(configPath, configContent);
|
|
3457
3529
|
if (configChanged) {
|
|
3458
3530
|
logger.header("Config:");
|
|
@@ -3490,13 +3562,40 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3490
3562
|
if (resultParts.length > 0) {
|
|
3491
3563
|
logger.success(`Pull complete: ${resultParts.join(", ")}`);
|
|
3492
3564
|
}
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3565
|
+
}
|
|
3566
|
+
logger.newline();
|
|
3567
|
+
const createdPkg = ensurePackageJson(cwd);
|
|
3568
|
+
if (createdPkg) logger.success("Created package.json");
|
|
3569
|
+
if (!options.force) {
|
|
3570
|
+
const { confirmPrompt: confirmPrompt2 } = await import("../prompt-BPDPYRS7.js");
|
|
3571
|
+
const shouldInstall = await confirmPrompt2("Install dependencies?", { default: true });
|
|
3572
|
+
if (shouldInstall) {
|
|
3573
|
+
const pm = detectPackageManager(cwd);
|
|
3574
|
+
const hasPlaywrightTests = tests.some((t) => t.type !== "performance" && t.type !== "k6");
|
|
3575
|
+
const packages = ["@supercheck/cli", "typescript", "@types/node"];
|
|
3576
|
+
if (hasPlaywrightTests) {
|
|
3577
|
+
packages.push("@playwright/test");
|
|
3578
|
+
}
|
|
3579
|
+
await installDependencies(cwd, pm, {
|
|
3580
|
+
packages,
|
|
3581
|
+
skipInstall: false
|
|
3582
|
+
});
|
|
3498
3583
|
}
|
|
3499
3584
|
}
|
|
3585
|
+
const hasK6Tests = tests.some((t) => t.type === "performance" || t.type === "k6");
|
|
3586
|
+
if (hasK6Tests) {
|
|
3587
|
+
const hasK6 = await checkK6Binary();
|
|
3588
|
+
if (!hasK6) {
|
|
3589
|
+
logger.warn("k6 binary not found in PATH.");
|
|
3590
|
+
logger.info("Please install k6 to run performance tests: https://grafana.com/docs/k6/latest/set-up/install-k6/");
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
if (totalErrors > 0) {
|
|
3594
|
+
throw new CLIError(
|
|
3595
|
+
`Pull completed with ${totalErrors} error(s)`,
|
|
3596
|
+
1 /* GeneralError */
|
|
3597
|
+
);
|
|
3598
|
+
}
|
|
3500
3599
|
logger.newline();
|
|
3501
3600
|
logger.info("Tip: Review the changes, then commit to version control.");
|
|
3502
3601
|
logger.info(" Run `supercheck diff` to compare local vs remote.");
|
|
@@ -3578,13 +3677,9 @@ notificationCommand.command("update <id>").description("Update a notification pr
|
|
|
3578
3677
|
});
|
|
3579
3678
|
notificationCommand.command("delete <id>").description("Delete a notification provider").option("--force", "Skip confirmation").action(async (id, options) => {
|
|
3580
3679
|
if (!options.force) {
|
|
3581
|
-
const {
|
|
3582
|
-
const
|
|
3583
|
-
|
|
3584
|
-
rl.question(`Are you sure you want to delete notification provider ${id}? (y/N) `, resolve5);
|
|
3585
|
-
});
|
|
3586
|
-
rl.close();
|
|
3587
|
-
if (answer.toLowerCase() !== "y") {
|
|
3680
|
+
const { confirmPrompt: confirmPrompt2 } = await import("../prompt-BPDPYRS7.js");
|
|
3681
|
+
const confirmed = await confirmPrompt2(`Delete notification provider ${id}?`, { default: false });
|
|
3682
|
+
if (!confirmed) {
|
|
3588
3683
|
logger.info("Aborted");
|
|
3589
3684
|
return;
|
|
3590
3685
|
}
|