@rhseung/ps-cli 1.10.2 → 1.11.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.
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getUserProblemStats,
4
+ getUserStats,
5
+ getUserTagRatings,
6
+ getUserTop100,
7
+ scrapeUserStats
8
+ } from "./chunk-S7IL7OXF.js";
9
+ import {
10
+ findProjectRoot,
11
+ getArchiveDir,
12
+ getSolvingDir
13
+ } from "./chunk-AHE4QHJD.js";
14
+
15
+ // src/hooks/use-user-stats.ts
16
+ import { existsSync } from "fs";
17
+ import { readdir, stat } from "fs/promises";
18
+ import { join } from "path";
19
+ import { useEffect, useState } from "react";
20
+ async function countProblems(dir) {
21
+ let count = 0;
22
+ try {
23
+ if (!existsSync(dir)) return 0;
24
+ const entries = await readdir(dir);
25
+ for (const entry of entries) {
26
+ if (entry.startsWith(".")) continue;
27
+ const fullPath = join(dir, entry);
28
+ const s = await stat(fullPath);
29
+ if (s.isDirectory()) {
30
+ if (existsSync(join(fullPath, "meta.json"))) {
31
+ count++;
32
+ } else {
33
+ count += await countProblems(fullPath);
34
+ }
35
+ }
36
+ }
37
+ } catch {
38
+ }
39
+ return count;
40
+ }
41
+ function useUserStats({
42
+ handle,
43
+ onComplete,
44
+ fetchLocalCount = false
45
+ }) {
46
+ const [status, setStatus] = useState(
47
+ "loading"
48
+ );
49
+ const [user, setUser] = useState(null);
50
+ const [top100, setTop100] = useState(null);
51
+ const [problemStats, setProblemStats] = useState(null);
52
+ const [tagRatings, setTagRatings] = useState(
53
+ null
54
+ );
55
+ const [bojStats, setBojStats] = useState(null);
56
+ const [localSolvedCount, setLocalSolvedCount] = useState(null);
57
+ const [error, setError] = useState(null);
58
+ useEffect(() => {
59
+ async function fetchData() {
60
+ try {
61
+ const userData = await getUserStats(handle).catch((err) => {
62
+ if (err instanceof Error && err.message.includes("404")) {
63
+ throw new Error(`\uC0AC\uC6A9\uC790 '${handle}'\uC744(\uB97C) \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
64
+ }
65
+ throw err;
66
+ });
67
+ if (!userData) {
68
+ throw new Error(`\uC0AC\uC6A9\uC790 '${handle}'\uC744(\uB97C) \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
69
+ }
70
+ setUser(userData);
71
+ const [top100Data, problemStatsData, tagRatingsData, bojStatsData] = await Promise.all([
72
+ getUserTop100(handle).catch((err) => {
73
+ console.error("Error fetching top 100:", err);
74
+ return null;
75
+ }),
76
+ getUserProblemStats(handle).catch((err) => {
77
+ console.error("Error fetching problem stats:", err);
78
+ return null;
79
+ }),
80
+ getUserTagRatings(handle).catch((err) => {
81
+ console.error("Error fetching tag ratings:", err);
82
+ return null;
83
+ }),
84
+ scrapeUserStats(handle).catch((err) => {
85
+ console.error("Error scraping BOJ stats:", err);
86
+ return null;
87
+ })
88
+ ]);
89
+ setTop100(top100Data);
90
+ setProblemStats(problemStatsData);
91
+ setTagRatings(tagRatingsData);
92
+ setBojStats(bojStatsData);
93
+ if (fetchLocalCount) {
94
+ const projectRoot = findProjectRoot();
95
+ if (projectRoot) {
96
+ const archiveDir = getArchiveDir();
97
+ const solvingDir = getSolvingDir();
98
+ const archivePath = join(projectRoot, archiveDir);
99
+ const solvingPath = join(projectRoot, solvingDir);
100
+ const [archiveCount, solvingCount] = await Promise.all([
101
+ countProblems(archivePath),
102
+ countProblems(solvingPath)
103
+ ]);
104
+ setLocalSolvedCount(archiveCount + solvingCount);
105
+ }
106
+ }
107
+ setStatus("success");
108
+ setTimeout(() => {
109
+ onComplete();
110
+ }, 5e3);
111
+ } catch (err) {
112
+ setError(err instanceof Error ? err.message : String(err));
113
+ setStatus("error");
114
+ setTimeout(() => {
115
+ onComplete();
116
+ }, 3e3);
117
+ }
118
+ }
119
+ void fetchData();
120
+ }, [fetchLocalCount, handle, onComplete]);
121
+ return {
122
+ status,
123
+ user,
124
+ top100,
125
+ problemStats,
126
+ tagRatings,
127
+ bojStats,
128
+ localSolvedCount,
129
+ error
130
+ };
131
+ }
132
+
133
+ export {
134
+ useUserStats
135
+ };
@@ -0,0 +1,427 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getArchiveDir,
4
+ getArchiveStrategy,
5
+ getAutoOpenEditor,
6
+ getConfigMetadata,
7
+ getDefaultLanguage,
8
+ getEditor,
9
+ getIncludeTag,
10
+ getSolvedAcHandle,
11
+ getSolvingDir
12
+ } from "./chunk-AHE4QHJD.js";
13
+
14
+ // src/hooks/use-init.ts
15
+ import { existsSync } from "fs";
16
+ import {
17
+ mkdir,
18
+ readFile,
19
+ writeFile,
20
+ access,
21
+ copyFile,
22
+ readdir
23
+ } from "fs/promises";
24
+ import { join, dirname } from "path";
25
+ import { fileURLToPath } from "url";
26
+ import { execaCommand, execa } from "execa";
27
+ import { useEffect, useState, useCallback } from "react";
28
+ function getCliRoot() {
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = dirname(__filename);
31
+ let current = __dirname;
32
+ while (current !== dirname(current)) {
33
+ const templatesDir = join(current, "templates");
34
+ try {
35
+ const stats = existsSync(templatesDir);
36
+ if (stats) return current;
37
+ } catch {
38
+ }
39
+ current = dirname(current);
40
+ }
41
+ return join(__dirname, "../..");
42
+ }
43
+ function buildDefaultConfigYaml({
44
+ language,
45
+ editor,
46
+ autoOpen,
47
+ solvingDir,
48
+ archiveDir,
49
+ archiveStrategy,
50
+ includeTag,
51
+ handle
52
+ }) {
53
+ return `
54
+ # ps-cli \uC124\uC815 \uD30C\uC77C
55
+ # \uB354 \uC790\uC138\uD55C \uC815\uBCF4\uB294 \uB2E4\uC74C\uC744 \uCC38\uACE0\uD558\uC138\uC694: https://github.com/rhseung/ps-cli
56
+
57
+ general:
58
+ # \uC0C8\uB85C\uC6B4 \uBB38\uC81C\uB97C \uAC00\uC838\uC62C \uB54C \uC0AC\uC6A9\uD560 \uAE30\uBCF8 \uD504\uB85C\uADF8\uB798\uBC0D \uC5B8\uC5B4\uC785\uB2C8\uB2E4.
59
+ default_language: ${language}
60
+ # \uD1B5\uACC4 \uC870\uD68C\uB97C \uC704\uD55C Solved.ac \uD578\uB4E4(\uB2C9\uB124\uC784)\uC785\uB2C8\uB2E4.
61
+ solved_ac_handle: "${handle}"
62
+
63
+ editor:
64
+ # \uC5D0\uB514\uD130\uB97C \uC5F4 \uB54C \uC0AC\uC6A9\uD560 \uBA85\uB839\uC5B4\uC785\uB2C8\uB2E4 (\uC608: code, cursor, vim).
65
+ command: ${editor}
66
+ # \uBB38\uC81C\uB97C \uAC00\uC838\uC628 \uD6C4 \uC790\uB3D9\uC73C\uB85C \uC5D0\uB514\uD130\uB97C \uC5F4\uC9C0 \uC5EC\uBD80\uC785\uB2C8\uB2E4.
67
+ auto_open: ${autoOpen}
68
+
69
+ paths:
70
+ # \uD604\uC7AC \uD480\uACE0 \uC788\uB294 \uBB38\uC81C\uB4E4\uC744 \uB2F4\uC744 \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C\uC785\uB2C8\uB2E4.
71
+ solving: ${solvingDir}
72
+ # \uD574\uACB0\uD55C \uBB38\uC81C\uB97C \uBCF4\uAD00\uD560 \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C\uC785\uB2C8\uB2E4.
73
+ archive: ${archiveDir}
74
+ # \uC544\uCE74\uC774\uBE59 \uC804\uB7B5\uC785\uB2C8\uB2E4 (flat, by-range, by-tier, by-tag).
75
+ archive_strategy: ${archiveStrategy}
76
+
77
+ archive:
78
+ # \uC544\uCE74\uC774\uBE0C \uC2DC \uC790\uB3D9\uC73C\uB85C Git \uCEE4\uBC0B\uC744 \uC218\uD589\uD560\uC9C0 \uC5EC\uBD80\uC785\uB2C8\uB2E4.
79
+ auto_commit: true
80
+ # Git \uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uD15C\uD50C\uB9BF\uC785\uB2C8\uB2E4 ({id}, {title} \uC0AC\uC6A9 \uAC00\uB2A5).
81
+ commit_message: "feat: solve {id} {title}"
82
+
83
+ markdown:
84
+ # \uBB38\uC81C README\uC5D0 \uC54C\uACE0\uB9AC\uC998 \uBD84\uB958(\uD0DC\uADF8)\uB97C \uD3EC\uD568\uD560\uC9C0 \uC5EC\uBD80\uC785\uB2C8\uB2E4.
85
+ include_tag: ${includeTag}
86
+
87
+ # \uC5B8\uC5B4\uBCC4 \uC124\uC815
88
+ # \uC774\uACF3\uC5D0\uC11C \uCEF4\uD30C\uC77C/\uC2E4\uD589 \uBA85\uB839\uC5B4\uB098 \uD15C\uD50C\uB9BF \uD30C\uC77C\uBA85\uC744 \uC218\uC815\uD558\uAC70\uB098 \uC0C8\uB85C\uC6B4 \uC5B8\uC5B4\uB97C \uCD94\uAC00\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
89
+ languages:
90
+ python:
91
+ extension: py
92
+ # \uD15C\uD50C\uB9BF \uD30C\uC77C\uBA85 (\uC635\uC158, \uAE30\uBCF8\uAC12: solution.py)
93
+ template_file: "solution.py"
94
+ # \uC2E4\uD589 \uBA85\uB839\uC5B4 (\uD544\uC218)
95
+ run: python3
96
+ cpp:
97
+ extension: cpp
98
+ # \uD15C\uD50C\uB9BF \uD30C\uC77C\uBA85 (\uC635\uC158, \uAE30\uBCF8\uAC12: solution.cpp)
99
+ template_file: "solution.cpp"
100
+ # \uCEF4\uD30C\uC77C \uBA85\uB839\uC5B4 (\uC635\uC158)
101
+ compile: "g++ -fdiagnostics-absolute-paths -o solution solution.cpp"
102
+ # \uC2E4\uD589 \uBA85\uB839\uC5B4 (\uD544\uC218)
103
+ run: "./solution"
104
+ # rust:
105
+ # extension: rs
106
+ # # \uD15C\uD50C\uB9BF \uD30C\uC77C\uBA85\uC744 \uC9C1\uC811 \uC9C0\uC815\uD558\uB824\uBA74 template_file\uC744 \uC0AC\uC6A9\uD558\uC138\uC694 (\uAE30\uBCF8: solution.{extension})
107
+ # template_file: "solution.rs"
108
+ # compile: "rustc {file}"
109
+ # run: "./{file_no_ext}"
110
+ `.trim();
111
+ }
112
+ function useInit({ onComplete }) {
113
+ const [currentStep, setCurrentStep] = useState("archive-dir");
114
+ const [completedSteps, setCompletedSteps] = useState([]);
115
+ const [confirmExit, setConfirmExit] = useState(false);
116
+ const [initialized, setInitialized] = useState(false);
117
+ const [form, setForm] = useState({
118
+ archiveDir: getArchiveDir(),
119
+ solvingDir: getSolvingDir(),
120
+ archiveStrategy: getArchiveStrategy(),
121
+ language: getDefaultLanguage(),
122
+ editor: getEditor(),
123
+ autoOpen: getAutoOpenEditor(),
124
+ includeTag: getIncludeTag(),
125
+ handle: getSolvedAcHandle() || ""
126
+ });
127
+ const [handleInputMode, setHandleInputMode] = useState(false);
128
+ const [created, setCreated] = useState([]);
129
+ const [cancelled, setCancelled] = useState(false);
130
+ useEffect(() => {
131
+ const handleSigInt = () => {
132
+ if (confirmExit) {
133
+ setCancelled(true);
134
+ setCurrentStep("cancelled");
135
+ setTimeout(() => {
136
+ onComplete();
137
+ }, 500);
138
+ return;
139
+ }
140
+ setConfirmExit(true);
141
+ };
142
+ process.on("SIGINT", handleSigInt);
143
+ return () => {
144
+ process.off("SIGINT", handleSigInt);
145
+ };
146
+ }, [confirmExit, onComplete]);
147
+ useEffect(() => {
148
+ async function loadProjectConfig() {
149
+ try {
150
+ const cwd = process.cwd();
151
+ const projectConfigPath = join(cwd, ".ps-cli.json");
152
+ await access(projectConfigPath);
153
+ const configContent = await readFile(projectConfigPath, "utf-8");
154
+ const projectConfig = JSON.parse(configContent);
155
+ setForm((prev) => ({
156
+ ...prev,
157
+ archiveDir: projectConfig.archiveDir ?? prev.archiveDir,
158
+ solvingDir: projectConfig.solvingDir ?? prev.solvingDir,
159
+ archiveStrategy: projectConfig.archiveStrategy ?? prev.archiveStrategy,
160
+ language: projectConfig.defaultLanguage ?? prev.language,
161
+ editor: projectConfig.editor ?? prev.editor,
162
+ autoOpen: projectConfig.autoOpenEditor !== void 0 ? projectConfig.autoOpenEditor : prev.autoOpen,
163
+ includeTag: projectConfig.includeTag !== void 0 ? projectConfig.includeTag : prev.includeTag,
164
+ handle: projectConfig.solvedAcHandle ?? prev.handle
165
+ }));
166
+ } catch {
167
+ } finally {
168
+ setInitialized(true);
169
+ }
170
+ }
171
+ void loadProjectConfig();
172
+ }, []);
173
+ const getStepLabel = useCallback((step) => {
174
+ const stepToConfigKey = {
175
+ "archive-dir": "paths.archive",
176
+ "solving-dir": "paths.solving",
177
+ "archive-strategy": "paths.archive-strategy",
178
+ language: "general.default-language",
179
+ editor: "editor.command",
180
+ "auto-open": "editor.auto-open",
181
+ "include-tag": "markdown.include-tag",
182
+ handle: "general.solved-ac-handle"
183
+ };
184
+ const configKey = stepToConfigKey[step];
185
+ if (!configKey) return "";
186
+ const meta = getConfigMetadata().find((m) => m.key === configKey);
187
+ return meta?.label ?? "";
188
+ }, []);
189
+ const getStepValue = useCallback(
190
+ (step) => {
191
+ switch (step) {
192
+ case "archive-dir":
193
+ return form.archiveDir === "." ? "\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8" : form.archiveDir;
194
+ case "solving-dir":
195
+ return form.solvingDir === "." ? "\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8" : form.solvingDir;
196
+ case "archive-strategy": {
197
+ const strategyLabels = {
198
+ flat: "\uD3C9\uBA74 (\uC804\uBD80 \uB098\uC5F4)",
199
+ "by-range": "1000\uBC88\uB300 \uBB36\uAE30",
200
+ "by-tier": "\uD2F0\uC5B4\uBCC4",
201
+ "by-tag": "\uD0DC\uADF8\uBCC4"
202
+ };
203
+ return strategyLabels[form.archiveStrategy] || form.archiveStrategy;
204
+ }
205
+ case "language":
206
+ return form.language;
207
+ case "editor":
208
+ return form.editor;
209
+ case "auto-open":
210
+ return form.autoOpen ? "\uC608" : "\uC544\uB2C8\uC624";
211
+ case "include-tag":
212
+ return form.includeTag ? "\uC608" : "\uC544\uB2C8\uC624";
213
+ case "handle":
214
+ return form.handle || "(\uC2A4\uD0B5)";
215
+ default:
216
+ return "";
217
+ }
218
+ },
219
+ [form]
220
+ );
221
+ const executeInit = useCallback(
222
+ async (overrideHandle) => {
223
+ try {
224
+ const cwd = process.cwd();
225
+ const cliRoot = getCliRoot();
226
+ const psCliDir = join(cwd, ".ps-cli");
227
+ const templatesDir = join(psCliDir, "templates");
228
+ await mkdir(psCliDir, { recursive: true });
229
+ await mkdir(templatesDir, { recursive: true });
230
+ setCreated((prev) => [...prev, ".ps-cli/"]);
231
+ const handleToUse = (overrideHandle ?? form.handle)?.trim() || "";
232
+ const configYaml = buildDefaultConfigYaml({
233
+ language: form.language,
234
+ editor: form.editor,
235
+ autoOpen: form.autoOpen,
236
+ solvingDir: form.solvingDir,
237
+ archiveDir: form.archiveDir,
238
+ archiveStrategy: form.archiveStrategy,
239
+ includeTag: form.includeTag,
240
+ handle: handleToUse
241
+ });
242
+ await writeFile(join(psCliDir, "config.yaml"), configYaml, "utf-8");
243
+ setCreated((prev) => [...prev, ".ps-cli/config.yaml"]);
244
+ const defaultTemplatesDir = join(cliRoot, "templates");
245
+ if (existsSync(defaultTemplatesDir)) {
246
+ const files = await readdir(defaultTemplatesDir);
247
+ for (const file of files) {
248
+ await copyFile(
249
+ join(defaultTemplatesDir, file),
250
+ join(templatesDir, file)
251
+ );
252
+ }
253
+ setCreated((prev) => [
254
+ ...prev,
255
+ ".ps-cli/templates/ (\uAE30\uBCF8 \uD15C\uD50C\uB9BF \uBCF5\uC0AC)"
256
+ ]);
257
+ }
258
+ if (form.archiveDir !== "." && form.archiveDir !== "") {
259
+ const archiveDirPath = join(cwd, form.archiveDir);
260
+ try {
261
+ await mkdir(archiveDirPath, { recursive: true });
262
+ setCreated((prev) => [...prev, `${form.archiveDir}/`]);
263
+ } catch (err) {
264
+ const error = err;
265
+ if (error.code !== "EEXIST") {
266
+ throw err;
267
+ }
268
+ }
269
+ }
270
+ if (form.solvingDir !== "." && form.solvingDir !== "") {
271
+ const solvingDirPath = join(cwd, form.solvingDir);
272
+ try {
273
+ await mkdir(solvingDirPath, { recursive: true });
274
+ setCreated((prev) => [...prev, `${form.solvingDir}/`]);
275
+ } catch (err) {
276
+ const error = err;
277
+ if (error.code !== "EEXIST") {
278
+ throw err;
279
+ }
280
+ }
281
+ }
282
+ const gitignorePath = join(cwd, ".gitignore");
283
+ const gitignorePatterns = [];
284
+ if (form.solvingDir !== "." && form.solvingDir !== "") {
285
+ gitignorePatterns.push(`${form.solvingDir}/`);
286
+ }
287
+ if (gitignorePatterns.length > 0) {
288
+ try {
289
+ const gitignoreContent = await readFile(gitignorePath, "utf-8");
290
+ let updatedContent = gitignoreContent.trim();
291
+ let hasChanges = false;
292
+ for (const pattern of gitignorePatterns) {
293
+ if (!gitignoreContent.includes(pattern)) {
294
+ updatedContent += (updatedContent ? "\n" : "") + `
295
+ # ps-cli \uBB38\uC81C \uB514\uB809\uD1A0\uB9AC
296
+ ${pattern}`;
297
+ hasChanges = true;
298
+ }
299
+ }
300
+ if (hasChanges) {
301
+ await writeFile(gitignorePath, updatedContent + "\n", "utf-8");
302
+ setCreated((prev) => [...prev, ".gitignore \uC5C5\uB370\uC774\uD2B8"]);
303
+ }
304
+ } catch (err) {
305
+ const error = err;
306
+ if (error.code === "ENOENT") {
307
+ const content = `# ps-cli \uBB38\uC81C \uB514\uB809\uD1A0\uB9AC
308
+ ${gitignorePatterns.join("\n")}
309
+ `;
310
+ await writeFile(gitignorePath, content, "utf-8");
311
+ setCreated((prev) => [...prev, ".gitignore \uC0DD\uC131"]);
312
+ } else {
313
+ console.warn(".gitignore \uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328:", error.message);
314
+ }
315
+ }
316
+ }
317
+ try {
318
+ const gitDir = join(cwd, ".git");
319
+ let isGitRepo = false;
320
+ try {
321
+ await access(gitDir);
322
+ isGitRepo = true;
323
+ } catch {
324
+ }
325
+ if (!isGitRepo) {
326
+ await execaCommand("git init", { cwd });
327
+ setCreated((prev) => [...prev, "Git \uC800\uC7A5\uC18C \uCD08\uAE30\uD654"]);
328
+ }
329
+ const filesToAdd = [".ps-cli"];
330
+ try {
331
+ await access(gitignorePath);
332
+ filesToAdd.push(".gitignore");
333
+ } catch {
334
+ }
335
+ if (filesToAdd.length > 0) {
336
+ await execa("git", ["add", ...filesToAdd], { cwd });
337
+ try {
338
+ await execa("git", ["rev-parse", "--verify", "HEAD"], { cwd });
339
+ } catch {
340
+ await execa(
341
+ "git",
342
+ ["commit", "-m", "chore: ps-cli \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654"],
343
+ { cwd }
344
+ );
345
+ setCreated((prev) => [...prev, "\uCD08\uAE30 \uCEE4\uBC0B \uC0DD\uC131"]);
346
+ }
347
+ }
348
+ } catch (err) {
349
+ const error = err;
350
+ console.warn("Git \uC5F0\uB3D9 \uC2E4\uD328:", error.message);
351
+ }
352
+ setTimeout(() => {
353
+ onComplete();
354
+ }, 3e3);
355
+ } catch (err) {
356
+ const error = err;
357
+ console.error("\uCD08\uAE30\uD654 \uC911 \uC624\uB958 \uBC1C\uC0DD:", error.message);
358
+ setCancelled(true);
359
+ setCurrentStep("cancelled");
360
+ setTimeout(() => {
361
+ onComplete();
362
+ }, 2e3);
363
+ }
364
+ },
365
+ [form, onComplete]
366
+ );
367
+ const moveToNextStep = useCallback(
368
+ (selectedValue, stepLabel, handleValue) => {
369
+ setCompletedSteps((prev) => [
370
+ ...prev,
371
+ { label: stepLabel, value: selectedValue }
372
+ ]);
373
+ if (currentStep === "handle" && handleValue !== void 0) {
374
+ setForm((prev) => ({
375
+ ...prev,
376
+ handle: handleValue
377
+ }));
378
+ }
379
+ const stepOrder = [
380
+ "archive-dir",
381
+ "solving-dir",
382
+ "archive-strategy",
383
+ "language",
384
+ "editor",
385
+ "auto-open",
386
+ "include-tag",
387
+ "handle",
388
+ "done"
389
+ ];
390
+ const currentIndex = stepOrder.indexOf(currentStep);
391
+ if (currentIndex < stepOrder.length - 1) {
392
+ const nextStep = stepOrder[currentIndex + 1];
393
+ setCurrentStep(nextStep);
394
+ if (nextStep === "done") {
395
+ if (currentStep === "handle" && handleValue !== void 0) {
396
+ void executeInit(handleValue.trim());
397
+ } else {
398
+ void executeInit();
399
+ }
400
+ }
401
+ }
402
+ },
403
+ [currentStep, executeInit]
404
+ );
405
+ return {
406
+ currentStep,
407
+ completedSteps,
408
+ confirmExit,
409
+ initialized,
410
+ form,
411
+ handleInputMode,
412
+ created,
413
+ cancelled,
414
+ setForm,
415
+ setHandleInputMode,
416
+ setConfirmExit,
417
+ setCurrentStep,
418
+ setCancelled,
419
+ moveToNextStep,
420
+ getStepLabel,
421
+ getStepValue
422
+ };
423
+ }
424
+
425
+ export {
426
+ useInit
427
+ };
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findProjectRoot,
4
+ getConfigMetadata
5
+ } from "./chunk-AHE4QHJD.js";
6
+
7
+ // src/hooks/use-config.ts
8
+ import { existsSync, mkdirSync } from "fs";
9
+ import { readFile, writeFile, unlink, rm } from "fs/promises";
10
+ import { join } from "path";
11
+ import { useEffect, useState } from "react";
12
+ import { parse, stringify } from "yaml";
13
+ function getProjectConfigPath() {
14
+ const root = findProjectRoot();
15
+ if (!root) return null;
16
+ return join(root, ".ps-cli", "config.yaml");
17
+ }
18
+ async function readProjectConfig() {
19
+ const configPath = getProjectConfigPath();
20
+ if (!configPath || !existsSync(configPath)) {
21
+ return null;
22
+ }
23
+ try {
24
+ const content = await readFile(configPath, "utf-8");
25
+ return parse(content);
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ async function writeProjectConfig(config) {
31
+ const configPath = getProjectConfigPath();
32
+ if (!configPath) {
33
+ const root = process.cwd();
34
+ const psCliDir = join(root, ".ps-cli");
35
+ if (!existsSync(psCliDir)) {
36
+ mkdirSync(psCliDir, { recursive: true });
37
+ }
38
+ const newConfigPath = join(psCliDir, "config.yaml");
39
+ await writeFile(newConfigPath, stringify(config), "utf-8");
40
+ return;
41
+ }
42
+ await writeFile(configPath, stringify(config), "utf-8");
43
+ }
44
+ function setDeep(obj, path, value) {
45
+ const keys = path.split(".");
46
+ let current = obj;
47
+ for (let i = 0; i < keys.length - 1; i++) {
48
+ const key = keys[i];
49
+ if (!(key in current) || typeof current[key] !== "object") {
50
+ current[key] = {};
51
+ }
52
+ current = current[key];
53
+ }
54
+ current[keys[keys.length - 1]] = value;
55
+ }
56
+ function useConfig({
57
+ configKey,
58
+ value,
59
+ get: _get,
60
+ list: _list,
61
+ clear,
62
+ onComplete: _onComplete
63
+ }) {
64
+ const [config, setConfig] = useState(null);
65
+ const [loading, setLoading] = useState(true);
66
+ const [cleared, setCleared] = useState(false);
67
+ const [saved, setSaved] = useState(false);
68
+ useEffect(() => {
69
+ async function loadConfig() {
70
+ const projectConfig = await readProjectConfig();
71
+ setConfig(projectConfig);
72
+ setLoading(false);
73
+ }
74
+ void loadConfig();
75
+ }, []);
76
+ useEffect(() => {
77
+ if (clear && !cleared) {
78
+ void (async () => {
79
+ const root = findProjectRoot();
80
+ if (root) {
81
+ const psCliDir = join(root, ".ps-cli");
82
+ if (existsSync(psCliDir)) {
83
+ await rm(psCliDir, { recursive: true, force: true });
84
+ }
85
+ const legacyPath = join(root, ".ps-cli.json");
86
+ if (existsSync(legacyPath)) {
87
+ await unlink(legacyPath);
88
+ }
89
+ }
90
+ setCleared(true);
91
+ })();
92
+ }
93
+ }, [clear, cleared]);
94
+ useEffect(() => {
95
+ if (configKey && value !== void 0 && !saved) {
96
+ void (async () => {
97
+ const currentConfig = await readProjectConfig() ?? {};
98
+ const updatedConfig = { ...currentConfig };
99
+ const metadata = getConfigMetadata();
100
+ const item = metadata.find((m) => m.key === configKey);
101
+ if (!item) {
102
+ console.error(`\uC54C \uC218 \uC5C6\uB294 \uC124\uC815 \uD0A4: ${configKey}`);
103
+ process.exit(1);
104
+ }
105
+ const path = item.path;
106
+ let finalValue = value;
107
+ if (item.type === "boolean") {
108
+ if (value !== "true" && value !== "false") {
109
+ console.error(
110
+ `${configKey} \uAC12\uC740 true \uB610\uB294 false \uC5EC\uC57C \uD569\uB2C8\uB2E4: ${value}`
111
+ );
112
+ process.exit(1);
113
+ }
114
+ finalValue = value === "true";
115
+ } else if (item.type === "select" && item.suggestions) {
116
+ if (!item.suggestions.includes(value)) {
117
+ console.error(
118
+ `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uAC12\uC785\uB2C8\uB2E4: ${value}
119
+ \uC9C0\uC6D0\uB418\uB294 \uAC12: ${item.suggestions.join(", ")}`
120
+ );
121
+ process.exit(1);
122
+ }
123
+ }
124
+ setDeep(
125
+ updatedConfig,
126
+ path,
127
+ finalValue
128
+ );
129
+ await writeProjectConfig(updatedConfig);
130
+ setSaved(true);
131
+ })();
132
+ }
133
+ }, [configKey, value, saved]);
134
+ return {
135
+ config,
136
+ loading,
137
+ cleared,
138
+ saved
139
+ };
140
+ }
141
+
142
+ export {
143
+ useConfig
144
+ };