hyouji 0.0.14 → 0.0.16

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 +25 -15
  2. package/dist/index.js +211 -39
  3. package/package.json +1 -2
package/README.md CHANGED
@@ -94,6 +94,21 @@ These credentials will be securely saved and reused for future sessions.
94
94
  8. **Display your settings** - View your saved configuration
95
95
  9. **Exit**
96
96
 
97
+ When you choose a create/delete/import action, you’ll be asked if you want to run in **dry-run mode**. Selecting “yes” shows what would happen without any API calls. Example:
98
+
99
+ ```
100
+ === Create preset labels summary ===
101
+ Mode: dry run (no API calls executed)
102
+ Created: 0 Failed: 0 Deleted: 0 Skipped: 24
103
+ ```
104
+
105
+ Quick dry-run example (generate sample JSON then import it without touching GitHub):
106
+
107
+ ```bash
108
+ hyouji # choose “Generate sample JSON”
109
+ hyouji # choose “Import labels from JSON or YAML” -> pick hyouji.json -> select dry-run = yes
110
+ ```
111
+
97
112
  ### Settings Management
98
113
 
99
114
  The tool now includes persistent configuration storage with enhanced security:
@@ -105,6 +120,12 @@ The tool now includes persistent configuration storage with enhanced security:
105
120
  - **Automatic migration**: Existing plain text configurations are automatically upgraded to encrypted format
106
121
  - **Token security**: Your personal token is never displayed in plain text, only an obfuscated preview is shown
107
122
 
123
+ ### Dry-run Mode & Progress
124
+
125
+ - Select **yes** when prompted for dry-run to avoid any API calls; actions are listed as “Would create/delete…”.
126
+ - Each API call shows short status lines (e.g., “Creating label…”, “Deleted …”).
127
+ - A final summary reports created/deleted/skipped/failed counts and hints for next steps.
128
+
108
129
  ### Security Features
109
130
 
110
131
  **Token Encryption**:
@@ -132,22 +153,11 @@ If you want to create/delete a single label, you need to type the followings.
132
153
 
133
154
  - label name
134
155
 
135
- In terms of multiple labels, this script is using `label.js` to define name, color and description. The format is very simple.
136
- If you want to put your own labels, you will need to modify `label.js` file.
156
+ For multiple labels, use the import flow:
137
157
 
138
- ```js
139
- module.exports = Object.freeze([
140
- {
141
- name: "Type: Bug Fix",
142
- color: "FF8A65",
143
- description: "Fix features that are not working",
144
- },
145
- {
146
- name: "Type: Enhancement",
147
- color: "64B5F7",
148
- description: "Add new features",
149
- },
150
- ```
158
+ - Prepare a JSON or YAML file (examples live in `examples/labels.{json,yaml}`).
159
+ - Or generate fresh templates from the menu: **Generate sample JSON/YAML** will create `hyouji.json` or `hyouji.yaml` in your CWD.
160
+ - Run **Import labels** and provide the file path. You can choose **dry-run** first to see what would be created without hitting the API.
151
161
 
152
162
  ## Quick Start
153
163
 
package/dist/index.js CHANGED
@@ -56,6 +56,14 @@ const labelFilePath = {
56
56
  name: "filePath",
57
57
  message: "Please type the path to your JSON or YAML file"
58
58
  };
