hyouji 0.0.5 → 0.0.7

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 (3) hide show
  1. package/README.md +5 -1
  2. package/dist/index.js +353 -104
  3. package/package.json +7 -1
package/README.md CHANGED
@@ -16,7 +16,8 @@ https://levelup.gitconnected.com/create-github-labels-from-terminal-158d4868fab
16
16
 
17
17
  ![hyouji_terminal](./hyouji.png)
18
18
 
19
- https://user-images.githubusercontent.com/474225/130368605-b5c6410f-53f6-4ef0-b321-8950edeebf7d.mov
19
+ https://github.com/user-attachments/assets/739f185a-1bd0-411b-8947-dd4600c452c8
20
+
20
21
 
21
22
  ### Labels API
22
23
 
@@ -76,6 +77,9 @@ hyouji
76
77
  On your first run, you'll be prompted to enter:
77
78
 
78
79
  - **GitHub Personal Token** - Generate one [here](https://github.com/settings/tokens) with `repo` scope
80
+ <img width="792" height="564" alt="github_token" src="https://github.com/user-attachments/assets/e460738f-833a-4158-a8ba-61752beaad72" />
81
+
82
+
79
83
  - **GitHub Username** - Your GitHub account name
80
84
 
81
85
  These credentials will be securely saved and reused for future sessions.
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { Octokit } from "@octokit/core";
3
2
  import chalk from "chalk";
4
- import prompts from "prompts";
5
3
  import { renderFilled } from "oh-my-logo";
6
4
  import * as fs from "fs";
7
5
  import { promises, existsSync } from "fs";
8
6
  import { homedir } from "os";
9
- import { join } from "path";
7
+ import { join, dirname } from "path";
10
8
  import { createHash, randomBytes, createCipheriv, createDecipheriv } from "crypto";
9
+ import prompts from "prompts";
10
+ import { Octokit } from "@octokit/core";
11
+ import { exec } from "child_process";
12
+ import { promisify } from "util";
11
13
  const githubConfigs = [
12
14
  {
13
15
  type: "password",
@@ -280,17 +282,34 @@ const createLabels = async (configs2) => {
280
282
  log$2("Created all labels");
281
283
  log$2(chalk.bgBlueBright(extraGuideText));
282
284
  };
283
- const deleteLabel = (configs2, labelNames) => {
284
- labelNames.forEach(async (labelName) => {
285
- await configs2.octokit.request(
286
- "DELETE /repos/{owner}/{repo}/labels/{name}",
287
- {
288
- owner: configs2.owner,
289
- repo: configs2.repo,
290
- name: labelName
285
+ const deleteLabel = async (configs2, labelNames) => {
286
+ for (const labelName of labelNames) {
287
+ try {
288
+ const resp = await configs2.octokit.request(
289
+ "DELETE /repos/{owner}/{repo}/labels/{name}",
290
+ {
291
+ owner: configs2.owner,
292
+ repo: configs2.repo,
293
+ name: labelName
294
+ }
295
+ );
296
+ if (resp.status === 204) {
297
+ log$2(chalk.green(`${resp.status}: Deleted ${labelName}`));
298
+ } else {
299
+ log$2(chalk.yellow(`${resp.status}: Something wrong with ${labelName}`));
291
300
  }
292
- );
293
- });
301
+ } catch (error) {
302
+ if (error && typeof error === "object" && "status" in error && error.status === 404) {
303
+ log$2(chalk.red(`404: Label "${labelName}" not found`));
304
+ } else {
305
+ log$2(
306
+ chalk.red(
307
+ `Error deleting label "${labelName}": ${error instanceof Error ? error.message : "Unknown error"}`
308
+ )
309
+ );
310
+ }
311
+ }
312
+ }
294
313
  };
295
314
  const getLabels = async (configs2) => {
296
315
  const resp = await configs2.octokit.request(
@@ -310,18 +329,39 @@ const getLabels = async (configs2) => {
310
329
  };
311
330
  const deleteLabels = async (configs2) => {
312
331
  const names = await getLabels(configs2);
313
- names.forEach(async (name) => {
314
- await configs2.octokit.request(
315
- "DELETE /repos/{owner}/{repo}/labels/{name}",
316
- {
317
- owner: configs2.owner,
318
- repo: configs2.repo,
319
- name
332
+ if (names.length === 0) {
333
+ log$2(chalk.yellow("No labels found to delete"));
334
+ return;
335
+ }
336
+ log$2(chalk.blue(`Deleting ${names.length} labels...`));
337
+ for (const name of names) {
338
+ try {
339
+ const resp = await configs2.octokit.request(
340
+ "DELETE /repos/{owner}/{repo}/labels/{name}",
341
+ {
342
+ owner: configs2.owner,
343
+ repo: configs2.repo,
344
+ name
345
+ }
346
+ );
347
+ if (resp.status === 204) {
348
+ log$2(chalk.green(`${resp.status}: Deleted ${name}`));
349
+ } else {
350
+ log$2(chalk.yellow(`${resp.status}: Something wrong with ${name}`));
320
351
  }
321
- );
322
- });
323
- log$2("");
324
- names.forEach((label) => log$2(chalk.bgGreen(`deleted ${label}`)));
352
+ } catch (error) {
353
+ if (error && typeof error === "object" && "status" in error && error.status === 404) {
354
+ log$2(chalk.red(`404: Label "${name}" not found`));
355
+ } else {
356
+ log$2(
357
+ chalk.red(
358
+ `Error deleting label "${name}": ${error instanceof Error ? error.message : "Unknown error"}`
359
+ )
360
+ );
361
+ }
362
+ }
363
+ }
364
+ log$2(chalk.blue("Finished deleting labels"));
325
365
  log$2(chalk.bgBlueBright(extraGuideText));
326
366
  };
327
367
  const _CryptoUtils = class _CryptoUtils {
@@ -1071,6 +1111,177 @@ const getTargetLabel = async () => {
1071
1111
  const response = await prompts(deleteLabel$1);
1072
1112
  return [response.name];
1073
1113
  };
1114
+ const execAsync = promisify(exec);
1115
+ const GIT_COMMAND_TIMEOUT_MS = 5e3;
1116
+ class GitRepositoryDetector {
1117
+ /**
1118
+ * Detects Git repository information from the current working directory
1119
+ * @param cwd - Current working directory (defaults to process.cwd())
1120
+ * @returns Promise<GitDetectionResult>
1121
+ */
1122
+ static async detectRepository(cwd) {
1123
+ const workingDir = cwd || process.cwd();
1124
+ try {
1125
+ const gitRoot = await this.findGitRoot(workingDir);
1126
+ if (!gitRoot) {
1127
+ return {
1128
+ isGitRepository: false,
1129
+ error: "Not a Git repository"
1130
+ };
1131
+ }
1132
+ const remotes = await this.getAllRemotes(gitRoot);
1133
+ if (remotes.length === 0) {
1134
+ return {
1135
+ isGitRepository: true,
1136
+ error: "No remotes configured"
1137
+ };
1138
+ }
1139
+ let remoteUrl = null;
1140
+ let detectionMethod = "origin";
1141
+ if (remotes.includes("origin")) {
1142
+ remoteUrl = await this.getRemoteUrl(gitRoot, "origin");
1143
+ }
1144
+ if (!remoteUrl && remotes.length > 0) {
1145
+ remoteUrl = await this.getRemoteUrl(gitRoot, remotes[0]);
1146
+ detectionMethod = "first-remote";
1147
+ }
1148
+ if (!remoteUrl) {
1149
+ return {
1150
+ isGitRepository: true,
1151
+ error: "Could not retrieve remote URL"
1152
+ };
1153
+ }
1154
+ const parsedUrl = this.parseGitUrl(remoteUrl);
1155
+ if (!parsedUrl) {
1156
+ return {
1157
+ isGitRepository: true,
1158
+ error: "Could not parse remote URL"
1159
+ };
1160
+ }
1161
+ return {
1162
+ isGitRepository: true,
1163
+ repositoryInfo: {
1164
+ owner: parsedUrl.owner,
1165
+ repo: parsedUrl.repo,
1166
+ remoteUrl,
1167
+ detectionMethod
1168
+ }
1169
+ };
1170
+ } catch (err) {
1171
+ return {
1172
+ isGitRepository: false,
1173
+ error: err instanceof Error ? err.message : "Unknown error occurred"
1174
+ };
1175
+ }
1176
+ }
1177
+ /**
1178
+ * Finds the Git root directory by traversing up the directory tree
1179
+ * @param startPath - Starting directory path
1180
+ * @returns Promise<string | null> - Git root path or null if not found
1181
+ */
1182
+ static async findGitRoot(startPath) {
1183
+ let currentPath = startPath;
1184
+ while (currentPath !== dirname(currentPath)) {
1185
+ const gitPath = join(currentPath, ".git");
1186
+ if (existsSync(gitPath)) {
1187
+ return currentPath;
1188
+ }
1189
+ currentPath = dirname(currentPath);
1190
+ }
1191
+ return null;
1192
+ }
1193
+ /**
1194
+ * Gets the URL for a specific Git remote
1195
+ * @param gitRoot - Git repository root directory
1196
+ * @param remoteName - Name of the remote (e.g., 'origin')
1197
+ * @returns Promise<string | null> - Remote URL or null if not found
1198
+ */
1199
+ static async getRemoteUrl(gitRoot, remoteName) {
1200
+ try {
1201
+ const { stdout } = await execAsync(`git remote get-url ${remoteName}`, {
1202
+ cwd: gitRoot,
1203
+ timeout: GIT_COMMAND_TIMEOUT_MS
1204
+ });
1205
+ return stdout.trim() || null;
1206
+ } catch {
1207
+ return null;
1208
+ }
1209
+ }
1210
+ /**
1211
+ * Parses a Git URL to extract owner and repository name
1212
+ * @param url - Git remote URL
1213
+ * @returns Object with owner and repo or null if parsing fails
1214
+ */
1215
+ static parseGitUrl(url) {
1216
+ if (!url || typeof url !== "string" || url.trim().length === 0) {
1217
+ return null;
1218
+ }
1219
+ const trimmedUrl = url.trim();
1220
+ try {
1221
+ const sshMatch = trimmedUrl.match(
1222
+ /^git@github\.com:([^/\s:]+)\/([^/\s:]+?)(?:\.git)?$/
1223
+ );
1224
+ if (sshMatch) {
1225
+ const owner = sshMatch[1];
1226
+ const repo = sshMatch[2];
1227
+ if (this.isValidGitHubIdentifier(owner) && this.isValidGitHubIdentifier(repo)) {
1228
+ return { owner, repo };
1229
+ }
1230
+ }
1231
+ const httpsMatch = trimmedUrl.match(
1232
+ /^https:\/\/github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?(?:\/)?$/
1233
+ );
1234
+ if (httpsMatch) {
1235
+ const owner = httpsMatch[1];
1236
+ const repo = httpsMatch[2];
1237
+ if (this.isValidGitHubIdentifier(owner) && this.isValidGitHubIdentifier(repo)) {
1238
+ return { owner, repo };
1239
+ }
1240
+ }
1241
+ const httpMatch = trimmedUrl.match(
1242
+ /^http:\/\/github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?(?:\/)?$/
1243
+ );
1244
+ if (httpMatch) {
1245
+ const owner = httpMatch[1];
1246
+ const repo = httpMatch[2];
1247
+ if (this.isValidGitHubIdentifier(owner) && this.isValidGitHubIdentifier(repo)) {
1248
+ return { owner, repo };
1249
+ }
1250
+ }
1251
+ } catch {
1252
+ return null;
1253
+ }
1254
+ return null;
1255
+ }
1256
+ /**
1257
+ * Validates if a string is a valid GitHub identifier (username or repository name)
1258
+ * @param identifier - The identifier to validate
1259
+ * @returns boolean - True if valid, false otherwise
1260
+ */
1261
+ static isValidGitHubIdentifier(identifier) {
1262
+ if (!identifier || typeof identifier !== "string") {
1263
+ return false;
1264
+ }
1265
+ const GITHUB_IDENTIFIER_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
1266
+ return identifier.length >= 1 && identifier.length <= 39 && GITHUB_IDENTIFIER_REGEX.test(identifier) && !identifier.includes("--");
1267
+ }
1268
+ /**
1269
+ * Gets all configured Git remotes
1270
+ * @param gitRoot - Git repository root directory
1271
+ * @returns Promise<string[]> - Array of remote names
1272
+ */
1273
+ static async getAllRemotes(gitRoot) {
1274
+ try {
1275
+ const { stdout } = await execAsync("git remote", {
1276
+ cwd: gitRoot,
1277
+ timeout: GIT_COMMAND_TIMEOUT_MS
1278
+ });
1279
+ return stdout.trim().split("\n").filter((remote) => remote.length > 0);
1280
+ } catch {
1281
+ return [];
1282
+ }
1283
+ }
1284
+ }
1074
1285
  const getGitHubConfigs = async () => {
1075
1286
  var _a, _b;
1076
1287
  const configManager2 = new ConfigManager();
@@ -1092,6 +1303,50 @@ const getGitHubConfigs = async () => {
1092
1303
  };
1093
1304
  }
1094
1305
  if (validationResult.config && !validationResult.shouldPromptForCredentials) {
1306
+ try {
1307
+ const detectionResult = await GitRepositoryDetector.detectRepository();
1308
+ if (detectionResult.isGitRepository && detectionResult.repositoryInfo) {
1309
+ console.log(
1310
+ chalk.green(
1311
+ `✓ Detected repository: ${detectionResult.repositoryInfo.owner}/${detectionResult.repositoryInfo.repo}`
1312
+ )
1313
+ );
1314
+ console.log(
1315
+ chalk.gray(
1316
+ ` Detection method: ${detectionResult.repositoryInfo.detectionMethod === "origin" ? "origin remote" : "first available remote"}`
1317
+ )
1318
+ );
1319
+ const octokit3 = new Octokit({
1320
+ auth: validationResult.config.token
1321
+ });
1322
+ return {
1323
+ octokit: octokit3,
1324
+ owner: detectionResult.repositoryInfo.owner,
1325
+ repo: detectionResult.repositoryInfo.repo,
1326
+ fromSavedConfig: true,
1327
+ autoDetected: true,
1328
+ detectionMethod: detectionResult.repositoryInfo.detectionMethod
1329
+ };
1330
+ } else {
1331
+ if (detectionResult.error) {
1332
+ console.log(
1333
+ chalk.yellow(
1334
+ `⚠️ Repository auto-detection failed: ${detectionResult.error}`
1335
+ )
1336
+ );
1337
+ }
1338
+ console.log(chalk.gray(" Falling back to manual input..."));
1339
+ }
1340
+ } catch (error) {
1341
+ console.log(
1342
+ chalk.yellow(
1343
+ "⚠️ Repository auto-detection failed, falling back to manual input"
1344
+ )
1345
+ );
1346
+ if (error instanceof Error) {
1347
+ console.log(chalk.gray(` Error: ${error.message}`));
1348
+ }
1349
+ }
1095
1350
  const repoResponse = await prompts([
1096
1351
  {
1097
1352
  type: "text",
@@ -1106,7 +1361,9 @@ const getGitHubConfigs = async () => {
1106
1361
  octokit: octokit2,
1107
1362
  owner: validationResult.config.owner,
1108
1363
  repo: repoResponse.repo,
1109
- fromSavedConfig: true
1364
+ fromSavedConfig: true,
1365
+ autoDetected: false,
1366
+ detectionMethod: "manual"
1110
1367
  };
1111
1368
  }
1112
1369
  const promptConfig = [...githubConfigs];
@@ -1158,7 +1415,9 @@ const getGitHubConfigs = async () => {
1158
1415
  octokit,
1159
1416
  owner: response.owner,
1160
1417
  repo: response.repo,
1161
- fromSavedConfig: false
1418
+ fromSavedConfig: false,
1419
+ autoDetected: false,
1420
+ detectionMethod: "manual"
1162
1421
  };
1163
1422
  };
1164
1423
  const getJsonFilePath = async () => {
@@ -1177,33 +1436,6 @@ const selectAction = async () => {
1177
1436
  const log = console.log;
1178
1437
  let firstStart = true;
1179
1438
  const configManager = new ConfigManager();
1180
- const setupConfigs = async () => {
1181
- console.log(initialText);
1182
- if (firstStart) {
1183
- await configManager.migrateToEncrypted();
1184
- }
1185
- const config = await getGitHubConfigs();
1186
- if (!config.octokit || !config.owner || !config.repo) {
1187
- throw new Error("Invalid configuration: missing required fields");
1188
- }
1189
- try {
1190
- await config.octokit.request("GET /user");
1191
- } catch (error) {
1192
- if (config.fromSavedConfig) {
1193
- console.log(
1194
- chalk.yellow(
1195
- "Saved credentials are invalid. Please provide new credentials."
1196
- )
1197
- );
1198
- await configManager.clearConfig();
1199
- return setupConfigs();
1200
- }
1201
- throw new Error(
1202
- `GitHub API authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`
1203
- );
1204
- }
1205
- return config;
1206
- };
1207
1439
  const displaySettings = async () => {
1208
1440
  log(chalk.cyan("\n=== Current Settings ==="));
1209
1441
  const configPath = configManager.getConfigPath();
@@ -1247,9 +1479,9 @@ const displaySettings = async () => {
1247
1479
  log(chalk.cyan("========================\n"));
1248
1480
  };
1249
1481
  let configs;
1250
- const main = async () => {
1482
+ const initializeConfigs = async () => {
1251
1483
  let hasValidConfig = false;
1252
- if (firstStart && configManager.configExists()) {
1484
+ if (configManager.configExists()) {
1253
1485
  try {
1254
1486
  const existingConfig = await configManager.loadValidatedConfig();
1255
1487
  if (existingConfig && existingConfig.config && !existingConfig.shouldPromptForCredentials) {
@@ -1268,58 +1500,75 @@ const main = async () => {
1268
1500
  `Please go to ${linkToPersonalToken} and generate a personal token!`
1269
1501
  )
1270
1502
  );
1271
- return;
1503
+ return null;
1272
1504
  }
1273
1505
  }
1274
- if (firstStart) {
1506
+ try {
1507
+ const asciiText = await getAsciiText();
1508
+ if (asciiText != null) {
1509
+ log(asciiText);
1510
+ }
1511
+ } catch (error) {
1512
+ console.warn("Failed to display ASCII art, continuing...");
1513
+ console.error("Error:", error);
1514
+ }
1515
+ try {
1516
+ console.log(initialText);
1517
+ if (firstStart) {
1518
+ await configManager.migrateToEncrypted();
1519
+ }
1520
+ const config = await getGitHubConfigs();
1521
+ if (!config.octokit || !config.owner || !config.repo) {
1522
+ throw new Error("Invalid configuration: missing required fields");
1523
+ }
1275
1524
  try {
1276
- const asciiText = await getAsciiText();
1277
- if (asciiText != null) {
1278
- log(asciiText);
1279
- }
1525
+ await config.octokit.request("GET /user");
1280
1526
  } catch (error) {
1281
- console.warn("Failed to display ASCII art, continuing...");
1282
- console.error("Error:", error);
1283
- }
1284
- if (hasValidConfig) {
1285
- try {
1286
- const existingConfig = await configManager.loadValidatedConfig();
1287
- if (existingConfig && existingConfig.config) {
1288
- const repoResponse = await prompts([
1289
- {
1290
- type: "text",
1291
- name: "repo",
1292
- message: "Please type your target repo name"
1293
- }
1294
- ]);
1295
- configs = {
1296
- octokit: new Octokit({ auth: existingConfig.config.token }),
1297
- owner: existingConfig.config.owner,
1298
- repo: repoResponse.repo,
1299
- fromSavedConfig: true
1300
- };
1301
- log(chalk.green(`Using saved configuration for ${configs.owner}`));
1302
- } else {
1303
- configs = await setupConfigs();
1304
- }
1305
- } catch (error) {
1306
- console.error("Error:", error);
1307
- configs = await setupConfigs();
1308
- }
1309
- } else {
1310
- try {
1311
- configs = await setupConfigs();
1312
- if (configs.fromSavedConfig) {
1313
- log(chalk.green(`Using saved configuration for ${configs.owner}`));
1314
- }
1315
- } catch (error) {
1316
- log(
1317
- chalk.red(
1318
- `Configuration error: ${error instanceof Error ? error.message : "Unknown error"}`
1527
+ if (config.fromSavedConfig) {
1528
+ console.log(
1529
+ chalk.yellow(
1530
+ "Saved credentials are invalid. Please provide new credentials."
1319
1531
  )
1320
1532
  );
1321
- return;
1533
+ await configManager.clearConfig();
1534
+ return initializeConfigs();
1322
1535
  }
1536
+ throw new Error(
1537
+ `GitHub API authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`
1538
+ );
1539
+ }
1540
+ if (config.fromSavedConfig) {
1541
+ log(chalk.green(`✓ Using saved configuration for ${config.owner}`));
1542
+ }
1543
+ if (config.autoDetected) {
1544
+ log(
1545
+ chalk.green(
1546
+ `✓ Repository auto-detected: ${config.owner}/${config.repo}`
1547
+ )
1548
+ );
1549
+ const detectionMethodText = config.detectionMethod === "origin" ? "origin remote" : config.detectionMethod === "first-remote" ? "first available remote" : "manual input";
1550
+ log(chalk.gray(` Detection method: ${detectionMethodText}`));
1551
+ } else if (config.detectionMethod === "manual") {
1552
+ log(
1553
+ chalk.blue(`✓ Repository configured: ${config.owner}/${config.repo}`)
1554
+ );
1555
+ log(chalk.gray(` Input method: manual`));
1556
+ }
1557
+ return config;
1558
+ } catch (error) {
1559
+ log(
1560
+ chalk.red(
1561
+ `Configuration error: ${error instanceof Error ? error.message : "Unknown error"}`
1562
+ )
1563
+ );
1564
+ return null;
1565
+ }
1566
+ };
1567
+ const main = async () => {
1568
+ if (firstStart) {
1569
+ configs = await initializeConfigs();
1570
+ if (!configs) {
1571
+ return;
1323
1572
  }
1324
1573
  }
1325
1574
  let selectedIndex = await selectAction();
@@ -1329,23 +1578,23 @@ const main = async () => {
1329
1578
  switch (selectedIndex) {
1330
1579
  case 0: {
1331
1580
  const newLabel2 = await getNewLabel();
1332
- createLabel(configs, newLabel2);
1581
+ await createLabel(configs, newLabel2);
1333
1582
  firstStart = firstStart && false;
1334
1583
  break;
1335
1584
  }
1336
1585
  case 1: {
1337
- createLabels(configs);
1586
+ await createLabels(configs);
1338
1587
  firstStart = firstStart && false;
1339
1588
  break;
1340
1589
  }
1341
1590
  case 2: {
1342
1591
  const targetLabel = await getTargetLabel();
1343
- deleteLabel(configs, targetLabel);
1592
+ await deleteLabel(configs, targetLabel);
1344
1593
  firstStart = firstStart && false;
1345
1594
  break;
1346
1595
  }
1347
1596
  case 3: {
1348
- deleteLabels(configs);
1597
+ await deleteLabels(configs);
1349
1598
  firstStart = firstStart && false;
1350
1599
  break;
1351
1600
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyouji",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Hyouji (表示) — A command-line tool for organizing and displaying GitHub labels with clarity and harmony.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -35,6 +35,12 @@
35
35
  "test:coverage": "vitest run --coverage",
36
36
  "test:lint": "eslint src --ext .ts",
37
37
  "test:prettier": "prettier \"src/**/*.ts\" --list-different",
38
+ "test:error-handling": "node tests/scripts/error-handling/run-all.cjs",
39
+ "test:config": "node tests/scripts/config/run-all.cjs",
40
+ "test:integration": "node tests/scripts/run-integration.cjs",
41
+ "test:auto-detection": "node tests/integration/auto-detection/integration-flow.cjs",
42
+ "test:verification": "node tests/scripts/verification/run-all.cjs",
43
+ "test:all-custom": "npm run test:error-handling && npm run test:config && npm run test:integration && npm run test:verification",
38
44
  "check-cli": "run-s test diff-integration-tests check-integration-tests",
39
45
  "check-integration-tests": "run-s check-integration-test:*",
40
46
  "diff-integration-tests": "mkdir -p diff && rm -rf diff/test && cp -r test diff/test && rm -rf diff/test/test-*/.git && cd diff && git init --quiet && git add -A && git commit --quiet --no-verify --allow-empty -m 'WIP' && echo '\\n\\nCommitted most recent integration test output in the \"diff\" directory. Review the changes with \"cd diff && git diff HEAD\" or your preferred git diff viewer.'",