hyouji 0.0.6 → 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.
- package/README.md +3 -0
- package/dist/index.js +268 -65
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -77,6 +77,9 @@ hyouji
|
|
|
77
77
|
On your first run, you'll be prompted to enter:
|
|
78
78
|
|
|
79
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
|
+
|
|
80
83
|
- **GitHub Username** - Your GitHub account name
|
|
81
84
|
|
|
82
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",
|
|
@@ -1109,6 +1111,177 @@ const getTargetLabel = async () => {
|
|
|
1109
1111
|
const response = await prompts(deleteLabel$1);
|
|
1110
1112
|
return [response.name];
|
|
1111
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
|
+
}
|
|
1112
1285
|
const getGitHubConfigs = async () => {
|
|
1113
1286
|
var _a, _b;
|
|
1114
1287
|
const configManager2 = new ConfigManager();
|
|
@@ -1130,6 +1303,50 @@ const getGitHubConfigs = async () => {
|
|
|
1130
1303
|
};
|
|
1131
1304
|
}
|
|
1132
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
|
+
}
|
|
1133
1350
|
const repoResponse = await prompts([
|
|
1134
1351
|
{
|
|
1135
1352
|
type: "text",
|
|
@@ -1144,7 +1361,9 @@ const getGitHubConfigs = async () => {
|
|
|
1144
1361
|
octokit: octokit2,
|
|
1145
1362
|
owner: validationResult.config.owner,
|
|
1146
1363
|
repo: repoResponse.repo,
|
|
1147
|
-
fromSavedConfig: true
|
|
1364
|
+
fromSavedConfig: true,
|
|
1365
|
+
autoDetected: false,
|
|
1366
|
+
detectionMethod: "manual"
|
|
1148
1367
|
};
|
|
1149
1368
|
}
|
|
1150
1369
|
const promptConfig = [...githubConfigs];
|
|
@@ -1196,7 +1415,9 @@ const getGitHubConfigs = async () => {
|
|
|
1196
1415
|
octokit,
|
|
1197
1416
|
owner: response.owner,
|
|
1198
1417
|
repo: response.repo,
|
|
1199
|
-
fromSavedConfig: false
|
|
1418
|
+
fromSavedConfig: false,
|
|
1419
|
+
autoDetected: false,
|
|
1420
|
+
detectionMethod: "manual"
|
|
1200
1421
|
};
|
|
1201
1422
|
};
|
|
1202
1423
|
const getJsonFilePath = async () => {
|
|
@@ -1215,33 +1436,6 @@ const selectAction = async () => {
|
|
|
1215
1436
|
const log = console.log;
|
|
1216
1437
|
let firstStart = true;
|
|
1217
1438
|
const configManager = new ConfigManager();
|
|
1218
|
-
const setupConfigs = async () => {
|
|
1219
|
-
console.log(initialText);
|
|
1220
|
-
if (firstStart) {
|
|
1221
|
-
await configManager.migrateToEncrypted();
|
|
1222
|
-
}
|
|
1223
|
-
const config = await getGitHubConfigs();
|
|
1224
|
-
if (!config.octokit || !config.owner || !config.repo) {
|
|
1225
|
-
throw new Error("Invalid configuration: missing required fields");
|
|
1226
|
-
}
|
|
1227
|
-
try {
|
|
1228
|
-
await config.octokit.request("GET /user");
|
|
1229
|
-
} catch (error) {
|
|
1230
|
-
if (config.fromSavedConfig) {
|
|
1231
|
-
console.log(
|
|
1232
|
-
chalk.yellow(
|
|
1233
|
-
"Saved credentials are invalid. Please provide new credentials."
|
|
1234
|
-
)
|
|
1235
|
-
);
|
|
1236
|
-
await configManager.clearConfig();
|
|
1237
|
-
return setupConfigs();
|
|
1238
|
-
}
|
|
1239
|
-
throw new Error(
|
|
1240
|
-
`GitHub API authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
|
-
return config;
|
|
1244
|
-
};
|
|
1245
1439
|
const displaySettings = async () => {
|
|
1246
1440
|
log(chalk.cyan("\n=== Current Settings ==="));
|
|
1247
1441
|
const configPath = configManager.getConfigPath();
|
|
@@ -1318,47 +1512,56 @@ const initializeConfigs = async () => {
|
|
|
1318
1512
|
console.warn("Failed to display ASCII art, continuing...");
|
|
1319
1513
|
console.error("Error:", error);
|
|
1320
1514
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
message: "Please type your target repo name"
|
|
1330
|
-
}
|
|
1331
|
-
]);
|
|
1332
|
-
const config = {
|
|
1333
|
-
octokit: new Octokit({ auth: existingConfig.config.token }),
|
|
1334
|
-
owner: existingConfig.config.owner,
|
|
1335
|
-
repo: repoResponse.repo,
|
|
1336
|
-
fromSavedConfig: true
|
|
1337
|
-
};
|
|
1338
|
-
log(chalk.green(`Using saved configuration for ${config.owner}`));
|
|
1339
|
-
return config;
|
|
1340
|
-
} else {
|
|
1341
|
-
return await setupConfigs();
|
|
1342
|
-
}
|
|
1343
|
-
} catch (error) {
|
|
1344
|
-
console.error("Error:", error);
|
|
1345
|
-
return await setupConfigs();
|
|
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");
|
|
1346
1523
|
}
|
|
1347
|
-
} else {
|
|
1348
1524
|
try {
|
|
1349
|
-
|
|
1525
|
+
await config.octokit.request("GET /user");
|
|
1526
|
+
} catch (error) {
|
|
1350
1527
|
if (config.fromSavedConfig) {
|
|
1351
|
-
log(
|
|
1528
|
+
console.log(
|
|
1529
|
+
chalk.yellow(
|
|
1530
|
+
"Saved credentials are invalid. Please provide new credentials."
|
|
1531
|
+
)
|
|
1532
|
+
);
|
|
1533
|
+
await configManager.clearConfig();
|
|
1534
|
+
return initializeConfigs();
|
|
1352
1535
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
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) {
|
|
1355
1544
|
log(
|
|
1356
|
-
chalk.
|
|
1357
|
-
|
|
1545
|
+
chalk.green(
|
|
1546
|
+
`✓ Repository auto-detected: ${config.owner}/${config.repo}`
|
|
1358
1547
|
)
|
|
1359
1548
|
);
|
|
1360
|
-
|
|
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`));
|
|
1361
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;
|
|
1362
1565
|
}
|
|
1363
1566
|
};
|
|
1364
1567
|
const main = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hyouji",
|
|
3
|
-
"version": "0.0.
|
|
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": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"test:error-handling": "node tests/scripts/error-handling/run-all.cjs",
|
|
39
39
|
"test:config": "node tests/scripts/config/run-all.cjs",
|
|
40
40
|
"test:integration": "node tests/scripts/run-integration.cjs",
|
|
41
|
+
"test:auto-detection": "node tests/integration/auto-detection/integration-flow.cjs",
|
|
41
42
|
"test:verification": "node tests/scripts/verification/run-all.cjs",
|
|
42
43
|
"test:all-custom": "npm run test:error-handling && npm run test:config && npm run test:integration && npm run test:verification",
|
|
43
44
|
"check-cli": "run-s test diff-integration-tests check-integration-tests",
|