59
+ const dryRunToggle = {
60
+ type: "toggle",
61
+ name: "dryRun",
62
+ message: "Run in dry-run mode? (no API calls will be made)",
63
+ active: "yes",
64
+ inactive: "no",
65
+ initial: false
66
+ };
59
67
  const actionSelector = {
60
68
  type: "multiselect",
61
69
  name: "action",
@@ -270,42 +278,67 @@ const extraGuideText = `If you don't see action selector, please hit space key.`
270
278
  const linkToPersonalToken = "https://github.com/settings/tokens";
271
279
  const log$4 = console.log;
272
280
  const createLabel = async (configs2, label) => {
273
- const resp = await configs2.octokit.request(
274
- "POST /repos/{owner}/{repo}/labels",
275
- {
276
- owner: configs2.owner,
277
- repo: configs2.repo,
278
- name: label.name,
279
- color: label.color,
280
- description: label.description
281
+ try {
282
+ log$4(chalk.cyan(`⏳ Creating label "${label.name}"...`));
283
+ const resp = await configs2.octokit.request(
284
+ "POST /repos/{owner}/{repo}/labels",
285
+ {
286
+ owner: configs2.owner,
287
+ repo: configs2.repo,
288
+ name: label.name,
289
+ color: label.color,
290
+ description: label.description
291
+ }
292
+ );
293
+ const status = resp.status;
294
+ switch (status) {
295
+ case 201:
296
+ log$4(chalk.green(`✓ ${resp.status}: Created ${label.name}`));
297
+ return true;
298
+ case 404:
299
+ log$4(chalk.red(`${resp.status}: Resource not found`));
300
+ return false;
301
+ case 422:
302
+ log$4(chalk.red(`${resp.status}: Validation failed`));
303
+ return false;
304
+ default:
305
+ log$4(chalk.yellow(`${resp.status}: Something wrong`));
306
+ return false;
281
307
  }
282
- );
283
- const status = resp.status;
284
- switch (status) {
285
- case 201:
286
- log$4(chalk.green(`${resp.status}: Created ${label.name}`));
287
- break;
288
- case 404:
289
- log$4(chalk.red(`${resp.status}: Resource not found`));
290
- break;
291
- case 422:
292
- log$4(chalk.red(`${resp.status}: Validation failed`));
293
- break;
294
- default:
295
- log$4(chalk.yellow(`${resp.status}: Something wrong`));
296
- break;
308
+ } catch (error) {
309
+ log$4(
310
+ chalk.red(
311
+ `Error creating label "${label.name}": ${error instanceof Error ? error.message : "Unknown error"}`
312
+ )
313
+ );
314
+ return false;
297
315
  }
298
316
  };
299
317
  const createLabels = async (configs2) => {
300
- labels.forEach(async (label) => {
301
- createLabel(configs2, label);
302
- });
303
- log$4("Created all labels");
318
+ let created = 0;
319
+ let failed = 0;
320
+ for (const label of labels) {
321
+ const ok = await createLabel(configs2, label);
322
+ if (ok) {
323
+ created++;
324
+ } else {
325
+ failed++;
326
+ }
327
+ }
328
+ if (failed === 0) {
329
+ log$4(chalk.green("✓ Created all labels successfully"));
330
+ } else {
331
+ log$4(chalk.yellow(`Finished processing labels: ${created} created, ${failed} failed`));
332
+ }
304
333
  log$4(chalk.bgBlueBright(extraGuideText));
334
+ return { created, failed };
305
335
  };
306
336
  const deleteLabel = async (configs2, labelNames) => {
337
+ let deleted = 0;
338
+ let failed = 0;
307
339
  for (const labelName of labelNames) {
308
340
  try {
341
+ log$4(chalk.cyan(`⏳ Deleting label "${labelName}"...`));
309
342
  const resp = await configs2.octokit.request(
310
343
  "DELETE /repos/{owner}/{repo}/labels/{name}",
311
344
  {
@@ -315,11 +348,14 @@ const deleteLabel = async (configs2, labelNames) => {
315
348
  }
316
349
  );
317
350
  if (resp.status === 204) {
351
+ deleted++;
318
352
  log$4(chalk.green(`${resp.status}: Deleted ${labelName}`));
319
353
  } else {
354
+ failed++;
320
355
  log$4(chalk.yellow(`${resp.status}: Something wrong with ${labelName}`));
321
356
  }
322
357
  } catch (error) {
358
+ failed++;
323
359
  if (error && typeof error === "object" && "status" in error && error.status === 404) {
324
360
  log$4(chalk.red(`404: Label "${labelName}" not found`));
325
361
  } else {
@@ -331,6 +367,7 @@ const deleteLabel = async (configs2, labelNames) => {
331
367
  }
332
368
  }
333
369
  }
370
+ return { deleted, failed };
334
371
  };
335
372
  const getLabels = async (configs2) => {
336
373
  const resp = await configs2.octokit.request(
@@ -352,9 +389,11 @@ const deleteLabels = async (configs2) => {
352
389
  const names = await getLabels(configs2);
353
390
  if (names.length === 0) {
354
391
  log$4(chalk.yellow("No labels found to delete"));
355
- return;
392
+ return { deleted: 0, failed: 0 };
356
393
  }
357
394
  log$4(chalk.blue(`Deleting ${names.length} labels...`));
395
+ let deleted = 0;
396
+ let failed = 0;
358
397
  for (const name of names) {
359
398
  try {
360
399
  const resp = await configs2.octokit.request(
@@ -366,11 +405,14 @@ const deleteLabels = async (configs2) => {
366
405
  }
367
406
  );
368
407
  if (resp.status === 204) {
408
+ deleted++;
369
409
  log$4(chalk.green(`${resp.status}: Deleted ${name}`));
370
410
  } else {
411
+ failed++;
371
412
  log$4(chalk.yellow(`${resp.status}: Something wrong with ${name}`));
372
413
  }
373
414
  } catch (error) {
415
+ failed++;
374
416
  if (error && typeof error === "object" && "status" in error && error.status === 404) {
375
417
  log$4(chalk.red(`404: Label "${name}" not found`));
376
418
  } else {
@@ -384,6 +426,7 @@ const deleteLabels = async (configs2) => {
384
426
  }
385
427
  log$4(chalk.blue("Finished deleting labels"));
386
428
  log$4(chalk.bgBlueBright(extraGuideText));
429
+ return { deleted, failed };
387
430
  };
388
431
  const _CryptoUtils = class _CryptoUtils {
389
432
  /**
@@ -977,6 +1020,10 @@ const getConfirmation = async () => {
977
1020
  const response = await prompts(holdToken);
978
1021
  return response.value;
979
1022
  };
1023
+ const getDryRunChoice = async () => {
1024
+ const response = await prompts(dryRunToggle);
1025
+ return Boolean(response.dryRun);
1026
+ };
980
1027
  const log$3 = console.log;
981
1028
  const generateSampleJson = async () => {
982
1029
  try {
@@ -1103,11 +1150,18 @@ const formatSupportedExtensions = () => {
1103
1150
  return getSupportedExtensions().join(", ");
1104
1151
  };
1105
1152
  const log$1 = console.log;
1106
- const importLabelsFromFile = async (configs2, filePath) => {
1153
+ const importLabelsFromFile = async (configs2, filePath, dryRun = false) => {
1154
+ const summary = {
1155
+ attempted: 0,
1156
+ succeeded: 0,
1157
+ failed: 0,
1158
+ skipped: 0
1159
+ };
1107
1160
  try {
1108
1161
  if (!fs.existsSync(filePath)) {
1109
1162
  log$1(chalk.red(`Error: File not found at path: ${filePath}`));
1110
- return;
1163
+ summary.failed += 1;
1164
+ return summary;
1111
1165
  }
1112
1166
  const format = detectFileFormat(filePath);
1113
1167
  if (!format) {
@@ -1116,7 +1170,8 @@ const importLabelsFromFile = async (configs2, filePath) => {
1116
1170
  `Error: Unsupported file format. Supported formats: ${formatSupportedExtensions()}`
1117
1171
  )
1118
1172
  );
1119
- return;
1173
+ summary.failed += 1;
1174
+ return summary;
1120
1175
  }
1121
1176
  const fileContent = fs.readFileSync(filePath, "utf8");
1122
1177
  let parsedData;
@@ -1134,11 +1189,13 @@ const importLabelsFromFile = async (configs2, filePath) => {
1134
1189
  `Parse error: ${parseError instanceof Error ? parseError.message : "Unknown error"}`
1135
1190
  )
1136
1191
  );
1137
- return;
1192
+ summary.failed += 1;
1193
+ return summary;
1138
1194
  }
1139
1195
  if (!Array.isArray(parsedData)) {
1140
1196
  log$1(chalk.red("Error: File must contain an array of label objects"));
1141
- return;
1197
+ summary.failed += 1;
1198
+ return summary;
1142
1199
  }
1143
1200
  const validLabels = [];
1144
1201
  for (let i = 0; i < parsedData.length; i++) {
@@ -1224,7 +1281,21 @@ const importLabelsFromFile = async (configs2, filePath) => {
1224
1281
  }
1225
1282
  if (validLabels.length === 0) {
1226
1283
  log$1(chalk.red("Error: No valid labels found in file"));
1227
- return;
1284
+ summary.failed += 1;
1285
+ return summary;
1286
+ }
1287
+ summary.attempted = validLabels.length;
1288
+ if (dryRun) {
1289
+ validLabels.forEach((label) => {
1290
+ summary.skipped += 1;
1291
+ log$1(chalk.yellow(`[dry-run] Would create label "${label.name}"`));
1292
+ });
1293
+ log$1(
1294
+ chalk.blue(
1295
+ `Dry run summary: Would create ${validLabels.length} labels.`
1296
+ )
1297
+ );
1298
+ return summary;
1228
1299
  }
1229
1300
  log$1(chalk.blue(`Starting import of ${validLabels.length} labels...`));
1230
1301
  log$1("");
@@ -1253,11 +1324,14 @@ const importLabelsFromFile = async (configs2, filePath) => {
1253
1324
  `✅ Import completed successfully! Created ${successCount} labels.`
1254
1325
  )
1255
1326
  );
1327
+ summary.succeeded = successCount;
1256
1328
  } else {
1257
1329
  log$1(chalk.yellow(`⚠️ Import completed with some errors:`));
1258
1330
  log$1(chalk.green(` • Successfully created: ${successCount} labels`));
1259
1331
  log$1(chalk.red(` • Failed to create: ${errorCount} labels`));
1260
1332
  log$1(chalk.blue(` • Total processed: ${validLabels.length} labels`));
1333
+ summary.succeeded = successCount;
1334
+ summary.failed += errorCount;
1261
1335
  }
1262
1336
  } catch (error) {
1263
1337
  log$1(
@@ -1265,7 +1339,9 @@ const importLabelsFromFile = async (configs2, filePath) => {
1265
1339
  `Error reading file: ${error instanceof Error ? error.message : "Unknown error"}`
1266
1340
  )
1267
1341
  );
1342
+ summary.failed += 1;
1268
1343
  }
1344
+ return summary;
1269
1345
  };
1270
1346
  const getTargetLabel = async () => {
1271
1347
  const response = await prompts(deleteLabel$1);
@@ -1753,6 +1829,32 @@ const initializeConfigs = async () => {
1753
1829
  return null;
1754
1830
  }
1755
1831
  };
1832
+ const makeSummary = () => ({
1833
+ created: 0,
1834
+ deleted: 0,
1835
+ skipped: 0,
1836
+ failed: 0,
1837
+ notes: []
1838
+ });
1839
+ const printSummary = (action, summary, dryRun) => {
1840
+ log(chalk.cyan(`
1841
+ === ${action} summary ===`));
1842
+ if (dryRun) {
1843
+ log(chalk.yellow("Mode: dry run (no API calls executed)"));
1844
+ }
1845
+ log(
1846
+ chalk.green(`Created: ${summary.created}`) + chalk.red(` Failed: ${summary.failed}`) + chalk.blue(` Deleted: ${summary.deleted}`) + chalk.yellow(` Skipped: ${summary.skipped}`)
1847
+ );
1848
+ summary.notes.forEach((note) => log(chalk.gray(`- ${note}`)));
1849
+ if (summary.failed > 0 && !dryRun) {
1850
+ log(
1851
+ chalk.yellow(
1852
+ "Some operations failed. Re-run the command or check your credentials/permissions."
1853
+ )
1854
+ );
1855
+ }
1856
+ log(chalk.cyan("========================\n"));
1857
+ };
1756
1858
  const main = async () => {
1757
1859
  if (firstStart) {
1758
1860
  configs = await initializeConfigs();
@@ -1764,36 +1866,104 @@ const main = async () => {
1764
1866
  while (selectedIndex == 99) {
1765
1867
  selectedIndex = await selectAction();
1766
1868
  }
1869
+ if (selectedIndex === 8) {
1870
+ console.log("exit");
1871
+ process.exit(0);
1872
+ return;
1873
+ }
1874
+ const dryRun = selectedIndex >= 0 && selectedIndex <= 4 ? await getDryRunChoice() : false;
1767
1875
  switch (selectedIndex) {
1768
1876
  case 0: {
1877
+ const summary = makeSummary();
1769
1878
  const newLabel2 = await getNewLabel();
1770
- await createLabel(configs, newLabel2);
1879
+ if (dryRun) {
1880
+ log(
1881
+ chalk.yellow(
1882
+ `[dry-run] Would create label "${newLabel2.name}" with color "${newLabel2.color ?? "N/A"}"`
1883
+ )
1884
+ );
1885
+ summary.skipped += 1;
1886
+ } else {
1887
+ const ok = await createLabel(configs, newLabel2);
1888
+ if (ok) {
1889
+ summary.created += 1;
1890
+ } else {
1891
+ summary.failed += 1;
1892
+ }
1893
+ }
1894
+ printSummary("Create a label", summary, dryRun);
1771
1895
  firstStart = firstStart && false;
1772
1896
  break;
1773
1897
  }
1774
1898
  case 1: {
1775
- await createLabels(configs);
1899
+ const summary = makeSummary();
1900
+ if (dryRun) {
1901
+ log(
1902
+ chalk.yellow(
1903
+ `[dry-run] Would create ${labels.length} preset labels (no API calls)`
1904
+ )
1905
+ );
1906
+ summary.skipped += labels.length;
1907
+ } else {
1908
+ const result = await createLabels(configs);
1909
+ summary.created = result.created;
1910
+ summary.failed = result.failed;
1911
+ }
1912
+ printSummary("Create preset labels", summary, dryRun);
1776
1913
  firstStart = firstStart && false;
1777
1914
  break;
1778
1915
  }
1779
1916
  case 2: {
1917
+ const summary = makeSummary();
1780
1918
  const targetLabel = await getTargetLabel();
1781
- await deleteLabel(configs, targetLabel);
1919
+ if (dryRun) {
1920
+ summary.skipped += targetLabel.length;
1921
+ targetLabel.forEach(
1922
+ (name) => log(chalk.yellow(`[dry-run] Would delete label "${name}"`))
1923
+ );
1924
+ } else {
1925
+ const result = await deleteLabel(configs, targetLabel);
1926
+ summary.deleted = result.deleted;
1927
+ summary.failed = result.failed;
1928
+ }
1929
+ printSummary("Delete a label", summary, dryRun);
1782
1930
  firstStart = firstStart && false;
1783
1931
  break;
1784
1932
  }
1785
1933
  case 3: {
1786
- await deleteLabels(configs);
1934
+ const summary = makeSummary();
1935
+ if (dryRun) {
1936
+ log(
1937
+ chalk.yellow(
1938
+ "[dry-run] Would delete all labels in the configured repository"
1939
+ )
1940
+ );
1941
+ summary.skipped += 1;
1942
+ } else {
1943
+ const result = await deleteLabels(configs);
1944
+ summary.deleted = result.deleted;
1945
+ summary.failed = result.failed;
1946
+ summary.notes.push("All labels processed");
1947
+ }
1948
+ printSummary("Delete all labels", summary, dryRun);
1787
1949
  firstStart = firstStart && false;
1788
1950
  break;
1789
1951
  }
1790
1952
  case 4: {
1953
+ const summary = makeSummary();
1791
1954
  try {
1792
1955
  const filePath = await getLabelFilePath();
1793
1956
  if (filePath) {
1794
- await importLabelsFromFile(configs, filePath);
1957
+ const result = await importLabelsFromFile(configs, filePath, dryRun);
1958
+ summary.created = result.succeeded;
1959
+ summary.failed = result.failed;
1960
+ summary.skipped = result.skipped;
1961
+ summary.notes.push(
1962
+ `Processed ${result.attempted} label entries from file`
1963
+ );
1795
1964
  } else {
1796
1965
  log(chalk.yellow("No file path provided. Returning to main menu."));
1966
+ summary.skipped += 1;
1797
1967
  }
1798
1968
  } catch (error) {
1799
1969
  log(
@@ -1801,7 +1971,9 @@ const main = async () => {
1801
1971
  `Error during label import: ${error instanceof Error ? error.message : "Unknown error"}`
1802
1972
  )
1803
1973
  );
1974
+ summary.failed += 1;
1804
1975
  }
1976
+ printSummary("Import labels", summary, dryRun);
1805
1977
  firstStart = firstStart && false;
1806
1978
  break;
1807
1979
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyouji",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
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>",
@@ -67,7 +67,6 @@
67
67
  "@types/node": "^24.10.0",
68
68
  "@vitest/coverage-v8": "^4.0.8",
69
69
  "@vitest/ui": "^4.0.8",
70
- "knip": "^5.68.0",
71
70
  "standard-version": "^9.5.0",
72
71
  "typescript": "^5.9.3",
73
72
  "vite": "^7.2.2",