shuho 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/LICENSE +49 -0
- package/README.md +72 -0
- package/dist/index.js +1252 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/commands/client.ts
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
|
|
9
|
+
// src/config/clients.ts
|
|
10
|
+
var ClientOperationError = class extends Error {
|
|
11
|
+
};
|
|
12
|
+
function addClient(config, client2, plan) {
|
|
13
|
+
if (config.clients.some((c) => c.name === client2.name)) {
|
|
14
|
+
throw new ClientOperationError(
|
|
15
|
+
`\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u300C${client2.name}\u300D\u306F\u65E2\u306B\u767B\u9332\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u5225\u306E\u540D\u524D\u3067\u767B\u9332\u3059\u308B\u304B\u3001\`shuho client remove ${client2.name}\` \u3067\u524A\u9664\u3057\u3066\u304B\u3089\u518D\u767B\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
if (plan === "free" && config.clients.length >= 1) {
|
|
19
|
+
throw new ClientOperationError(
|
|
20
|
+
"Free \u30D7\u30E9\u30F3\u3067\u767B\u9332\u3067\u304D\u308B\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306F 1 \u4EF6\u307E\u3067\u3067\u3059\u3002\u8907\u6570\u6848\u4EF6\u3092\u6271\u3046\u5834\u5408\u306F Pro \u30D7\u30E9\u30F3(\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u6570\u7121\u5236\u9650)\u3092\u3054\u691C\u8A0E\u304F\u3060\u3055\u3044\u3002"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return { ...config, clients: [...config.clients, client2] };
|
|
24
|
+
}
|
|
25
|
+
function removeClient(config, name) {
|
|
26
|
+
if (!config.clients.some((c) => c.name === name)) {
|
|
27
|
+
const registered = config.clients.map((c) => c.name).join(", ");
|
|
28
|
+
throw new ClientOperationError(
|
|
29
|
+
`\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u300C${name}\u300D\u306F\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u767B\u9332\u6E08\u307F: ${registered === "" ? "(\u306A\u3057)" : registered}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return { ...config, clients: config.clients.filter((c) => c.name !== name) };
|
|
33
|
+
}
|
|
34
|
+
function findClient(config, name) {
|
|
35
|
+
if (name !== void 0) {
|
|
36
|
+
const found = config.clients.find((c) => c.name === name);
|
|
37
|
+
if (found === void 0) {
|
|
38
|
+
throw new ClientOperationError(
|
|
39
|
+
`\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u300C${name}\u300D\u306F\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\`shuho client list\` \u3067\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return found;
|
|
43
|
+
}
|
|
44
|
+
const first = config.clients[0];
|
|
45
|
+
if (first === void 0) {
|
|
46
|
+
throw new ClientOperationError(
|
|
47
|
+
"\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u304C\u672A\u767B\u9332\u3067\u3059\u3002`shuho client add <name>` \u3067\u767B\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (config.clients.length > 1) {
|
|
51
|
+
throw new ClientOperationError(
|
|
52
|
+
`\u8907\u6570\u306E\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u304C\u767B\u9332\u3055\u308C\u3066\u3044\u307E\u3059\u3002--client \u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${config.clients.map((c) => c.name).join(", ")}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return first;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/config/schema.ts
|
|
59
|
+
import { z } from "zod";
|
|
60
|
+
|
|
61
|
+
// src/llm/index.ts
|
|
62
|
+
var LLM_PROVIDERS = ["claude-cli", "codex-cli", "anthropic", "openai"];
|
|
63
|
+
|
|
64
|
+
// src/config/schema.ts
|
|
65
|
+
var toneSchema = z.enum(["polite", "casual"]);
|
|
66
|
+
var llmConfigSchema = z.object({
|
|
67
|
+
provider: z.enum(LLM_PROVIDERS),
|
|
68
|
+
model: z.string().min(1).optional()
|
|
69
|
+
});
|
|
70
|
+
var clientConfigSchema = z.object({
|
|
71
|
+
name: z.string().min(1),
|
|
72
|
+
repos: z.array(
|
|
73
|
+
z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, "owner/repo \u5F62\u5F0F\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044")
|
|
74
|
+
),
|
|
75
|
+
addressee: z.string().min(1),
|
|
76
|
+
tone: toneSchema
|
|
77
|
+
});
|
|
78
|
+
var configSchema = z.object({
|
|
79
|
+
github: z.object({ token: z.string().min(1).optional() }).optional(),
|
|
80
|
+
llm: llmConfigSchema,
|
|
81
|
+
clients: z.array(clientConfigSchema).default([]),
|
|
82
|
+
license: z.object({ key: z.string().min(1).optional() }).optional()
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// src/config/store.ts
|
|
86
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
87
|
+
import { homedir } from "os";
|
|
88
|
+
import { join } from "path";
|
|
89
|
+
import { parse, stringify } from "smol-toml";
|
|
90
|
+
|
|
91
|
+
// src/util/errors.ts
|
|
92
|
+
function getErrorMessage(error) {
|
|
93
|
+
return error instanceof Error ? error.message : String(error);
|
|
94
|
+
}
|
|
95
|
+
function isErrnoException(error) {
|
|
96
|
+
return error instanceof Error && "code" in error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/config/store.ts
|
|
100
|
+
var CONFIG_FILE_NAME = "config.toml";
|
|
101
|
+
var ConfigError = class extends Error {
|
|
102
|
+
};
|
|
103
|
+
function getConfigDir(env) {
|
|
104
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
105
|
+
const base = xdg !== void 0 && xdg !== "" ? xdg : join(homedir(), ".config");
|
|
106
|
+
return join(base, "shuho");
|
|
107
|
+
}
|
|
108
|
+
async function loadConfig(dir) {
|
|
109
|
+
const path = join(dir, CONFIG_FILE_NAME);
|
|
110
|
+
let raw;
|
|
111
|
+
try {
|
|
112
|
+
raw = await readFile(path, "utf8");
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isErrnoException(error) && error.code === "ENOENT") return null;
|
|
115
|
+
throw new ConfigError(
|
|
116
|
+
`\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F: ${path}
|
|
117
|
+
${getErrorMessage(error)}
|
|
118
|
+
\u30D5\u30A1\u30A4\u30EB\u306E\u6A29\u9650\u3068\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
let data;
|
|
122
|
+
try {
|
|
123
|
+
data = parse(raw);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new ConfigError(
|
|
126
|
+
`\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u306E TOML \u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${path}
|
|
127
|
+
${getErrorMessage(error)}
|
|
128
|
+
\`shuho init\` \u3067\u518D\u751F\u6210\u3067\u304D\u307E\u3059\u3002`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const result = configSchema.safeParse(data);
|
|
132
|
+
if (!result.success) {
|
|
133
|
+
const issues = result.error.issues.map((i) => `- ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
134
|
+
throw new ConfigError(
|
|
135
|
+
`\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u306E\u5185\u5BB9\u304C\u4E0D\u6B63\u3067\u3059: ${path}
|
|
136
|
+
${issues}
|
|
137
|
+
\`shuho init\` \u3067\u518D\u751F\u6210\u3067\u304D\u307E\u3059\u3002`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return result.data;
|
|
141
|
+
}
|
|
142
|
+
async function requireConfig(dir) {
|
|
143
|
+
const config = await loadConfig(dir);
|
|
144
|
+
if (config === null) {
|
|
145
|
+
throw new Error("\u8A2D\u5B9A\u304C\u3042\u308A\u307E\u305B\u3093\u3002\u307E\u305A `shuho init` \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
146
|
+
}
|
|
147
|
+
return config;
|
|
148
|
+
}
|
|
149
|
+
async function saveConfig(dir, config) {
|
|
150
|
+
try {
|
|
151
|
+
await mkdir(dir, { recursive: true });
|
|
152
|
+
const clean = JSON.parse(JSON.stringify(config));
|
|
153
|
+
await writeFile(join(dir, CONFIG_FILE_NAME), stringify(clean), "utf8");
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new ConfigError(
|
|
156
|
+
`\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F: ${join(dir, CONFIG_FILE_NAME)}
|
|
157
|
+
${getErrorMessage(error)}
|
|
158
|
+
\u4FDD\u5B58\u5148\u306E\u6A29\u9650\u3068\u7A7A\u304D\u5BB9\u91CF\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/license/polar.ts
|
|
164
|
+
import { z as z2 } from "zod";
|
|
165
|
+
var VALIDATE_URL = "https://api.polar.sh/v1/customer-portal/license-keys/validate";
|
|
166
|
+
var SHUHO_POLAR_ORGANIZATION_ID = "ccf25697-6879-4154-ad05-e43b37e83066";
|
|
167
|
+
var responseSchema = z2.object({ status: z2.enum(["granted", "revoked", "disabled"]) });
|
|
168
|
+
function createPolarValidator(fetchFn, organizationId) {
|
|
169
|
+
return {
|
|
170
|
+
async validate(key2) {
|
|
171
|
+
const response = await fetchFn(VALIDATE_URL, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
174
|
+
body: JSON.stringify({ key: key2, organization_id: organizationId })
|
|
175
|
+
});
|
|
176
|
+
if (response.status === 404) return false;
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
throw new Error(`Polar API \u304C\u30B9\u30C6\u30FC\u30BF\u30B9 ${String(response.status)} \u3092\u8FD4\u3057\u307E\u3057\u305F`);
|
|
179
|
+
}
|
|
180
|
+
const data = await response.json();
|
|
181
|
+
const parsed = responseSchema.safeParse(data);
|
|
182
|
+
if (!parsed.success) throw new Error("Polar API \u306E\u5FDC\u7B54\u5F62\u5F0F\u304C\u60F3\u5B9A\u5916\u3067\u3059");
|
|
183
|
+
return parsed.data.status === "granted";
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/license/plan.ts
|
|
189
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
190
|
+
import { join as join2 } from "path";
|
|
191
|
+
import { z as z3 } from "zod";
|
|
192
|
+
var CACHE_FILE = "license-cache.json";
|
|
193
|
+
var CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
194
|
+
var cacheSchema = z3.object({
|
|
195
|
+
key: z3.string(),
|
|
196
|
+
plan: z3.enum(["free", "pro"]),
|
|
197
|
+
validatedAt: z3.string()
|
|
198
|
+
});
|
|
199
|
+
async function readCache(configDir) {
|
|
200
|
+
let raw;
|
|
201
|
+
try {
|
|
202
|
+
raw = await readFile2(join2(configDir, CACHE_FILE), "utf8");
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const parsed = cacheSchema.safeParse(JSON.parse(raw));
|
|
208
|
+
return parsed.success ? parsed.data : null;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function writeCache(configDir, entry) {
|
|
214
|
+
await mkdir2(configDir, { recursive: true });
|
|
215
|
+
await writeFile2(join2(configDir, CACHE_FILE), JSON.stringify(entry), "utf8");
|
|
216
|
+
}
|
|
217
|
+
function devProBypass(env) {
|
|
218
|
+
if (env.SHUHO_DEV_PRO === "1") {
|
|
219
|
+
return {
|
|
220
|
+
plan: "pro",
|
|
221
|
+
warning: "\u958B\u767A\u7528\u30D0\u30A4\u30D1\u30B9(SHUHO_DEV_PRO=1)\u6709\u52B9: \u30E9\u30A4\u30BB\u30F3\u30B9\u691C\u8A3C\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u3066 Pro \u30D7\u30E9\u30F3\u3068\u3057\u3066\u52D5\u4F5C\u3057\u307E\u3059\u3002"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
async function resolvePlan(opts) {
|
|
227
|
+
const { key: key2, configDir, validator, now } = opts;
|
|
228
|
+
if (key2 === void 0 || key2 === "") return { plan: "free" };
|
|
229
|
+
const cache = await readCache(configDir);
|
|
230
|
+
if (cache !== null && cache.key === key2 && now.getTime() - Date.parse(cache.validatedAt) < CACHE_TTL_MS) {
|
|
231
|
+
return { plan: cache.plan };
|
|
232
|
+
}
|
|
233
|
+
let valid;
|
|
234
|
+
try {
|
|
235
|
+
valid = await validator.validate(key2);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return {
|
|
238
|
+
plan: "free",
|
|
239
|
+
warning: `\u30E9\u30A4\u30BB\u30F3\u30B9\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F(${getErrorMessage(error)})\u3002\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u63A5\u7D9A\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4ECA\u56DE\u306F Free \u30D7\u30E9\u30F3\u3068\u3057\u3066\u52D5\u4F5C\u3057\u307E\u3059\u3002`
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const plan = valid ? "pro" : "free";
|
|
243
|
+
let cacheWarning;
|
|
244
|
+
try {
|
|
245
|
+
await writeCache(configDir, { key: key2, plan, validatedAt: now.toISOString() });
|
|
246
|
+
} catch (error) {
|
|
247
|
+
cacheWarning = `\u30E9\u30A4\u30BB\u30F3\u30B9\u691C\u8A3C\u7D50\u679C\u306E\u30AD\u30E3\u30C3\u30B7\u30E5\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F(${getErrorMessage(error)})\u3002\u52D5\u4F5C\u306B\u306F\u5F71\u97FF\u3057\u307E\u305B\u3093\u304C\u6B21\u56DE\u3082\u518D\u691C\u8A3C\u304C\u884C\u308F\u308C\u307E\u3059\u3002\u4FDD\u5B58\u5148(${configDir})\u306E\u6A29\u9650\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`;
|
|
248
|
+
}
|
|
249
|
+
const invalidWarning = valid ? void 0 : "\u30E9\u30A4\u30BB\u30F3\u30B9\u30AD\u30FC\u304C\u7121\u52B9\u3067\u3059\u3002Free \u30D7\u30E9\u30F3\u3068\u3057\u3066\u52D5\u4F5C\u3057\u307E\u3059\u3002\u30AD\u30FC\u306F\u8CFC\u5165\u6642\u306E\u30E1\u30FC\u30EB\u3067\u78BA\u8A8D\u3067\u304D\u307E\u3059\u3002";
|
|
250
|
+
const warnings = [invalidWarning, cacheWarning].filter((w) => w !== void 0);
|
|
251
|
+
return warnings.length > 0 ? { plan, warning: warnings.join("\n") } : { plan };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/util/tty.ts
|
|
255
|
+
function isInteractive(stdin = process.stdin, stdout = process.stdout) {
|
|
256
|
+
return stdin.isTTY === true && stdout.isTTY === true;
|
|
257
|
+
}
|
|
258
|
+
function canRenderSpinner(stdout = process.stdout) {
|
|
259
|
+
return stdout.isTTY === true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/cli/commands/client.ts
|
|
263
|
+
async function runClientAdd(name) {
|
|
264
|
+
if (!isInteractive()) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"shuho client add \u306F\u5BFE\u8A71\u5F0F\u30B3\u30DE\u30F3\u30C9\u3067\u3059\u3002\u30BF\u30FC\u30DF\u30CA\u30EB\u304B\u3089\u76F4\u63A5\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044(\u30D1\u30A4\u30D7\u30FB\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u7D4C\u7531\u3067\u306F\u5B9F\u884C\u3067\u304D\u307E\u305B\u3093)\u3002"
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
const dir = getConfigDir(process.env);
|
|
270
|
+
const config = await requireConfig(dir);
|
|
271
|
+
const reposInput = await p.text({
|
|
272
|
+
message: "\u5BFE\u8C61\u30EA\u30DD\u30B8\u30C8\u30EA(owner/repo \u5F62\u5F0F\u3002\u8907\u6570\u306F\u30AB\u30F3\u30DE\u533A\u5207\u308A)",
|
|
273
|
+
placeholder: "ysakae/shuho, ysakae/other"
|
|
274
|
+
});
|
|
275
|
+
if (p.isCancel(reposInput)) {
|
|
276
|
+
p.cancel("\u4E2D\u65AD\u3057\u307E\u3057\u305F");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const addressee = await p.text({ message: "\u5831\u544A\u66F8\u306E\u5B9B\u540D", placeholder: "\u682A\u5F0F\u4F1A\u793E\u25EF\u25EF \u25B3\u25B3\u69D8" });
|
|
280
|
+
if (p.isCancel(addressee)) {
|
|
281
|
+
p.cancel("\u4E2D\u65AD\u3057\u307E\u3057\u305F");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const tone = await p.select({
|
|
285
|
+
message: "\u6587\u4F53\u30C8\u30FC\u30F3",
|
|
286
|
+
options: [
|
|
287
|
+
{ value: "polite", label: "\u4E01\u5BE7(\u6539\u307E\u3063\u305F\u30D3\u30B8\u30CD\u30B9\u6587\u66F8)" },
|
|
288
|
+
{ value: "casual", label: "\u30AB\u30B8\u30E5\u30A2\u30EB(\u67D4\u3089\u304B\u3044\u4E01\u5BE7\u8A9E)" }
|
|
289
|
+
]
|
|
290
|
+
});
|
|
291
|
+
if (p.isCancel(tone)) {
|
|
292
|
+
p.cancel("\u4E2D\u65AD\u3057\u307E\u3057\u305F");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const parsed = clientConfigSchema.safeParse({
|
|
296
|
+
name,
|
|
297
|
+
repos: reposInput.split(",").map((r) => r.trim()).filter((r) => r !== ""),
|
|
298
|
+
addressee,
|
|
299
|
+
tone
|
|
300
|
+
});
|
|
301
|
+
if (!parsed.success) {
|
|
302
|
+
const issues = parsed.error.issues.map((i) => `- ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
303
|
+
throw new Error(`\u5165\u529B\u304C\u4E0D\u6B63\u3067\u3059:
|
|
304
|
+
${issues}`);
|
|
305
|
+
}
|
|
306
|
+
const { plan, warning } = devProBypass(process.env) ?? await resolvePlan({
|
|
307
|
+
key: config.license?.key,
|
|
308
|
+
configDir: dir,
|
|
309
|
+
validator: createPolarValidator(fetch, SHUHO_POLAR_ORGANIZATION_ID),
|
|
310
|
+
now: /* @__PURE__ */ new Date()
|
|
311
|
+
});
|
|
312
|
+
if (warning !== void 0) p.log.warn(warning);
|
|
313
|
+
await saveConfig(dir, addClient(config, parsed.data, plan));
|
|
314
|
+
p.log.success(`\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u300C${name}\u300D\u3092\u767B\u9332\u3057\u307E\u3057\u305F\u3002`);
|
|
315
|
+
}
|
|
316
|
+
async function runClientList() {
|
|
317
|
+
const config = await loadConfig(getConfigDir(process.env));
|
|
318
|
+
if (config === null || config.clients.length === 0) {
|
|
319
|
+
console.log("\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306F\u672A\u767B\u9332\u3067\u3059\u3002`shuho client add <name>` \u3067\u767B\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
for (const client2 of config.clients) {
|
|
323
|
+
console.log(
|
|
324
|
+
`${client2.name}
|
|
325
|
+
\u5B9B\u540D: ${client2.addressee}
|
|
326
|
+
\u30C8\u30FC\u30F3: ${client2.tone === "polite" ? "\u4E01\u5BE7" : "\u30AB\u30B8\u30E5\u30A2\u30EB"}
|
|
327
|
+
\u30EA\u30DD\u30B8\u30C8\u30EA: ${client2.repos.join(", ")}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function runClientRemove(name) {
|
|
332
|
+
const dir = getConfigDir(process.env);
|
|
333
|
+
const config = await requireConfig(dir);
|
|
334
|
+
await saveConfig(dir, removeClient(config, name));
|
|
335
|
+
console.log(`\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u300C${name}\u300D\u3092\u524A\u9664\u3057\u307E\u3057\u305F\u3002`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/config/github-auth.ts
|
|
339
|
+
async function resolveGithubToken(config, run) {
|
|
340
|
+
const stored = config?.github?.token;
|
|
341
|
+
if (stored !== void 0 && stored !== "") return stored;
|
|
342
|
+
try {
|
|
343
|
+
const result = await run("gh", ["auth", "token"]);
|
|
344
|
+
const token = result.stdout.trim();
|
|
345
|
+
if (result.exitCode === 0 && token !== "") return token;
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/doctor/checks.ts
|
|
352
|
+
async function runChecks(deps) {
|
|
353
|
+
const results = [];
|
|
354
|
+
let config = null;
|
|
355
|
+
try {
|
|
356
|
+
config = await deps.loadConfig();
|
|
357
|
+
results.push(
|
|
358
|
+
config === null ? {
|
|
359
|
+
name: "\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB",
|
|
360
|
+
ok: false,
|
|
361
|
+
detail: "\u672A\u4F5C\u6210\u3067\u3059\u3002`shuho init` \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
362
|
+
} : { name: "\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB", ok: true, detail: "\u6709\u52B9\u3067\u3059\u3002" }
|
|
363
|
+
);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
results.push({ name: "\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB", ok: false, detail: getErrorMessage(error) });
|
|
366
|
+
}
|
|
367
|
+
const token = await deps.resolveToken(config);
|
|
368
|
+
if (token !== null) {
|
|
369
|
+
results.push({ name: "GitHub \u8A8D\u8A3C", ok: true, detail: "\u30C8\u30FC\u30AF\u30F3\u3092\u691C\u51FA\u3057\u307E\u3057\u305F\u3002" });
|
|
370
|
+
} else {
|
|
371
|
+
const licenseKey = config?.license?.key;
|
|
372
|
+
const hasLicenseKey = licenseKey !== void 0 && licenseKey !== "";
|
|
373
|
+
results.push(
|
|
374
|
+
hasLicenseKey ? {
|
|
375
|
+
name: "GitHub \u8A8D\u8A3C",
|
|
376
|
+
ok: false,
|
|
377
|
+
detail: "Pro \u30D7\u30E9\u30F3(\u30E9\u30A4\u30BB\u30F3\u30B9\u30AD\u30FC\u8A2D\u5B9A\u6E08\u307F)\u3067\u3059\u304C\u3001\u30C8\u30FC\u30AF\u30F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002`gh auth login` \u3092\u5B9F\u884C\u3059\u308B\u304B\u3001`shuho init` \u3067 PAT \u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
378
|
+
} : {
|
|
379
|
+
name: "GitHub \u8A8D\u8A3C",
|
|
380
|
+
ok: true,
|
|
381
|
+
detail: "\u30C8\u30FC\u30AF\u30F3\u672A\u691C\u51FA(\u30ED\u30FC\u30AB\u30EB git \u306E\u307F\u4F7F\u3046 Free \u30D7\u30E9\u30F3\u3067\u306F\u4E0D\u8981\u3067\u3059)\u3002Pro \u30D7\u30E9\u30F3\u5229\u7528\u6642\u306F `gh auth login` \u307E\u305F\u306F `shuho init` \u3067 PAT \u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (config !== null) {
|
|
386
|
+
const provider = config.llm.provider;
|
|
387
|
+
if (provider === "claude-cli" || provider === "codex-cli") {
|
|
388
|
+
const command = provider === "claude-cli" ? "claude" : "codex";
|
|
389
|
+
const exists = await deps.commandExists(command);
|
|
390
|
+
results.push({
|
|
391
|
+
name: `LLM(${provider})`,
|
|
392
|
+
ok: exists,
|
|
393
|
+
detail: exists ? `${command} \u30B3\u30DE\u30F3\u30C9\u3092\u691C\u51FA\u3057\u307E\u3057\u305F\u3002` : `${command} \u30B3\u30DE\u30F3\u30C9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3059\u308B\u304B\u3001\`shuho init\` \u3067\u5225\u306E\u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
394
|
+
});
|
|
395
|
+
} else {
|
|
396
|
+
const envName = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
|
|
397
|
+
const isSet = (deps.env[envName] ?? "") !== "";
|
|
398
|
+
results.push({
|
|
399
|
+
name: `LLM(${provider})`,
|
|
400
|
+
ok: isSet,
|
|
401
|
+
detail: isSet ? `${envName} \u304C\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u3059\u3002` : `${envName} \u304C\u672A\u8A2D\u5B9A\u3067\u3059\u3002\u74B0\u5883\u5909\u6570\u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return results;
|
|
406
|
+
}
|
|
407
|
+
function formatChecks(results) {
|
|
408
|
+
return results.map((r) => `${r.ok ? "\u2713" : "\u2717"} ${r.name}: ${r.detail}`).join("\n");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/llm/detect.ts
|
|
412
|
+
async function commandExists(run, command, platform = process.platform) {
|
|
413
|
+
const finder = platform === "win32" ? "where" : "which";
|
|
414
|
+
try {
|
|
415
|
+
return (await run(finder, [command])).exitCode === 0;
|
|
416
|
+
} catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function detectBackends(run, env) {
|
|
421
|
+
const found = [];
|
|
422
|
+
if (await commandExists(run, "claude")) {
|
|
423
|
+
found.push({ provider: "claude-cli", label: "Claude Code CLI(\u8FFD\u52A0\u8CBB\u7528\u306A\u3057\u30FB\u63A8\u5968)" });
|
|
424
|
+
}
|
|
425
|
+
if (await commandExists(run, "codex")) {
|
|
426
|
+
found.push({ provider: "codex-cli", label: "Codex CLI(\u8FFD\u52A0\u8CBB\u7528\u306A\u3057)" });
|
|
427
|
+
}
|
|
428
|
+
if ((env.ANTHROPIC_API_KEY ?? "") !== "") {
|
|
429
|
+
found.push({ provider: "anthropic", label: "Anthropic API(ANTHROPIC_API_KEY \u3092\u4F7F\u7528)" });
|
|
430
|
+
}
|
|
431
|
+
if ((env.OPENAI_API_KEY ?? "") !== "") {
|
|
432
|
+
found.push({ provider: "openai", label: "OpenAI API(OPENAI_API_KEY \u3092\u4F7F\u7528)" });
|
|
433
|
+
}
|
|
434
|
+
return found;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/util/run-command.ts
|
|
438
|
+
import { spawn } from "child_process";
|
|
439
|
+
var runCommand = (command, args, options = {}) => new Promise((resolve2, reject) => {
|
|
440
|
+
const child = spawn(command, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
441
|
+
let stdout = "";
|
|
442
|
+
let stderr = "";
|
|
443
|
+
child.stdout.setEncoding("utf8");
|
|
444
|
+
child.stderr.setEncoding("utf8");
|
|
445
|
+
child.stdout.on("data", (chunk) => {
|
|
446
|
+
stdout += chunk;
|
|
447
|
+
});
|
|
448
|
+
child.stderr.on("data", (chunk) => {
|
|
449
|
+
stderr += chunk;
|
|
450
|
+
});
|
|
451
|
+
child.on("error", reject);
|
|
452
|
+
child.on("close", (code) => {
|
|
453
|
+
resolve2({ stdout, stderr, exitCode: code ?? -1 });
|
|
454
|
+
});
|
|
455
|
+
if (options.stdin !== void 0) child.stdin.write(options.stdin);
|
|
456
|
+
child.stdin.end();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// src/cli/commands/doctor.ts
|
|
460
|
+
async function runDoctor() {
|
|
461
|
+
const dir = getConfigDir(process.env);
|
|
462
|
+
const results = await runChecks({
|
|
463
|
+
loadConfig: () => loadConfig(dir),
|
|
464
|
+
// config 不在(init 前)でも gh CLI のトークン検出は試みる(Issue #3)
|
|
465
|
+
resolveToken: (config) => resolveGithubToken(config, runCommand),
|
|
466
|
+
commandExists: (command) => commandExists(runCommand, command),
|
|
467
|
+
env: process.env
|
|
468
|
+
});
|
|
469
|
+
console.log(formatChecks(results));
|
|
470
|
+
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/config/license-key.ts
|
|
474
|
+
function withLicenseKey(config, key2) {
|
|
475
|
+
return { ...config, license: { key: key2 } };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/cli/commands/license.ts
|
|
479
|
+
async function runLicense(key2) {
|
|
480
|
+
const trimmed = key2.trim();
|
|
481
|
+
if (trimmed === "") {
|
|
482
|
+
throw new Error("\u30E9\u30A4\u30BB\u30F3\u30B9\u30AD\u30FC\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B: shuho license XXXX-XXXX-XXXX");
|
|
483
|
+
}
|
|
484
|
+
const dir = getConfigDir(process.env);
|
|
485
|
+
const config = await requireConfig(dir);
|
|
486
|
+
await saveConfig(dir, withLicenseKey(config, trimmed));
|
|
487
|
+
console.log("\u30E9\u30A4\u30BB\u30F3\u30B9\u30AD\u30FC\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F\u3002\u691C\u8A3C\u3057\u3066\u3044\u307E\u3059...");
|
|
488
|
+
const { plan, warning } = await resolvePlan({
|
|
489
|
+
key: trimmed,
|
|
490
|
+
configDir: dir,
|
|
491
|
+
validator: createPolarValidator(fetch, SHUHO_POLAR_ORGANIZATION_ID),
|
|
492
|
+
now: /* @__PURE__ */ new Date()
|
|
493
|
+
});
|
|
494
|
+
if (warning !== void 0) console.warn(warning);
|
|
495
|
+
console.log(
|
|
496
|
+
plan === "pro" ? "\u2713 Pro \u30D7\u30E9\u30F3\u304C\u6709\u52B9\u306B\u306A\u308A\u307E\u3057\u305F\u3002`shuho generate` \u3067 GitHub API \u9023\u643A\u306E\u9031\u5831\u3092\u751F\u6210\u3067\u304D\u307E\u3059\u3002" : "\u73FE\u5728\u306F Free \u30D7\u30E9\u30F3\u3068\u3057\u3066\u52D5\u4F5C\u3057\u307E\u3059(\u4E0A\u8A18\u306E\u8B66\u544A\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044)\u3002"
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/cli/commands/generate.ts
|
|
501
|
+
import * as p2 from "@clack/prompts";
|
|
502
|
+
import { Octokit } from "@octokit/rest";
|
|
503
|
+
|
|
504
|
+
// src/util/date.ts
|
|
505
|
+
function toDateOnly(date) {
|
|
506
|
+
return date.toISOString().slice(0, 10);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/collector/types.ts
|
|
510
|
+
function emptyActivity(periodStart, periodEnd) {
|
|
511
|
+
return {
|
|
512
|
+
periodStart,
|
|
513
|
+
periodEnd,
|
|
514
|
+
commits: [],
|
|
515
|
+
pullRequestsCreated: [],
|
|
516
|
+
pullRequestsMerged: [],
|
|
517
|
+
pullRequestsReviewed: [],
|
|
518
|
+
issuesOpened: [],
|
|
519
|
+
issuesClosed: []
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/collector/github.ts
|
|
524
|
+
function repoFromUrl(url) {
|
|
525
|
+
return url.split("/repos/")[1] ?? url;
|
|
526
|
+
}
|
|
527
|
+
function firstLine(message) {
|
|
528
|
+
return message.split("\n")[0] ?? message;
|
|
529
|
+
}
|
|
530
|
+
function toPullRequest(item) {
|
|
531
|
+
return {
|
|
532
|
+
repo: repoFromUrl(item.repository_url),
|
|
533
|
+
number: item.number,
|
|
534
|
+
title: item.title,
|
|
535
|
+
body: item.body ?? ""
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function toIssue(item) {
|
|
539
|
+
return {
|
|
540
|
+
repo: repoFromUrl(item.repository_url),
|
|
541
|
+
number: item.number,
|
|
542
|
+
title: item.title,
|
|
543
|
+
body: item.body ?? ""
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
var MAX_SEARCH_QUERY_LENGTH = 256;
|
|
547
|
+
function chunkRepoQualifiers(repos, conditionLength) {
|
|
548
|
+
const budget = MAX_SEARCH_QUERY_LENGTH - conditionLength - " ".length;
|
|
549
|
+
const chunks = [];
|
|
550
|
+
let current = "";
|
|
551
|
+
for (const repo of repos) {
|
|
552
|
+
const qualifier = `repo:${repo}`;
|
|
553
|
+
const combined = current === "" ? qualifier : `${current} ${qualifier}`;
|
|
554
|
+
if (current !== "" && combined.length > budget) {
|
|
555
|
+
chunks.push(current);
|
|
556
|
+
current = qualifier;
|
|
557
|
+
} else {
|
|
558
|
+
current = combined;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return current === "" ? chunks : [...chunks, current];
|
|
562
|
+
}
|
|
563
|
+
async function collectGithub(port, repos, range) {
|
|
564
|
+
const login = await port.getLogin();
|
|
565
|
+
const start = toDateOnly(range.start);
|
|
566
|
+
const end = toDateOnly(range.end);
|
|
567
|
+
const dateRange = `${start}..${end}`;
|
|
568
|
+
const commits = (await Promise.all(
|
|
569
|
+
repos.map(async (full) => {
|
|
570
|
+
const [owner, repo] = full.split("/");
|
|
571
|
+
if (owner === void 0 || repo === void 0 || owner === "" || repo === "") {
|
|
572
|
+
throw new Error(`\u30EA\u30DD\u30B8\u30C8\u30EA\u6307\u5B9A\u304C\u4E0D\u6B63\u3067\u3059(owner/repo \u5F62\u5F0F\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044): ${full}`);
|
|
573
|
+
}
|
|
574
|
+
const items = await port.listRepoCommits(owner, repo, {
|
|
575
|
+
author: login,
|
|
576
|
+
since: range.start.toISOString(),
|
|
577
|
+
until: range.end.toISOString()
|
|
578
|
+
});
|
|
579
|
+
return items.map((item) => ({
|
|
580
|
+
repo: full,
|
|
581
|
+
message: firstLine(item.commit.message),
|
|
582
|
+
date: item.commit.author?.date ?? ""
|
|
583
|
+
}));
|
|
584
|
+
})
|
|
585
|
+
)).flat();
|
|
586
|
+
const searchAll = async (condition) => {
|
|
587
|
+
const results = await Promise.all(
|
|
588
|
+
chunkRepoQualifiers(repos, condition.length).map(
|
|
589
|
+
(qualifiers) => port.searchIssues(`${qualifiers} ${condition}`)
|
|
590
|
+
)
|
|
591
|
+
);
|
|
592
|
+
return results.flat();
|
|
593
|
+
};
|
|
594
|
+
const [created, merged, reviewed, opened, closed] = await Promise.all([
|
|
595
|
+
searchAll(`is:pr author:${login} created:${dateRange}`),
|
|
596
|
+
searchAll(`is:pr author:${login} merged:${dateRange}`),
|
|
597
|
+
searchAll(`is:pr reviewed-by:${login} -author:${login} updated:${dateRange}`),
|
|
598
|
+
searchAll(`is:issue author:${login} created:${dateRange}`),
|
|
599
|
+
searchAll(`is:issue author:${login} closed:${dateRange}`)
|
|
600
|
+
]);
|
|
601
|
+
return {
|
|
602
|
+
...emptyActivity(start, end),
|
|
603
|
+
commits,
|
|
604
|
+
pullRequestsCreated: created.map(toPullRequest),
|
|
605
|
+
pullRequestsMerged: merged.map(toPullRequest),
|
|
606
|
+
pullRequestsReviewed: reviewed.map(toPullRequest),
|
|
607
|
+
issuesOpened: opened.map(toIssue),
|
|
608
|
+
issuesClosed: closed.map(toIssue)
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/collector/github-octokit.ts
|
|
613
|
+
var PER_PAGE = 100;
|
|
614
|
+
function createOctokitPort(octokit) {
|
|
615
|
+
return {
|
|
616
|
+
async getLogin() {
|
|
617
|
+
return (await octokit.rest.users.getAuthenticated()).data.login;
|
|
618
|
+
},
|
|
619
|
+
async searchIssues(query) {
|
|
620
|
+
return (await octokit.rest.search.issuesAndPullRequests({ q: query, per_page: PER_PAGE })).data.items;
|
|
621
|
+
},
|
|
622
|
+
async listRepoCommits(owner, repo, opts) {
|
|
623
|
+
return (await octokit.rest.repos.listCommits({
|
|
624
|
+
owner,
|
|
625
|
+
repo,
|
|
626
|
+
author: opts.author,
|
|
627
|
+
since: opts.since,
|
|
628
|
+
until: opts.until,
|
|
629
|
+
per_page: PER_PAGE
|
|
630
|
+
})).data;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/collector/local-git.ts
|
|
636
|
+
import { basename } from "path";
|
|
637
|
+
var FIELD_SEP = "";
|
|
638
|
+
var RECORD_SEP = "";
|
|
639
|
+
var FETCH_MARGIN_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
640
|
+
var GitCollectError = class extends Error {
|
|
641
|
+
};
|
|
642
|
+
function parseGitLog(output, repo) {
|
|
643
|
+
return output.split(RECORD_SEP).map((record) => record.trim()).filter((record) => record !== "").map((record) => {
|
|
644
|
+
const [date, message] = record.split(FIELD_SEP);
|
|
645
|
+
if (date === void 0 || message === void 0) {
|
|
646
|
+
throw new GitCollectError(
|
|
647
|
+
`git log \u306E\u51FA\u529B\u3092\u89E3\u6790\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F(\u60F3\u5B9A\u5916\u306E\u5F62\u5F0F): ${record}
|
|
648
|
+
git \u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u3068\u30EA\u30DD\u30B8\u30C8\u30EA\u306E\u72B6\u614B\u3092\u78BA\u8A8D\u306E\u3046\u3048\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
return { repo, date, message };
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
async function collectLocalGit(run, range, cwd) {
|
|
655
|
+
const toplevel = await run("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
656
|
+
if (toplevel.exitCode !== 0) {
|
|
657
|
+
throw new GitCollectError(
|
|
658
|
+
"\u30AB\u30EC\u30F3\u30C8\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306F git \u30EA\u30DD\u30B8\u30C8\u30EA\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u30EA\u30DD\u30B8\u30C8\u30EA\u5185\u3067\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044(GitHub API \u304B\u3089\u53CE\u96C6\u3059\u308B Pro \u30D7\u30E9\u30F3\u3067\u306F\u3053\u306E\u5236\u7D04\u306F\u3042\u308A\u307E\u305B\u3093)\u3002"
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const repo = basename(toplevel.stdout.trim());
|
|
662
|
+
const email = (await run("git", ["config", "user.email"], { cwd })).stdout.trim();
|
|
663
|
+
const since = new Date(range.start.getTime() - FETCH_MARGIN_MS);
|
|
664
|
+
const result = await run(
|
|
665
|
+
"git",
|
|
666
|
+
[
|
|
667
|
+
"log",
|
|
668
|
+
`--since=${since.toISOString()}`,
|
|
669
|
+
...email === "" ? [] : [`--author=${email}`],
|
|
670
|
+
`--pretty=format:%aI${FIELD_SEP}%s${RECORD_SEP}`
|
|
671
|
+
],
|
|
672
|
+
{ cwd }
|
|
673
|
+
);
|
|
674
|
+
if (result.exitCode !== 0) {
|
|
675
|
+
throw new GitCollectError(
|
|
676
|
+
`git log \u306E\u5B9F\u884C\u306B\u5931\u6557\u3057\u307E\u3057\u305F:
|
|
677
|
+
${result.stderr.trim()}
|
|
678
|
+
\u30EA\u30DD\u30B8\u30C8\u30EA\u306E\u72B6\u614B\u3068 git \u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const commits = parseGitLog(result.stdout, repo).filter((commit) => {
|
|
682
|
+
const authoredAt = Date.parse(commit.date);
|
|
683
|
+
if (Number.isNaN(authoredAt)) {
|
|
684
|
+
throw new GitCollectError(
|
|
685
|
+
`\u30B3\u30DF\u30C3\u30C8\u306E\u8457\u8005\u65E5\u6642\u3092\u89E3\u91C8\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F: ${commit.date}(${commit.message})
|
|
686
|
+
git \u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u78BA\u8A8D\u306E\u3046\u3048\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
return authoredAt >= range.start.getTime() && authoredAt <= range.end.getTime();
|
|
690
|
+
});
|
|
691
|
+
return {
|
|
692
|
+
...emptyActivity(toDateOnly(range.start), toDateOnly(range.end)),
|
|
693
|
+
commits
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/collector/week-range.ts
|
|
698
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
699
|
+
var DAYS_PER_WEEK = 7;
|
|
700
|
+
function getWeekRange(now, weeksAgo = 1) {
|
|
701
|
+
if (!Number.isInteger(weeksAgo) || weeksAgo < 0) {
|
|
702
|
+
throw new Error(`weeksAgo \u306F 0 \u4EE5\u4E0A\u306E\u6574\u6570\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${String(weeksAgo)}`);
|
|
703
|
+
}
|
|
704
|
+
const dayOfWeek = now.getUTCDay();
|
|
705
|
+
const daysSinceMonday = (dayOfWeek + 6) % DAYS_PER_WEEK;
|
|
706
|
+
const startMs = Date.UTC(
|
|
707
|
+
now.getUTCFullYear(),
|
|
708
|
+
now.getUTCMonth(),
|
|
709
|
+
now.getUTCDate() - daysSinceMonday - weeksAgo * DAYS_PER_WEEK,
|
|
710
|
+
0,
|
|
711
|
+
0,
|
|
712
|
+
0,
|
|
713
|
+
0
|
|
714
|
+
);
|
|
715
|
+
return {
|
|
716
|
+
start: new Date(startMs),
|
|
717
|
+
end: new Date(startMs + DAYS_PER_WEEK * MS_PER_DAY - 1)
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/llm/errors.ts
|
|
722
|
+
var LLMError = class extends Error {
|
|
723
|
+
constructor(provider, message, remedy) {
|
|
724
|
+
super(message);
|
|
725
|
+
this.provider = provider;
|
|
726
|
+
this.remedy = remedy;
|
|
727
|
+
this.name = "LLMError";
|
|
728
|
+
}
|
|
729
|
+
provider;
|
|
730
|
+
remedy;
|
|
731
|
+
};
|
|
732
|
+
function formatLLMError(error) {
|
|
733
|
+
return `LLM \u30D0\u30C3\u30AF\u30A8\u30F3\u30C9(${error.provider})\u3067\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F:
|
|
734
|
+
${error.message}
|
|
735
|
+
|
|
736
|
+
\u5BFE\u51E6\u65B9\u6CD5:
|
|
737
|
+
${error.remedy}`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/prompt/build.ts
|
|
741
|
+
var TONE_INSTRUCTION = {
|
|
742
|
+
polite: "\u4E01\u5BE7\u306A\u30D3\u30B8\u30CD\u30B9\u656C\u8A9E(\u3067\u3059\u30FB\u307E\u3059\u8ABF\u3002\u6539\u307E\u3063\u305F\u8868\u73FE\u3092\u7528\u3044\u308B)",
|
|
743
|
+
casual: "\u67D4\u3089\u304B\u3044\u4E01\u5BE7\u8A9E(\u3067\u3059\u30FB\u307E\u3059\u8ABF\u3002\u5805\u3059\u304E\u306A\u3044\u89AA\u3057\u307F\u3084\u3059\u3044\u8868\u73FE)"
|
|
744
|
+
};
|
|
745
|
+
var BODY_MAX = 300;
|
|
746
|
+
function formatMonthDay(isoDate) {
|
|
747
|
+
const parts = isoDate.split("-");
|
|
748
|
+
return `${Number(parts[1] ?? "0")}/${Number(parts[2] ?? "0")}`;
|
|
749
|
+
}
|
|
750
|
+
function truncateBody(text2) {
|
|
751
|
+
const oneLine = text2.replaceAll("\r", "").split("\n").join(" ").trim();
|
|
752
|
+
return oneLine.length > BODY_MAX ? `${oneLine.slice(0, BODY_MAX)}\u2026` : oneLine;
|
|
753
|
+
}
|
|
754
|
+
function listOrNone(lines) {
|
|
755
|
+
return lines.length > 0 ? lines.join("\n") : "(\u306A\u3057)";
|
|
756
|
+
}
|
|
757
|
+
function prLine(pr) {
|
|
758
|
+
const body = truncateBody(pr.body);
|
|
759
|
+
return `- [${pr.repo}] #${String(pr.number)} ${pr.title}${body === "" ? "" : ` \u2014 ${body}`}`;
|
|
760
|
+
}
|
|
761
|
+
function issueLine(issue) {
|
|
762
|
+
const body = truncateBody(issue.body);
|
|
763
|
+
return `- [${issue.repo}] #${String(issue.number)} ${issue.title}${body === "" ? "" : ` \u2014 ${body}`}`;
|
|
764
|
+
}
|
|
765
|
+
function key(item) {
|
|
766
|
+
return `${item.repo}#${String(item.number)}`;
|
|
767
|
+
}
|
|
768
|
+
function buildReportPrompt(ctx) {
|
|
769
|
+
const { activity, client: client2 } = ctx;
|
|
770
|
+
const period = `${formatMonthDay(activity.periodStart)}\u301C${formatMonthDay(activity.periodEnd)}`;
|
|
771
|
+
const mergedKeys = new Set(activity.pullRequestsMerged.map(key));
|
|
772
|
+
const inProgressPrs = activity.pullRequestsCreated.filter((pr) => !mergedKeys.has(key(pr)));
|
|
773
|
+
const closedKeys = new Set(activity.issuesClosed.map(key));
|
|
774
|
+
const openIssues = activity.issuesOpened.filter((issue) => !closedKeys.has(key(issue)));
|
|
775
|
+
return `\u3042\u306A\u305F\u306F\u30D5\u30EA\u30FC\u30E9\u30F3\u30B9\u958B\u767A\u8005\u306E\u30A2\u30B7\u30B9\u30BF\u30F3\u30C8\u3067\u3059\u3002\u4EE5\u4E0B\u306E\u6D3B\u52D5\u8A18\u9332\u3092\u3082\u3068\u306B\u3001\u975E\u30A8\u30F3\u30B8\u30CB\u30A2\u306E\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306B\u9001\u308B\u9031\u6B21\u5831\u544A\u306E\u4E0B\u66F8\u304D\u3092 Markdown \u3067\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
776
|
+
|
|
777
|
+
## \u51FA\u529B\u8981\u4EF6
|
|
778
|
+
- \u51FA\u529B\u306F Markdown \u672C\u6587\u306E\u307F\u3002\u524D\u7F6E\u304D\u30FB\u5F8C\u66F8\u304D\u30FB\u30B3\u30FC\u30C9\u30D5\u30A7\u30F3\u30B9\u306F\u4ED8\u3051\u306A\u3044\u3002
|
|
779
|
+
- \u6587\u4F53: ${TONE_INSTRUCTION[client2.tone]}
|
|
780
|
+
- \u6280\u8853\u7528\u8A9E(refactor\u3001LGTM\u3001\u30DE\u30FC\u30B8\u3001\u30B3\u30DF\u30C3\u30C8\u3001PR \u7B49)\u3092\u305D\u306E\u307E\u307E\u4F7F\u308F\u305A\u3001\u975E\u30A8\u30F3\u30B8\u30CB\u30A2\u306B\u4F1D\u308F\u308B\u65E5\u672C\u8A9E\u306B\u8A00\u3044\u63DB\u3048\u308B\u3002
|
|
781
|
+
- \u4F8B: \u300Crefactor\u3057\u3066LGTM\u3092\u3082\u3089\u3063\u305F\u300D\u2192\u300C\u5185\u90E8\u54C1\u8CEA\u306E\u6539\u5584\u3092\u884C\u3044\u3001\u30EC\u30D3\u30E5\u30FC\u3092\u5B8C\u4E86\u3057\u307E\u3057\u305F\u300D
|
|
782
|
+
- \u300C\u4ECA\u9031\u306E\u6210\u679C\u300D\u306F\u5B8C\u4E86\u3057\u305F\u5185\u5BB9\u3092 3\u301C5 \u9805\u76EE\u306B\u307E\u3068\u3081\u308B\u3002\u9805\u76EE\u304C\u5C11\u306A\u3044\u9031\u306F\u3042\u308B\u5206\u3060\u3051\u3067\u3088\u3044\u3002
|
|
783
|
+
- \u300C\u6765\u9031\u306E\u4E88\u5B9A\u300D\u306F\u9032\u884C\u4E2D\u306E\u4F5C\u696D\u304B\u3089\u63A8\u5B9A\u3057\u3001\u5404\u9805\u76EE\u306E\u672B\u5C3E\u306B\u300C(\u8981\u7DE8\u96C6)\u300D\u3068\u4ED8\u3051\u308B\u3002
|
|
784
|
+
- \u300C\u9023\u7D61\u30FB\u76F8\u8AC7\u4E8B\u9805\u300D\u306E\u7BC0\u306F\u898B\u51FA\u3057\u306E\u307F\u3068\u3057\u3001\u672C\u6587\u306F\u66F8\u304B\u306A\u3044(\u5229\u7528\u8005\u304C\u624B\u3067\u8A18\u5165\u3059\u308B)\u3002
|
|
785
|
+
- \u51FA\u529B\u306F\u300C# \u9031\u6B21\u5831\u544A\u300D\u306E\u898B\u51FA\u3057\u304B\u3089\u300C## \u9023\u7D61\u30FB\u76F8\u8AC7\u4E8B\u9805\u300D\u306E\u898B\u51FA\u3057\u307E\u3067\u3068\u3059\u308B\u3002\u5F8C\u8FF0\u306E\u300C\u6D3B\u52D5\u8A18\u9332\u300D\u306F\u751F\u6210\u306E\u305F\u3081\u306E\u5165\u529B\u30C7\u30FC\u30BF\u3067\u3042\u308A\u3001\u51FA\u529B\u306B\u8907\u88FD\u3057\u306A\u3044\u3053\u3068\u3002
|
|
786
|
+
- \u69CB\u6210\u306F\u6B21\u306E\u3068\u304A\u308A\u306B\u3059\u308B:
|
|
787
|
+
|
|
788
|
+
# \u9031\u6B21\u5831\u544A(${period})${client2.addressee}
|
|
789
|
+
|
|
790
|
+
## \u4ECA\u9031\u306E\u6210\u679C
|
|
791
|
+
|
|
792
|
+
## \u9032\u884C\u4E2D\u306E\u4F5C\u696D
|
|
793
|
+
|
|
794
|
+
## \u6765\u9031\u306E\u4E88\u5B9A
|
|
795
|
+
|
|
796
|
+
## \u9023\u7D61\u30FB\u76F8\u8AC7\u4E8B\u9805
|
|
797
|
+
|
|
798
|
+
--- \u3053\u3053\u304B\u3089\u4E0B\u306F\u751F\u6210\u306E\u305F\u3081\u306E\u5165\u529B\u30C7\u30FC\u30BF(\u51FA\u529B\u306B\u542B\u3081\u306A\u3044) ---
|
|
799
|
+
|
|
800
|
+
## \u6D3B\u52D5\u8A18\u9332(\u4ECA\u9031\u306E\u5B9F\u30C7\u30FC\u30BF)
|
|
801
|
+
|
|
802
|
+
### \u30B3\u30DF\u30C3\u30C8
|
|
803
|
+
${listOrNone(activity.commits.map((c) => `- [${c.repo}] ${c.message}`))}
|
|
804
|
+
|
|
805
|
+
### \u30DE\u30FC\u30B8\u3055\u308C\u305F Pull Request(\u5B8C\u4E86\u3057\u305F\u4F5C\u696D)
|
|
806
|
+
${listOrNone(activity.pullRequestsMerged.map(prLine))}
|
|
807
|
+
|
|
808
|
+
### \u30AF\u30ED\u30FC\u30BA\u3057\u305F Issue(\u89E3\u6C7A\u3057\u305F\u8AB2\u984C)
|
|
809
|
+
${listOrNone(activity.issuesClosed.map(issueLine))}
|
|
810
|
+
|
|
811
|
+
### \u9032\u884C\u4E2D\u306E Pull Request(\u672A\u30DE\u30FC\u30B8)
|
|
812
|
+
${listOrNone(inProgressPrs.map(prLine))}
|
|
813
|
+
|
|
814
|
+
### \u9032\u884C\u4E2D\u306E Issue(\u672A\u89E3\u6C7A)
|
|
815
|
+
${listOrNone(openIssues.map(issueLine))}
|
|
816
|
+
|
|
817
|
+
### \u5B9F\u65BD\u3057\u305F\u30EC\u30D3\u30E5\u30FC
|
|
818
|
+
${listOrNone(activity.pullRequestsReviewed.map(prLine))}
|
|
819
|
+
`;
|
|
820
|
+
}
|
|
821
|
+
function stripActivityAppendix(markdown) {
|
|
822
|
+
const match = /^## 活動記録/m.exec(markdown);
|
|
823
|
+
if (match === null) return markdown;
|
|
824
|
+
return `${markdown.slice(0, match.index).trimEnd()}
|
|
825
|
+
`;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/generate/orchestrate.ts
|
|
829
|
+
async function orchestrateGenerate(options, deps) {
|
|
830
|
+
const config = await deps.loadConfig();
|
|
831
|
+
if (config === null) {
|
|
832
|
+
throw new Error("\u8A2D\u5B9A\u304C\u3042\u308A\u307E\u305B\u3093\u3002\u307E\u305A `shuho init` \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
833
|
+
}
|
|
834
|
+
const weeksAgo = Number(options.week);
|
|
835
|
+
if (!Number.isInteger(weeksAgo) || weeksAgo < 0) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`--week \u306F 0 \u4EE5\u4E0A\u306E\u6574\u6570\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044(\u4F8B: --week 2 \u3067 2 \u9031\u524D): ${options.week}`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
const client2 = findClient(config, options.client);
|
|
841
|
+
const range = getWeekRange(deps.now(), weeksAgo);
|
|
842
|
+
const { plan, warning } = await deps.resolvePlan(config.license?.key);
|
|
843
|
+
if (warning !== void 0) deps.warn(warning);
|
|
844
|
+
let activity;
|
|
845
|
+
if (plan === "pro") {
|
|
846
|
+
const token = await deps.resolveToken(config);
|
|
847
|
+
if (token === null) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
"GitHub \u30C8\u30FC\u30AF\u30F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002`gh auth login` \u3092\u5B9F\u884C\u3059\u308B\u304B\u3001`shuho init` \u3067 PAT \u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
activity = await deps.collectGithub(token, client2.repos, range);
|
|
853
|
+
} else {
|
|
854
|
+
deps.log(
|
|
855
|
+
"Free \u30D7\u30E9\u30F3: \u30AB\u30EC\u30F3\u30C8\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E git log \u304B\u3089\u30B3\u30DF\u30C3\u30C8\u306E\u307F\u53CE\u96C6\u3057\u307E\u3059(PR/Issue \u306E\u53CE\u96C6\u306F Pro \u30D7\u30E9\u30F3)\u3002"
|
|
856
|
+
);
|
|
857
|
+
activity = await deps.collectLocalGit(range);
|
|
858
|
+
}
|
|
859
|
+
const prompt = buildReportPrompt({ activity, client: client2 });
|
|
860
|
+
const llmClient = deps.createLLMClient(config.llm);
|
|
861
|
+
deps.progress.start(`${config.llm.provider} \u3067\u9031\u5831\u3092\u751F\u6210\u4E2D...`);
|
|
862
|
+
let markdown;
|
|
863
|
+
try {
|
|
864
|
+
markdown = await llmClient.generateReport(prompt);
|
|
865
|
+
} catch (error) {
|
|
866
|
+
deps.progress.stop("\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
|
|
867
|
+
deps.error(error instanceof LLMError ? formatLLMError(error) : getErrorMessage(error));
|
|
868
|
+
deps.setExitCode(1);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
deps.progress.stop("\u751F\u6210\u304C\u5B8C\u4E86\u3057\u307E\u3057\u305F");
|
|
872
|
+
markdown = stripActivityAppendix(markdown);
|
|
873
|
+
const path = await deps.saveReport(markdown, client2.name, activity.periodStart);
|
|
874
|
+
deps.log(`
|
|
875
|
+
${markdown}
|
|
876
|
+
`);
|
|
877
|
+
deps.log(`\u4FDD\u5B58\u3057\u307E\u3057\u305F: ${path}(\u5185\u5BB9\u3092\u78BA\u8A8D\u30FB\u7DE8\u96C6\u306E\u3046\u3048\u9001\u4ED8\u3057\u3066\u304F\u3060\u3055\u3044)`);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/llm/factory.ts
|
|
881
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
882
|
+
import OpenAI from "openai";
|
|
883
|
+
|
|
884
|
+
// src/llm/api-backend.ts
|
|
885
|
+
var DEFAULT_ANTHROPIC_MODEL = "claude-opus-4-8";
|
|
886
|
+
var DEFAULT_OPENAI_MODEL = "gpt-5.5";
|
|
887
|
+
var MAX_TOKENS = 4096;
|
|
888
|
+
function toLLMError(provider, error, remedy) {
|
|
889
|
+
if (error instanceof LLMError) return error;
|
|
890
|
+
return new LLMError(provider, getErrorMessage(error), remedy);
|
|
891
|
+
}
|
|
892
|
+
function createAnthropicClient(sdk, model) {
|
|
893
|
+
return {
|
|
894
|
+
async generateReport(prompt) {
|
|
895
|
+
try {
|
|
896
|
+
const response = await sdk.messages.create({
|
|
897
|
+
model,
|
|
898
|
+
max_tokens: MAX_TOKENS,
|
|
899
|
+
messages: [{ role: "user", content: prompt }]
|
|
900
|
+
});
|
|
901
|
+
const text2 = response.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join("");
|
|
902
|
+
if (text2 === "") {
|
|
903
|
+
throw new LLMError(
|
|
904
|
+
"anthropic",
|
|
905
|
+
"\u5FDC\u7B54\u306B\u30C6\u30AD\u30B9\u30C8\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
906
|
+
"config.toml \u306E llm.model \u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
return text2;
|
|
910
|
+
} catch (error) {
|
|
911
|
+
throw toLLMError(
|
|
912
|
+
"anthropic",
|
|
913
|
+
error,
|
|
914
|
+
"ANTHROPIC_API_KEY \u3068 llm.model \u306E\u8A2D\u5B9A\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function createOpenAiClient(sdk, model) {
|
|
921
|
+
return {
|
|
922
|
+
async generateReport(prompt) {
|
|
923
|
+
try {
|
|
924
|
+
const response = await sdk.chat.completions.create({
|
|
925
|
+
model,
|
|
926
|
+
messages: [{ role: "user", content: prompt }]
|
|
927
|
+
});
|
|
928
|
+
const text2 = response.choices[0]?.message.content;
|
|
929
|
+
if (text2 === null || text2 === void 0 || text2 === "") {
|
|
930
|
+
throw new LLMError(
|
|
931
|
+
"openai",
|
|
932
|
+
"\u5FDC\u7B54\u304C\u7A7A\u3067\u3057\u305F\u3002",
|
|
933
|
+
"config.toml \u306E llm.model \u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
return text2;
|
|
937
|
+
} catch (error) {
|
|
938
|
+
throw toLLMError("openai", error, "OPENAI_API_KEY \u3068 llm.model \u306E\u8A2D\u5B9A\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/llm/cli-backend.ts
|
|
945
|
+
import { tmpdir } from "os";
|
|
946
|
+
var CLAUDE_ARGS = ["-p", "--output-format", "text"];
|
|
947
|
+
var CODEX_ARGS = ["exec", "-"];
|
|
948
|
+
var SWITCH_REMEDY = "`shuho init` \u3092\u518D\u5B9F\u884C\u3059\u308B\u3068\u5225\u306E LLM \u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u306B\u5207\u308A\u66FF\u3048\u3089\u308C\u307E\u3059(\u4F8B: ANTHROPIC_API_KEY \u3092\u8A2D\u5B9A\u3057\u3066 anthropic \u3092\u9078\u629E)\u3002";
|
|
949
|
+
function createCliClient(provider, command, args, run, installGuide) {
|
|
950
|
+
return {
|
|
951
|
+
async generateReport(prompt) {
|
|
952
|
+
let result;
|
|
953
|
+
try {
|
|
954
|
+
result = await run(command, args, { stdin: prompt, cwd: tmpdir() });
|
|
955
|
+
} catch (error) {
|
|
956
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
957
|
+
throw new LLMError(
|
|
958
|
+
provider,
|
|
959
|
+
`${command} \u30B3\u30DE\u30F3\u30C9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002`,
|
|
960
|
+
`${installGuide}
|
|
961
|
+
\u307E\u305F\u306F ${SWITCH_REMEDY}`
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
throw new LLMError(provider, getErrorMessage(error), SWITCH_REMEDY);
|
|
965
|
+
}
|
|
966
|
+
if (result.exitCode !== 0) {
|
|
967
|
+
throw new LLMError(
|
|
968
|
+
provider,
|
|
969
|
+
`${command} \u304C\u7D42\u4E86\u30B3\u30FC\u30C9 ${String(result.exitCode)} \u3067\u5931\u6557\u3057\u307E\u3057\u305F:
|
|
970
|
+
${result.stderr.trim()}`,
|
|
971
|
+
`${command} \u306B\u30ED\u30B0\u30A4\u30F3\u6E08\u307F\u304B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
972
|
+
\u307E\u305F\u306F ${SWITCH_REMEDY}`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
const text2 = result.stdout.trim();
|
|
976
|
+
if (text2 === "") {
|
|
977
|
+
throw new LLMError(provider, `${command} \u306E\u51FA\u529B\u304C\u7A7A\u3067\u3057\u305F\u3002`, SWITCH_REMEDY);
|
|
978
|
+
}
|
|
979
|
+
return text2;
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function createClaudeCliClient(run) {
|
|
984
|
+
return createCliClient(
|
|
985
|
+
"claude-cli",
|
|
986
|
+
"claude",
|
|
987
|
+
CLAUDE_ARGS,
|
|
988
|
+
run,
|
|
989
|
+
"\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB: npm install -g @anthropic-ai/claude-code(https://docs.anthropic.com/claude-code)"
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
function createCodexCliClient(run) {
|
|
993
|
+
return createCliClient(
|
|
994
|
+
"codex-cli",
|
|
995
|
+
"codex",
|
|
996
|
+
CODEX_ARGS,
|
|
997
|
+
run,
|
|
998
|
+
"\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB: npm install -g @openai/codex"
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/llm/factory.ts
|
|
1003
|
+
function createLLMClient(llm, env, run) {
|
|
1004
|
+
switch (llm.provider) {
|
|
1005
|
+
case "claude-cli":
|
|
1006
|
+
return createClaudeCliClient(run);
|
|
1007
|
+
case "codex-cli":
|
|
1008
|
+
return createCodexCliClient(run);
|
|
1009
|
+
case "anthropic": {
|
|
1010
|
+
const apiKey = env.ANTHROPIC_API_KEY;
|
|
1011
|
+
if (apiKey === void 0 || apiKey === "") {
|
|
1012
|
+
throw new LLMError(
|
|
1013
|
+
"anthropic",
|
|
1014
|
+
"ANTHROPIC_API_KEY \u304C\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1015
|
+
"\u74B0\u5883\u5909\u6570 ANTHROPIC_API_KEY \u3092\u8A2D\u5B9A\u3059\u308B\u304B\u3001`shuho init` \u3067\u5225\u306E\u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
const sdk = new Anthropic({ apiKey });
|
|
1019
|
+
return createAnthropicClient(
|
|
1020
|
+
{ messages: { create: (params) => sdk.messages.create(params) } },
|
|
1021
|
+
llm.model ?? DEFAULT_ANTHROPIC_MODEL
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
case "openai": {
|
|
1025
|
+
const apiKey = env.OPENAI_API_KEY;
|
|
1026
|
+
if (apiKey === void 0 || apiKey === "") {
|
|
1027
|
+
throw new LLMError(
|
|
1028
|
+
"openai",
|
|
1029
|
+
"OPENAI_API_KEY \u304C\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1030
|
+
"\u74B0\u5883\u5909\u6570 OPENAI_API_KEY \u3092\u8A2D\u5B9A\u3059\u308B\u304B\u3001`shuho init` \u3067\u5225\u306E\u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
const sdk = new OpenAI({ apiKey });
|
|
1034
|
+
return createOpenAiClient(
|
|
1035
|
+
{ chat: { completions: { create: (params) => sdk.chat.completions.create(params) } } },
|
|
1036
|
+
llm.model ?? DEFAULT_OPENAI_MODEL
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/output/report-file.ts
|
|
1043
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
1044
|
+
import { resolve } from "path";
|
|
1045
|
+
function sanitizeForFileName(name) {
|
|
1046
|
+
return name.replace(/[\\/:*?"<>|\s]+/g, "-");
|
|
1047
|
+
}
|
|
1048
|
+
function reportFileName(clientName, periodStart) {
|
|
1049
|
+
return `shuho-${sanitizeForFileName(clientName)}-${periodStart}.md`;
|
|
1050
|
+
}
|
|
1051
|
+
async function saveReport(markdown, dir, clientName, periodStart) {
|
|
1052
|
+
const path = resolve(dir, reportFileName(clientName, periodStart));
|
|
1053
|
+
const content = markdown.endsWith("\n") ? markdown : `${markdown}
|
|
1054
|
+
`;
|
|
1055
|
+
await writeFile3(path, content, "utf8");
|
|
1056
|
+
return path;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/cli/commands/generate.ts
|
|
1060
|
+
function toOctokitLike(octokit) {
|
|
1061
|
+
return {
|
|
1062
|
+
rest: {
|
|
1063
|
+
users: {
|
|
1064
|
+
getAuthenticated: () => octokit.rest.users.getAuthenticated()
|
|
1065
|
+
},
|
|
1066
|
+
search: {
|
|
1067
|
+
issuesAndPullRequests: async (params) => {
|
|
1068
|
+
const response = await octokit.rest.search.issuesAndPullRequests(params);
|
|
1069
|
+
return {
|
|
1070
|
+
data: {
|
|
1071
|
+
items: response.data.items.map((item) => ({
|
|
1072
|
+
number: item.number,
|
|
1073
|
+
title: item.title,
|
|
1074
|
+
body: item.body ?? null,
|
|
1075
|
+
repository_url: item.repository_url
|
|
1076
|
+
}))
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
repos: {
|
|
1082
|
+
listCommits: async (params) => {
|
|
1083
|
+
const response = await octokit.rest.repos.listCommits(params);
|
|
1084
|
+
return {
|
|
1085
|
+
data: response.data.map((item) => ({
|
|
1086
|
+
sha: item.sha,
|
|
1087
|
+
commit: {
|
|
1088
|
+
message: item.commit.message,
|
|
1089
|
+
author: item.commit.author?.date === void 0 ? null : { date: item.commit.author.date }
|
|
1090
|
+
}
|
|
1091
|
+
}))
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function createProgress() {
|
|
1099
|
+
if (canRenderSpinner()) {
|
|
1100
|
+
const spinner2 = p2.spinner();
|
|
1101
|
+
return {
|
|
1102
|
+
start: (message) => {
|
|
1103
|
+
spinner2.start(message);
|
|
1104
|
+
},
|
|
1105
|
+
stop: (message) => {
|
|
1106
|
+
spinner2.stop(message);
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
start: (message) => {
|
|
1112
|
+
console.log(message);
|
|
1113
|
+
},
|
|
1114
|
+
stop: (message) => {
|
|
1115
|
+
console.log(message);
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
async function runGenerate(options) {
|
|
1120
|
+
const dir = getConfigDir(process.env);
|
|
1121
|
+
await orchestrateGenerate(options, {
|
|
1122
|
+
loadConfig: () => loadConfig(dir),
|
|
1123
|
+
resolvePlan: (key2) => {
|
|
1124
|
+
const bypass = devProBypass(process.env);
|
|
1125
|
+
if (bypass !== null) return Promise.resolve(bypass);
|
|
1126
|
+
return resolvePlan({
|
|
1127
|
+
key: key2,
|
|
1128
|
+
configDir: dir,
|
|
1129
|
+
validator: createPolarValidator(fetch, SHUHO_POLAR_ORGANIZATION_ID),
|
|
1130
|
+
now: /* @__PURE__ */ new Date()
|
|
1131
|
+
});
|
|
1132
|
+
},
|
|
1133
|
+
resolveToken: (config) => resolveGithubToken(config, runCommand),
|
|
1134
|
+
collectGithub: (token, repos, range) => collectGithub(createOctokitPort(toOctokitLike(new Octokit({ auth: token }))), repos, range),
|
|
1135
|
+
collectLocalGit: (range) => collectLocalGit(runCommand, range, process.cwd()),
|
|
1136
|
+
createLLMClient: (llm) => createLLMClient(llm, process.env, runCommand),
|
|
1137
|
+
saveReport: (markdown, clientName, periodStart) => saveReport(markdown, process.cwd(), clientName, periodStart),
|
|
1138
|
+
progress: createProgress(),
|
|
1139
|
+
log: (message) => {
|
|
1140
|
+
console.log(message);
|
|
1141
|
+
},
|
|
1142
|
+
warn: (message) => {
|
|
1143
|
+
console.warn(message);
|
|
1144
|
+
},
|
|
1145
|
+
error: (message) => {
|
|
1146
|
+
console.error(message);
|
|
1147
|
+
},
|
|
1148
|
+
setExitCode: (code) => {
|
|
1149
|
+
process.exitCode = code;
|
|
1150
|
+
},
|
|
1151
|
+
now: () => /* @__PURE__ */ new Date()
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/cli/commands/init.ts
|
|
1156
|
+
import { join as join3 } from "path";
|
|
1157
|
+
import * as p3 from "@clack/prompts";
|
|
1158
|
+
var FALLBACK_LABELS = {
|
|
1159
|
+
"claude-cli": "Claude Code CLI(\u672A\u691C\u51FA\u3002\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u5F8C\u306B\u5229\u7528\u53EF\u80FD)",
|
|
1160
|
+
"codex-cli": "Codex CLI(\u672A\u691C\u51FA\u3002\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u5F8C\u306B\u5229\u7528\u53EF\u80FD)",
|
|
1161
|
+
anthropic: "Anthropic API(ANTHROPIC_API_KEY \u304C\u5FC5\u8981)",
|
|
1162
|
+
openai: "OpenAI API(OPENAI_API_KEY \u304C\u5FC5\u8981)"
|
|
1163
|
+
};
|
|
1164
|
+
async function runInit() {
|
|
1165
|
+
if (!isInteractive()) {
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
"shuho init \u306F\u5BFE\u8A71\u5F0F\u30B3\u30DE\u30F3\u30C9\u3067\u3059\u3002\u30BF\u30FC\u30DF\u30CA\u30EB\u304B\u3089\u76F4\u63A5\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044(\u30D1\u30A4\u30D7\u30FB\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u7D4C\u7531\u3067\u306F\u5B9F\u884C\u3067\u304D\u307E\u305B\u3093)\u3002"
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
p3.intro("shuho \u521D\u671F\u8A2D\u5B9A");
|
|
1171
|
+
const dir = getConfigDir(process.env);
|
|
1172
|
+
const existing = await loadConfig(dir);
|
|
1173
|
+
let patToken;
|
|
1174
|
+
const ghResult = await runCommand("gh", ["auth", "token"]).catch(() => null);
|
|
1175
|
+
if (ghResult !== null && ghResult.exitCode === 0 && ghResult.stdout.trim() !== "") {
|
|
1176
|
+
p3.log.success(
|
|
1177
|
+
"GitHub: gh CLI \u306E\u30C8\u30FC\u30AF\u30F3\u3092\u691C\u51FA\u3057\u307E\u3057\u305F(\u5B9F\u884C\u6642\u306B\u81EA\u52D5\u5229\u7528\u3057\u307E\u3059\u3002\u30D5\u30A1\u30A4\u30EB\u306B\u306F\u4FDD\u5B58\u3057\u307E\u305B\u3093)"
|
|
1178
|
+
);
|
|
1179
|
+
patToken = void 0;
|
|
1180
|
+
} else {
|
|
1181
|
+
p3.log.info("gh CLI \u306E\u30C8\u30FC\u30AF\u30F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002");
|
|
1182
|
+
const pat = await p3.password({
|
|
1183
|
+
message: "GitHub fine-grained PAT \u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044(read-only \u63A8\u5968)\u3002\u30ED\u30FC\u30AB\u30EB git \u306E\u307F\u4F7F\u3046\u5834\u5408\u306F\u7A7A\u306E\u307E\u307E Enter:"
|
|
1184
|
+
});
|
|
1185
|
+
if (p3.isCancel(pat)) {
|
|
1186
|
+
p3.cancel("\u4E2D\u65AD\u3057\u307E\u3057\u305F");
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
patToken = pat === "" ? void 0 : pat;
|
|
1190
|
+
}
|
|
1191
|
+
const detected = await detectBackends(runCommand, process.env);
|
|
1192
|
+
if (detected.length === 0) {
|
|
1193
|
+
p3.log.warn(
|
|
1194
|
+
"\u5229\u7528\u53EF\u80FD\u306A LLM \u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u304C\u691C\u51FA\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002claude / codex \u306E\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3001\u307E\u305F\u306F API \u30AD\u30FC\u306E\u8A2D\u5B9A\u3092\u63A8\u5968\u3057\u307E\u3059\u3002"
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const options = detected.length > 0 ? detected.map((b) => ({ value: b.provider, label: b.label })) : LLM_PROVIDERS.map((provider2) => ({ value: provider2, label: FALLBACK_LABELS[provider2] }));
|
|
1198
|
+
const provider = await p3.select({ message: "LLM \u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044", options });
|
|
1199
|
+
if (p3.isCancel(provider)) {
|
|
1200
|
+
p3.cancel("\u4E2D\u65AD\u3057\u307E\u3057\u305F");
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const keepModel = existing?.llm.provider === provider ? existing.llm.model : void 0;
|
|
1204
|
+
const config = {
|
|
1205
|
+
...patToken !== void 0 ? { github: { token: patToken } } : {},
|
|
1206
|
+
llm: { provider, ...keepModel !== void 0 ? { model: keepModel } : {} },
|
|
1207
|
+
clients: existing?.clients ?? [],
|
|
1208
|
+
...existing?.license !== void 0 ? { license: existing.license } : {}
|
|
1209
|
+
};
|
|
1210
|
+
await saveConfig(dir, config);
|
|
1211
|
+
p3.outro(`\u8A2D\u5B9A\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F: ${join3(dir, CONFIG_FILE_NAME)}`);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// src/cli/index.ts
|
|
1215
|
+
var program = new Command();
|
|
1216
|
+
program.name("shuho").description("GitHub\u306E\u6D3B\u52D5\u5C65\u6B74\u304B\u3089\u975E\u30A8\u30F3\u30B8\u30CB\u30A2\u306E\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u5411\u3051\u9031\u5831\u306E\u4E0B\u66F8\u304D\u3092\u751F\u6210\u3059\u308BCLI").version("0.1.0");
|
|
1217
|
+
program.command("init").description("\u521D\u671F\u8A2D\u5B9A(GitHub\u8A8D\u8A3C\u30FBLLM\u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u306E\u691C\u51FA\u3068\u8A2D\u5B9A)").action(async () => {
|
|
1218
|
+
await runInit();
|
|
1219
|
+
});
|
|
1220
|
+
var client = program.command("client").description("\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8(\u6848\u4EF6)\u306E\u7BA1\u7406");
|
|
1221
|
+
client.command("add <name>").description("\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u3092\u767B\u9332\u3059\u308B").action(async (name) => {
|
|
1222
|
+
await runClientAdd(name);
|
|
1223
|
+
});
|
|
1224
|
+
client.command("list").description("\u767B\u9332\u6E08\u307F\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u3092\u4E00\u89A7\u8868\u793A\u3059\u308B").action(async () => {
|
|
1225
|
+
await runClientList();
|
|
1226
|
+
});
|
|
1227
|
+
client.command("remove <name>").description("\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u3092\u524A\u9664\u3059\u308B").action(async (name) => {
|
|
1228
|
+
await runClientRemove(name);
|
|
1229
|
+
});
|
|
1230
|
+
program.command("generate").description("\u9031\u5831\u306E\u4E0B\u66F8\u304D\u3092\u751F\u6210\u3059\u308B").option("--client <name>", "\u5BFE\u8C61\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8").option("--week <n>", "\u4F55\u9031\u524D\u3092\u5BFE\u8C61\u306B\u3059\u308B\u304B(\u65E2\u5B9A: 1=\u5148\u9031)", "1").action(async (options) => {
|
|
1231
|
+
await runGenerate(options);
|
|
1232
|
+
});
|
|
1233
|
+
program.command("license <key>").description("Pro \u30D7\u30E9\u30F3\u306E\u30E9\u30A4\u30BB\u30F3\u30B9\u30AD\u30FC\u3092\u8A2D\u5B9A\u3057\u3066\u691C\u8A3C\u3059\u308B").action(async (key2) => {
|
|
1234
|
+
await runLicense(key2);
|
|
1235
|
+
});
|
|
1236
|
+
program.command("doctor").description("GitHub\u8A8D\u8A3C\u30FBLLM\u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u30FB\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u306E\u758E\u901A\u78BA\u8A8D").action(async () => {
|
|
1237
|
+
await runDoctor();
|
|
1238
|
+
});
|
|
1239
|
+
program.addHelpText(
|
|
1240
|
+
"after",
|
|
1241
|
+
`
|
|
1242
|
+
\u30D7\u30E9\u30A4\u30D0\u30B7\u30FC:
|
|
1243
|
+
LLM \u3078\u9001\u4FE1\u3059\u308B\u306E\u306F\u30B3\u30DF\u30C3\u30C8\u30E1\u30C3\u30BB\u30FC\u30B8\u3001PR/Issue \u306E\u30BF\u30A4\u30C8\u30EB\u3068\u672C\u6587\u306E\u307F\u3067\u3059\u3002
|
|
1244
|
+
diff \u304A\u3088\u3073\u30B3\u30FC\u30C9\u672C\u6587\u306F\u4E00\u5207\u9001\u4FE1\u3057\u307E\u305B\u3093\u3002
|
|
1245
|
+
API \u30E2\u30FC\u30C9(anthropic/openai)\u306F BYOK \u306E\u305F\u3081\u3001\u30C7\u30FC\u30BF\u306E\u884C\u304D\u5148\u306F\u3054\u81EA\u8EAB\u306E\u30A2\u30AB\u30A6\u30F3\u30C8\u306E\u307F\u3067\u3059\u3002
|
|
1246
|
+
CLI \u30E2\u30FC\u30C9(claude-cli/codex-cli)\u306F\u3054\u81EA\u8EAB\u306E\u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\u7D4C\u7531\u3067\u3059\u3002`
|
|
1247
|
+
);
|
|
1248
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
1249
|
+
console.error(error instanceof LLMError ? formatLLMError(error) : getErrorMessage(error));
|
|
1250
|
+
process.exitCode = 1;
|
|
1251
|
+
});
|
|
1252
|
+
//# sourceMappingURL=index.js.map
|