hyouji 0.0.16 → 0.1.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/README.md +3 -0
- package/dist/index.js +478 -87
- package/package.json +12 -15
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Hyouji(表示) GitHub Label Manager
|
|
2
2
|
|
|
3
|
+
<img width="2816" height="1536" alt="hyouji_generated_image" src="https://github.com/user-attachments/assets/636382d1-a718-4289-81d7-2943f1962ce8" />
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
### article
|
|
4
7
|
|
|
5
8
|
https://levelup.gitconnected.com/create-github-labels-from-terminal-158d4868fab
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,9 @@ import { homedir } from "os";
|
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { join, dirname } from "path";
|
|
9
9
|
import { createHash, randomBytes, createCipheriv, createDecipheriv } from "crypto";
|
|
10
|
-
import
|
|
10
|
+
import { stdout, stdin } from "node:process";
|
|
11
|
+
import { emitKeypressEvents } from "node:readline";
|
|
12
|
+
import { createInterface } from "node:readline/promises";
|
|
11
13
|
import YAML from "yaml";
|
|
12
14
|
import { Octokit } from "@octokit/core";
|
|
13
15
|
import { exec } from "child_process";
|
|
@@ -47,26 +49,16 @@ const newLabel = [
|
|
|
47
49
|
}
|
|
48
50
|
];
|
|
49
51
|
const deleteLabel$1 = {
|
|
50
|
-
type: "text",
|
|
51
|
-
name: "name",
|
|
52
52
|
message: "Please type label name you want to delete"
|
|
53
53
|
};
|
|
54
54
|
const labelFilePath = {
|
|
55
|
-
type: "text",
|
|
56
|
-
name: "filePath",
|
|
57
55
|
message: "Please type the path to your JSON or YAML file"
|
|
58
56
|
};
|
|
59
57
|
const dryRunToggle = {
|
|
60
|
-
type: "toggle",
|
|
61
|
-
name: "dryRun",
|
|
62
58
|
message: "Run in dry-run mode? (no API calls will be made)",
|
|
63
|
-
active: "yes",
|
|
64
|
-
inactive: "no",
|
|
65
59
|
initial: false
|
|
66
60
|
};
|
|
67
61
|
const actionSelector = {
|
|
68
|
-
type: "multiselect",
|
|
69
|
-
name: "action",
|
|
70
62
|
message: "Please select an action",
|
|
71
63
|
choices: [
|
|
72
64
|
{ title: "create a label", value: 0 },
|
|
@@ -81,8 +73,6 @@ const actionSelector = {
|
|
|
81
73
|
]
|
|
82
74
|
};
|
|
83
75
|
const holdToken = {
|
|
84
|
-
type: "confirm",
|
|
85
|
-
name: "value",
|
|
86
76
|
message: "Do you have a personal token?",
|
|
87
77
|
initial: true
|
|
88
78
|
};
|
|
@@ -428,7 +418,13 @@ const deleteLabels = async (configs2) => {
|
|
|
428
418
|
log$4(chalk.bgBlueBright(extraGuideText));
|
|
429
419
|
return { deleted, failed };
|
|
430
420
|
};
|
|
431
|
-
|
|
421
|
+
class CryptoUtils {
|
|
422
|
+
static {
|
|
423
|
+
this.ALGORITHM = "aes-256-cbc";
|
|
424
|
+
}
|
|
425
|
+
static {
|
|
426
|
+
this.ENCODING = "hex";
|
|
427
|
+
}
|
|
432
428
|
/**
|
|
433
429
|
* Generate a machine-specific key based on system information
|
|
434
430
|
* This provides basic obfuscation without requiring user passwords
|
|
@@ -507,10 +503,7 @@ const _CryptoUtils = class _CryptoUtils {
|
|
|
507
503
|
const middle = "*".repeat(Math.min(token.length - 8, 20));
|
|
508
504
|
return `${start}${middle}${end}`;
|
|
509
505
|
}
|
|
510
|
-
}
|
|
511
|
-
_CryptoUtils.ALGORITHM = "aes-256-cbc";
|
|
512
|
-
_CryptoUtils.ENCODING = "hex";
|
|
513
|
-
let CryptoUtils = _CryptoUtils;
|
|
506
|
+
}
|
|
514
507
|
class ConfigError extends Error {
|
|
515
508
|
constructor(type, message, originalError) {
|
|
516
509
|
super(message);
|
|
@@ -1016,13 +1009,374 @@ class ConfigManager {
|
|
|
1016
1009
|
].includes(error.type);
|
|
1017
1010
|
}
|
|
1018
1011
|
}
|
|
1012
|
+
const OPEN_TUI_INPUT_TIMEOUT_MS = 3e4;
|
|
1013
|
+
const ESCAPE_SELECTION_VALUE = 99;
|
|
1014
|
+
let opentuiLoadAttempted = false;
|
|
1015
|
+
let opentuiModule = null;
|
|
1016
|
+
const loadOpenTui = async () => {
|
|
1017
|
+
if (opentuiLoadAttempted) {
|
|
1018
|
+
return opentuiModule;
|
|
1019
|
+
}
|
|
1020
|
+
opentuiLoadAttempted = true;
|
|
1021
|
+
try {
|
|
1022
|
+
const loaded = await import("@opentui/core");
|
|
1023
|
+
opentuiModule = loaded;
|
|
1024
|
+
return loaded;
|
|
1025
|
+
} catch {
|
|
1026
|
+
opentuiModule = null;
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
const question = async (message) => {
|
|
1031
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
1032
|
+
try {
|
|
1033
|
+
const answer = await rl.question(`${message}: `);
|
|
1034
|
+
return answer.trim();
|
|
1035
|
+
} finally {
|
|
1036
|
+
rl.close();
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
const askText = async (message, options = {}) => {
|
|
1040
|
+
const core = await loadOpenTui();
|
|
1041
|
+
if (core) {
|
|
1042
|
+
const result = await askTextWithOpenTui(core, message, options);
|
|
1043
|
+
if (result !== null) {
|
|
1044
|
+
return result;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const answer = await question(message);
|
|
1048
|
+
if (answer.length === 0 && options.initial !== void 0) {
|
|
1049
|
+
return options.initial;
|
|
1050
|
+
}
|
|
1051
|
+
return answer;
|
|
1052
|
+
};
|
|
1053
|
+
const askPassword = async (message) => {
|
|
1054
|
+
const masked = await askPasswordWithRawInput(message);
|
|
1055
|
+
if (masked !== null) {
|
|
1056
|
+
return masked;
|
|
1057
|
+
}
|
|
1058
|
+
return askText(message);
|
|
1059
|
+
};
|
|
1060
|
+
const askConfirm = async (message, initial = true) => {
|
|
1061
|
+
const value = await askSelect(message, [
|
|
1062
|
+
{ title: initial ? "yes (default)" : "yes", value: 1 },
|
|
1063
|
+
{ title: initial ? "no" : "no (default)", value: 0 }
|
|
1064
|
+
]);
|
|
1065
|
+
if (value === 1) {
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
if (value === 0) {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
if (value === ESCAPE_SELECTION_VALUE) {
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
return initial;
|
|
1075
|
+
};
|
|
1076
|
+
const askSelect = async (message, choices) => {
|
|
1077
|
+
const core = await loadOpenTui();
|
|
1078
|
+
if (core) {
|
|
1079
|
+
const selected = await askSelectWithOpenTui(core, message, choices);
|
|
1080
|
+
if (selected !== null) {
|
|
1081
|
+
return selected;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
stdout.write(`${message}
|
|
1085
|
+
`);
|
|
1086
|
+
choices.forEach((choice, index) => {
|
|
1087
|
+
stdout.write(` ${index + 1}. ${choice.title}
|
|
1088
|
+
`);
|
|
1089
|
+
});
|
|
1090
|
+
const rawSelected = await askSelectWithRawInput(choices);
|
|
1091
|
+
if (rawSelected !== null) {
|
|
1092
|
+
return rawSelected;
|
|
1093
|
+
}
|
|
1094
|
+
while (true) {
|
|
1095
|
+
const answer = await question("Select number");
|
|
1096
|
+
const normalized = answer.toLowerCase();
|
|
1097
|
+
if (normalized === "esc" || normalized === "escape") {
|
|
1098
|
+
return choices.find((choice2) => choice2.title.toLowerCase() === "exit")?.value ?? 99;
|
|
1099
|
+
}
|
|
1100
|
+
const parsed = Number.parseInt(answer, 10);
|
|
1101
|
+
if (Number.isNaN(parsed)) {
|
|
1102
|
+
stdout.write("Please enter a valid number.\n");
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
const choice = choices[parsed - 1];
|
|
1106
|
+
if (!choice) {
|
|
1107
|
+
stdout.write("Out of range. Try again.\n");
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
return choice.value;
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
const askSelectWithOpenTui = async (core, message, choices) => {
|
|
1114
|
+
const renderer = await core.createCliRenderer({ exitOnCtrlC: true });
|
|
1115
|
+
let destroyed = false;
|
|
1116
|
+
const destroyRenderer = async () => {
|
|
1117
|
+
if (destroyed) {
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
destroyed = true;
|
|
1121
|
+
await renderer.destroy();
|
|
1122
|
+
};
|
|
1123
|
+
try {
|
|
1124
|
+
const root = new core.BoxRenderable(renderer, {
|
|
1125
|
+
id: "prompt-root",
|
|
1126
|
+
flexDirection: "column",
|
|
1127
|
+
border: true,
|
|
1128
|
+
padding: 1
|
|
1129
|
+
});
|
|
1130
|
+
const title = new core.TextRenderable(renderer, {
|
|
1131
|
+
id: "prompt-title",
|
|
1132
|
+
content: message
|
|
1133
|
+
});
|
|
1134
|
+
const select = new core.SelectRenderable(renderer, {
|
|
1135
|
+
id: "prompt-select",
|
|
1136
|
+
options: choices.map((choice) => ({
|
|
1137
|
+
name: choice.title,
|
|
1138
|
+
value: choice.value
|
|
1139
|
+
}))
|
|
1140
|
+
});
|
|
1141
|
+
root.add(title);
|
|
1142
|
+
root.add(select);
|
|
1143
|
+
renderer.root.add(root);
|
|
1144
|
+
select.focus?.();
|
|
1145
|
+
if (!select.on) {
|
|
1146
|
+
await destroyRenderer();
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
const escapeValue = choices.find((choice) => choice.title.toLowerCase() === "exit")?.value ?? 99;
|
|
1150
|
+
return await new Promise((resolve) => {
|
|
1151
|
+
let resolved = false;
|
|
1152
|
+
const resolveOnce = async (value) => {
|
|
1153
|
+
if (resolved) {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
resolved = true;
|
|
1157
|
+
await destroyRenderer();
|
|
1158
|
+
resolve(value);
|
|
1159
|
+
};
|
|
1160
|
+
select.on(
|
|
1161
|
+
core.SelectRenderableEvents.ITEM_SELECTED,
|
|
1162
|
+
async (_index, option) => {
|
|
1163
|
+
if (typeof option === "object" && option !== null && "value" in option && typeof option.value === "number") {
|
|
1164
|
+
await resolveOnce(option.value);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
await resolveOnce(99);
|
|
1168
|
+
}
|
|
1169
|
+
);
|
|
1170
|
+
renderer.keyInput?.on?.("keypress", async (key) => {
|
|
1171
|
+
if (typeof key === "object" && key !== null && "name" in key && (key.name === "escape" || key.name === "esc")) {
|
|
1172
|
+
await resolveOnce(escapeValue);
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
} catch {
|
|
1177
|
+
await destroyRenderer();
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
const askSelectWithRawInput = async (choices) => {
|
|
1182
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
const escapeValue = choices.find((choice) => choice.title.toLowerCase() === "exit")?.value ?? 99;
|
|
1186
|
+
stdout.write("Select number (or Esc to exit): ");
|
|
1187
|
+
return await new Promise((resolve) => {
|
|
1188
|
+
let digits = "";
|
|
1189
|
+
const wasRaw = stdin.isRaw;
|
|
1190
|
+
const wasPaused = stdin.isPaused();
|
|
1191
|
+
const cleanup = () => {
|
|
1192
|
+
stdin.removeListener("keypress", onKeypress);
|
|
1193
|
+
if (!wasRaw) {
|
|
1194
|
+
stdin.setRawMode(false);
|
|
1195
|
+
}
|
|
1196
|
+
if (wasPaused) {
|
|
1197
|
+
stdin.pause();
|
|
1198
|
+
}
|
|
1199
|
+
stdout.write("\n");
|
|
1200
|
+
};
|
|
1201
|
+
const resolveValue = (value) => {
|
|
1202
|
+
cleanup();
|
|
1203
|
+
resolve(value);
|
|
1204
|
+
};
|
|
1205
|
+
const onKeypress = (str, key) => {
|
|
1206
|
+
if (!key) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (key.name === "escape" || key.name === "esc") {
|
|
1210
|
+
resolveValue(escapeValue);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1214
|
+
const parsed = Number.parseInt(digits, 10);
|
|
1215
|
+
if (Number.isNaN(parsed)) {
|
|
1216
|
+
stdout.write("\nPlease enter a valid number.\nSelect number: ");
|
|
1217
|
+
digits = "";
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const choice = choices[parsed - 1];
|
|
1221
|
+
if (!choice) {
|
|
1222
|
+
stdout.write("\nOut of range. Try again.\nSelect number: ");
|
|
1223
|
+
digits = "";
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
resolveValue(choice.value);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (key.name === "backspace") {
|
|
1230
|
+
if (digits.length > 0) {
|
|
1231
|
+
digits = digits.slice(0, -1);
|
|
1232
|
+
stdout.write("\b \b");
|
|
1233
|
+
}
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (/^[0-9]$/.test(str)) {
|
|
1237
|
+
digits += str;
|
|
1238
|
+
stdout.write(str);
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
emitKeypressEvents(stdin);
|
|
1242
|
+
stdin.setRawMode(true);
|
|
1243
|
+
stdin.resume();
|
|
1244
|
+
stdin.on("keypress", onKeypress);
|
|
1245
|
+
});
|
|
1246
|
+
};
|
|
1247
|
+
const askPasswordWithRawInput = async (message) => {
|
|
1248
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
stdout.write(`${message}: `);
|
|
1252
|
+
return await new Promise((resolve) => {
|
|
1253
|
+
let value = "";
|
|
1254
|
+
const wasRaw = stdin.isRaw;
|
|
1255
|
+
const wasPaused = stdin.isPaused();
|
|
1256
|
+
const cleanup = () => {
|
|
1257
|
+
stdin.removeListener("keypress", onKeypress);
|
|
1258
|
+
if (!wasRaw) {
|
|
1259
|
+
stdin.setRawMode(false);
|
|
1260
|
+
}
|
|
1261
|
+
if (wasPaused) {
|
|
1262
|
+
stdin.pause();
|
|
1263
|
+
}
|
|
1264
|
+
stdout.write("\n");
|
|
1265
|
+
};
|
|
1266
|
+
const resolveValue = (password) => {
|
|
1267
|
+
cleanup();
|
|
1268
|
+
resolve(password);
|
|
1269
|
+
};
|
|
1270
|
+
const onKeypress = (str, key) => {
|
|
1271
|
+
if (!key) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1275
|
+
resolveValue(value);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (key.name === "backspace") {
|
|
1279
|
+
if (value.length > 0) {
|
|
1280
|
+
value = value.slice(0, -1);
|
|
1281
|
+
stdout.write("\b \b");
|
|
1282
|
+
}
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (key.name === "escape" || key.name === "esc") {
|
|
1286
|
+
resolveValue(null);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (str.length === 1 && str >= " ") {
|
|
1290
|
+
value += str;
|
|
1291
|
+
stdout.write("*");
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
emitKeypressEvents(stdin);
|
|
1295
|
+
stdin.setRawMode(true);
|
|
1296
|
+
stdin.resume();
|
|
1297
|
+
stdin.on("keypress", onKeypress);
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
const askTextWithOpenTui = async (core, message, options) => {
|
|
1301
|
+
const renderer = await core.createCliRenderer({ exitOnCtrlC: true });
|
|
1302
|
+
let destroyed = false;
|
|
1303
|
+
let currentValue = options.initial ?? "";
|
|
1304
|
+
const destroyRenderer = async () => {
|
|
1305
|
+
if (destroyed) {
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
destroyed = true;
|
|
1309
|
+
await renderer.destroy();
|
|
1310
|
+
};
|
|
1311
|
+
try {
|
|
1312
|
+
const root = new core.BoxRenderable(renderer, {
|
|
1313
|
+
id: "prompt-root",
|
|
1314
|
+
flexDirection: "column",
|
|
1315
|
+
border: true,
|
|
1316
|
+
padding: 1
|
|
1317
|
+
});
|
|
1318
|
+
const title = new core.TextRenderable(renderer, {
|
|
1319
|
+
id: "prompt-title",
|
|
1320
|
+
content: message
|
|
1321
|
+
});
|
|
1322
|
+
const inputField = new core.InputRenderable(renderer, {
|
|
1323
|
+
id: "prompt-input",
|
|
1324
|
+
placeholder: options.initial ?? ""
|
|
1325
|
+
});
|
|
1326
|
+
inputField.setValue?.(currentValue);
|
|
1327
|
+
if (!inputField.on) {
|
|
1328
|
+
await destroyRenderer();
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
inputField.on(core.InputRenderableEvents.CHANGE, (nextValue) => {
|
|
1332
|
+
if (typeof nextValue === "string") {
|
|
1333
|
+
currentValue = nextValue;
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
root.add(title);
|
|
1337
|
+
root.add(inputField);
|
|
1338
|
+
renderer.root.add(root);
|
|
1339
|
+
inputField.focus?.();
|
|
1340
|
+
if (!renderer.keyInput?.on) {
|
|
1341
|
+
await destroyRenderer();
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
return await new Promise((resolve) => {
|
|
1345
|
+
let settled = false;
|
|
1346
|
+
const timer = setTimeout(async () => {
|
|
1347
|
+
if (settled) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
settled = true;
|
|
1351
|
+
await destroyRenderer();
|
|
1352
|
+
resolve(null);
|
|
1353
|
+
}, OPEN_TUI_INPUT_TIMEOUT_MS);
|
|
1354
|
+
renderer.keyInput?.on?.("keypress", async (key) => {
|
|
1355
|
+
if (settled) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (typeof key === "object" && key !== null && "name" in key && (key.name === "enter" || key.name === "return")) {
|
|
1359
|
+
settled = true;
|
|
1360
|
+
clearTimeout(timer);
|
|
1361
|
+
await destroyRenderer();
|
|
1362
|
+
if (currentValue === "" && options.initial !== void 0) {
|
|
1363
|
+
resolve(options.initial);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
resolve(currentValue);
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
});
|
|
1370
|
+
} catch {
|
|
1371
|
+
await destroyRenderer();
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1019
1375
|
const getConfirmation = async () => {
|
|
1020
|
-
|
|
1021
|
-
return response.value;
|
|
1376
|
+
return askConfirm(holdToken.message, holdToken.initial);
|
|
1022
1377
|
};
|
|
1023
1378
|
const getDryRunChoice = async () => {
|
|
1024
|
-
|
|
1025
|
-
return Boolean(response.dryRun);
|
|
1379
|
+
return askConfirm(dryRunToggle.message, dryRunToggle.initial);
|
|
1026
1380
|
};
|
|
1027
1381
|
const log$3 = console.log;
|
|
1028
1382
|
const generateSampleJson = async () => {
|
|
@@ -1344,11 +1698,19 @@ const importLabelsFromFile = async (configs2, filePath, dryRun = false) => {
|
|
|
1344
1698
|
return summary;
|
|
1345
1699
|
};
|
|
1346
1700
|
const getTargetLabel = async () => {
|
|
1347
|
-
|
|
1348
|
-
|
|
1701
|
+
while (true) {
|
|
1702
|
+
const name = (await askText(deleteLabel$1.message)).trim();
|
|
1703
|
+
if (name.length > 0) {
|
|
1704
|
+
return [name];
|
|
1705
|
+
}
|
|
1706
|
+
console.log(chalk.yellow("Label name cannot be empty. Please try again."));
|
|
1707
|
+
}
|
|
1349
1708
|
};
|
|
1350
1709
|
const GIT_COMMAND_TIMEOUT_MS = 5e3;
|
|
1351
|
-
|
|
1710
|
+
class GitRepositoryDetector {
|
|
1711
|
+
static {
|
|
1712
|
+
this.execAsyncInternal = promisify(exec);
|
|
1713
|
+
}
|
|
1352
1714
|
/**
|
|
1353
1715
|
* Overrides the internal execAsync function for testing purposes.
|
|
1354
1716
|
* @param mock - The mock function to use for execAsync.
|
|
@@ -1449,14 +1811,14 @@ const _GitRepositoryDetector = class _GitRepositoryDetector {
|
|
|
1449
1811
|
*/
|
|
1450
1812
|
static async getRemoteUrl(gitRoot, remoteName) {
|
|
1451
1813
|
try {
|
|
1452
|
-
const { stdout } = await this.execAsyncInternal(
|
|
1814
|
+
const { stdout: stdout2 } = await this.execAsyncInternal(
|
|
1453
1815
|
`git remote get-url ${remoteName}`,
|
|
1454
1816
|
{
|
|
1455
1817
|
cwd: gitRoot,
|
|
1456
1818
|
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
1457
1819
|
}
|
|
1458
1820
|
);
|
|
1459
|
-
return
|
|
1821
|
+
return stdout2.trim() || null;
|
|
1460
1822
|
} catch {
|
|
1461
1823
|
return null;
|
|
1462
1824
|
}
|
|
@@ -1526,12 +1888,12 @@ const _GitRepositoryDetector = class _GitRepositoryDetector {
|
|
|
1526
1888
|
*/
|
|
1527
1889
|
static async getAllRemotes(gitRoot) {
|
|
1528
1890
|
try {
|
|
1529
|
-
const { stdout } = await this.execAsyncInternal("git remote", {
|
|
1891
|
+
const { stdout: stdout2 } = await this.execAsyncInternal("git remote", {
|
|
1530
1892
|
cwd: gitRoot,
|
|
1531
1893
|
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
1532
1894
|
});
|
|
1533
1895
|
return {
|
|
1534
|
-
remotes:
|
|
1896
|
+
remotes: stdout2.trim().split("\n").filter((remote) => remote.length > 0)
|
|
1535
1897
|
};
|
|
1536
1898
|
} catch (err) {
|
|
1537
1899
|
const error = err;
|
|
@@ -1544,11 +1906,23 @@ const _GitRepositoryDetector = class _GitRepositoryDetector {
|
|
|
1544
1906
|
return { remotes: [] };
|
|
1545
1907
|
}
|
|
1546
1908
|
}
|
|
1909
|
+
}
|
|
1910
|
+
const askRequiredValue = async (ask, fieldName) => {
|
|
1911
|
+
while (true) {
|
|
1912
|
+
const rawValue = await ask();
|
|
1913
|
+
if (rawValue === null) {
|
|
1914
|
+
throw new Error(`${fieldName} input was canceled by user.`);
|
|
1915
|
+
}
|
|
1916
|
+
const value = rawValue.trim();
|
|
1917
|
+
if (value.length > 0) {
|
|
1918
|
+
return value;
|
|
1919
|
+
}
|
|
1920
|
+
console.log(
|
|
1921
|
+
chalk.yellow(`⚠️ ${fieldName} cannot be empty. Please try again.`)
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1547
1924
|
};
|
|
1548
|
-
_GitRepositoryDetector.execAsyncInternal = promisify(exec);
|
|
1549
|
-
let GitRepositoryDetector = _GitRepositoryDetector;
|
|
1550
1925
|
const getGitHubConfigs = async () => {
|
|
1551
|
-
var _a, _b;
|
|
1552
1926
|
const configManager2 = new ConfigManager();
|
|
1553
1927
|
let validationResult = {
|
|
1554
1928
|
config: null,
|
|
@@ -1612,91 +1986,108 @@ const getGitHubConfigs = async () => {
|
|
|
1612
1986
|
console.log(chalk.gray(` Error: ${error.message}`));
|
|
1613
1987
|
}
|
|
1614
1988
|
}
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
}
|
|
1621
|
-
]);
|
|
1989
|
+
const repoPrompt2 = githubConfigs.find((prompt) => prompt.name === "repo");
|
|
1990
|
+
const repo2 = await askRequiredValue(
|
|
1991
|
+
() => askText(repoPrompt2?.message ?? "Please type your target repo name"),
|
|
1992
|
+
"Repository name"
|
|
1993
|
+
);
|
|
1622
1994
|
const octokit2 = new Octokit({
|
|
1623
1995
|
auth: validationResult.config.token
|
|
1624
1996
|
});
|
|
1625
1997
|
return {
|
|
1626
1998
|
octokit: octokit2,
|
|
1627
1999
|
owner: validationResult.config.owner,
|
|
1628
|
-
repo:
|
|
2000
|
+
repo: repo2,
|
|
1629
2001
|
fromSavedConfig: true,
|
|
1630
2002
|
autoDetected: false,
|
|
1631
2003
|
detectionMethod: "manual"
|
|
1632
2004
|
};
|
|
1633
2005
|
}
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
)
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
2006
|
+
const tokenPrompt = githubConfigs.find((prompt) => prompt.name === "octokit");
|
|
2007
|
+
const ownerPrompt = githubConfigs.find((prompt) => prompt.name === "owner");
|
|
2008
|
+
const repoPrompt = githubConfigs.find((prompt) => prompt.name === "repo");
|
|
2009
|
+
const octokitToken = await askRequiredValue(
|
|
2010
|
+
() => askPassword(tokenPrompt?.message ?? "Please type your personal token"),
|
|
2011
|
+
"Personal token"
|
|
2012
|
+
);
|
|
2013
|
+
const owner = await askRequiredValue(
|
|
2014
|
+
() => askText(ownerPrompt?.message ?? "Please type your GitHub account", {
|
|
2015
|
+
initial: validationResult.preservedData?.owner
|
|
2016
|
+
}),
|
|
2017
|
+
"GitHub account"
|
|
2018
|
+
);
|
|
2019
|
+
const repo = await askRequiredValue(
|
|
2020
|
+
() => askText(repoPrompt?.message ?? "Please type your target repo name"),
|
|
2021
|
+
"Repository name"
|
|
2022
|
+
);
|
|
2023
|
+
try {
|
|
2024
|
+
await configManager2.saveConfig({
|
|
2025
|
+
token: octokitToken,
|
|
2026
|
+
owner,
|
|
2027
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2028
|
+
});
|
|
2029
|
+
if (validationResult.preservedData?.owner && validationResult.preservedData.owner !== owner) {
|
|
2030
|
+
console.log("✓ Configuration updated with new credentials");
|
|
2031
|
+
} else {
|
|
2032
|
+
console.log("✓ Configuration saved successfully");
|
|
1645
2033
|
}
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
owner: response.owner,
|
|
1653
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1654
|
-
});
|
|
1655
|
-
if (((_b = validationResult.preservedData) == null ? void 0 : _b.owner) && validationResult.preservedData.owner !== response.owner) {
|
|
1656
|
-
console.log("✓ Configuration updated with new credentials");
|
|
1657
|
-
} else {
|
|
1658
|
-
console.log("✓ Configuration saved successfully");
|
|
1659
|
-
}
|
|
1660
|
-
} catch (error) {
|
|
1661
|
-
if (error instanceof ConfigError) {
|
|
1662
|
-
console.error(`❌ ${ConfigManager.getErrorMessage(error)}`);
|
|
1663
|
-
if (!ConfigManager.isRecoverableError(error)) {
|
|
1664
|
-
console.error(
|
|
1665
|
-
" This may affect future sessions. Please resolve the issue or contact support."
|
|
1666
|
-
);
|
|
1667
|
-
}
|
|
1668
|
-
} else {
|
|
1669
|
-
console.warn(
|
|
1670
|
-
"⚠️ Failed to save configuration:",
|
|
1671
|
-
error instanceof Error ? error.message : "Unknown error"
|
|
2034
|
+
} catch (error) {
|
|
2035
|
+
if (error instanceof ConfigError) {
|
|
2036
|
+
console.error(`❌ ${ConfigManager.getErrorMessage(error)}`);
|
|
2037
|
+
if (!ConfigManager.isRecoverableError(error)) {
|
|
2038
|
+
console.error(
|
|
2039
|
+
" This may affect future sessions. Please resolve the issue or contact support."
|
|
1672
2040
|
);
|
|
1673
2041
|
}
|
|
2042
|
+
} else {
|
|
2043
|
+
console.warn(
|
|
2044
|
+
"⚠️ Failed to save configuration:",
|
|
2045
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
2046
|
+
);
|
|
1674
2047
|
}
|
|
1675
2048
|
}
|
|
1676
2049
|
const octokit = new Octokit({
|
|
1677
|
-
auth:
|
|
2050
|
+
auth: octokitToken
|
|
1678
2051
|
});
|
|
1679
2052
|
return {
|
|
1680
2053
|
octokit,
|
|
1681
|
-
owner
|
|
1682
|
-
repo
|
|
2054
|
+
owner,
|
|
2055
|
+
repo,
|
|
1683
2056
|
fromSavedConfig: false,
|
|
1684
2057
|
autoDetected: false,
|
|
1685
2058
|
detectionMethod: "manual"
|
|
1686
2059
|
};
|
|
1687
2060
|
};
|
|
1688
2061
|
const getLabelFilePath = async () => {
|
|
1689
|
-
|
|
1690
|
-
|
|
2062
|
+
while (true) {
|
|
2063
|
+
const filePath = (await askText(labelFilePath.message)).trim();
|
|
2064
|
+
if (filePath.length > 0) {
|
|
2065
|
+
return filePath;
|
|
2066
|
+
}
|
|
2067
|
+
console.log("File path cannot be empty. Please try again.");
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
const getPromptMessage = (field, fallback) => {
|
|
2071
|
+
return newLabel.find((prompt) => prompt.name === field)?.message ?? fallback;
|
|
1691
2072
|
};
|
|
1692
2073
|
const getNewLabel = async () => {
|
|
1693
|
-
const
|
|
1694
|
-
|
|
2074
|
+
const name = await askText(
|
|
2075
|
+
getPromptMessage("name", "Please type new label name")
|
|
2076
|
+
);
|
|
2077
|
+
const color = await askText(
|
|
2078
|
+
getPromptMessage("color", 'Please type label color without "#" ')
|
|
2079
|
+
);
|
|
2080
|
+
const description = await askText(
|
|
2081
|
+
getPromptMessage("description", "Please type label description")
|
|
2082
|
+
);
|
|
2083
|
+
return {
|
|
2084
|
+
name,
|
|
2085
|
+
color,
|
|
2086
|
+
description
|
|
2087
|
+
};
|
|
1695
2088
|
};
|
|
1696
2089
|
const selectAction = async () => {
|
|
1697
|
-
|
|
1698
|
-
const { action } = response;
|
|
1699
|
-
return action[0] !== void 0 ? action[0] : 99;
|
|
2090
|
+
return askSelect(actionSelector.message, actionSelector.choices);
|
|
1700
2091
|
};
|
|
1701
2092
|
const log = console.log;
|
|
1702
2093
|
let firstStart = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hyouji",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
"author": "koji <baxin1919@gmail.com>",
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
"表示",
|
|
25
25
|
"Hyouji(表示)",
|
|
26
26
|
"JSON",
|
|
27
|
-
"yaml"
|
|
28
|
-
"yml"
|
|
27
|
+
"yaml"
|
|
29
28
|
],
|
|
30
29
|
"scripts": {
|
|
31
30
|
"start": "node dist/index.js",
|
|
@@ -43,34 +42,32 @@
|
|
|
43
42
|
"test:auto-detection": "node tests/integration/auto-detection/integration-flow.cjs",
|
|
44
43
|
"test:verification": "node tests/scripts/verification/run-all.cjs",
|
|
45
44
|
"test:all-custom": "npm run test:error-handling && npm run test:config && npm run test:integration && npm run test:verification",
|
|
46
|
-
"check-cli": "run-s test diff-integration-tests check-integration-tests",
|
|
47
|
-
"check-integration-tests": "run-s check-integration-test:*",
|
|
48
45
|
"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.'",
|
|
49
46
|
"watch:build": "tsc -p tsconfig.json -w",
|
|
50
|
-
"cov:send": "run-s cov:lcov && codecov",
|
|
51
47
|
"version": "standard-version",
|
|
52
|
-
"reset-hard": "git clean -dfx && git reset --hard &&
|
|
53
|
-
"prepare-release": "run-s reset-hard test cov:check doc:html version doc:publish"
|
|
48
|
+
"reset-hard": "git clean -dfx && git reset --hard && bun install"
|
|
54
49
|
},
|
|
55
50
|
"engines": {
|
|
56
|
-
"node": ">=
|
|
51
|
+
"node": ">=22.22.0"
|
|
57
52
|
},
|
|
58
53
|
"dependencies": {
|
|
59
54
|
"@octokit/core": "^7.0.6",
|
|
55
|
+
"@opentui/core": "0.1.80",
|
|
60
56
|
"chalk": "^5.6.2",
|
|
61
57
|
"oh-my-logo": "^0.3.2",
|
|
62
|
-
"prompts": "^2.4.2",
|
|
63
58
|
"yaml": "^2.8.1"
|
|
64
59
|
},
|
|
65
60
|
"devDependencies": {
|
|
66
|
-
"@biomejs/biome": "2.3.
|
|
61
|
+
"@biomejs/biome": "2.3.11",
|
|
67
62
|
"@types/node": "^24.10.0",
|
|
68
|
-
"@vitest/coverage-v8": "^4.0.
|
|
69
|
-
"@vitest/ui": "^4.0.
|
|
63
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
64
|
+
"@vitest/ui": "^4.0.17",
|
|
65
|
+
"knip": "^5.85.0",
|
|
66
|
+
"lefthook": "^2.1.1",
|
|
70
67
|
"standard-version": "^9.5.0",
|
|
71
68
|
"typescript": "^5.9.3",
|
|
72
|
-
"vite": "^7.
|
|
73
|
-
"vitest": "^4.0.
|
|
69
|
+
"vite": "^7.3.1",
|
|
70
|
+
"vitest": "^4.0.17"
|
|
74
71
|
},
|
|
75
72
|
"files": [
|
|
76
73
|
"dist/**/*",
|