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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +478 -87
  3. 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 prompts from "prompts";
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
- const _CryptoUtils = class _CryptoUtils {
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
- const response = await prompts(holdToken);
1021
- return response.value;
1376
+ return askConfirm(holdToken.message, holdToken.initial);
1022
1377
  };
1023
1378
  const getDryRunChoice = async () => {
1024
- const response = await prompts(dryRunToggle);
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
- const response = await prompts(deleteLabel$1);
1348
- return [response.name];
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
- const _GitRepositoryDetector = class _GitRepositoryDetector {
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 stdout.trim() || null;
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: stdout.trim().split("\n").filter((remote) => remote.length > 0)
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 repoResponse = await prompts([
1616
- {
1617
- type: "text",
1618
- name: "repo",
1619
- message: "Please type your target repo name"
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: repoResponse.repo,
2000
+ repo: repo2,
1629
2001
  fromSavedConfig: true,
1630
2002
  autoDetected: false,
1631
2003
  detectionMethod: "manual"
1632
2004
  };
1633
2005
  }
1634
- const promptConfig = [...githubConfigs];
1635
- if ((_a = validationResult.preservedData) == null ? void 0 : _a.owner) {
1636
- const ownerPromptIndex = promptConfig.findIndex(
1637
- (prompt) => prompt.name === "owner"
1638
- );
1639
- if (ownerPromptIndex !== -1) {
1640
- promptConfig[ownerPromptIndex] = {
1641
- ...promptConfig[ownerPromptIndex],
1642
- initial: validationResult.preservedData.owner
1643
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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
- const response = await prompts(promptConfig);
1648
- if (response.octokit && response.owner) {
1649
- try {
1650
- await configManager2.saveConfig({
1651
- token: response.octokit,
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: response.octokit
2050
+ auth: octokitToken
1678
2051
  });
1679
2052
  return {
1680
2053
  octokit,
1681
- owner: response.owner,
1682
- repo: response.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
- const response = await prompts(labelFilePath);
1690
- return response.filePath;
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 response = await prompts(newLabel);
1694
- return response;
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
- const response = await prompts(actionSelector);
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.16",
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 && yarn",
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": ">=10"
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.4",
61
+ "@biomejs/biome": "2.3.11",
67
62
  "@types/node": "^24.10.0",
68
- "@vitest/coverage-v8": "^4.0.8",
69
- "@vitest/ui": "^4.0.8",
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.2.2",
73
- "vitest": "^4.0.8"
69
+ "vite": "^7.3.1",
70
+ "vitest": "^4.0.17"
74
71
  },
75
72
  "files": [
76
73
  "dist/**/*",