project-auto-wizard 0.1.16 → 0.1.18

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 CHANGED
@@ -16,7 +16,7 @@ npx project-auto-wizard
16
16
  [![node](https://img.shields.io/badge/node-%3E%3D20.12-brightgreen)](package.json)
17
17
 
18
18
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
19
- ## 최신 버전 : v0.1.14 (2026-08-04)
19
+ ## 최신 버전 : v0.1.18 (2026-08-05)
20
20
 
21
21
  [전체 버전 기록 보기](CHANGELOG.md)
22
22
 
@@ -154,6 +154,8 @@ npx project-auto-wizard --mode doctor # 환경 진단 (읽기 전용, 규칙
154
154
  | `--mode status` | 설치된 버전·타입·브랜치 모드·옵션값과, 설치 시점 대비 사용자가 직접 수정한 워크플로우 파일 목록을 보여줍니다. 네트워크 접근 없음(로컬 파일 비교만) |
155
155
  | `--mode doctor` | `version.yml` 설치 여부, `gh` CLI 설치/인증 상태, GitHub Actions workflow permissions, `WORKFLOW_PAT` secret 등록 여부, merge commit 허용 설정을 점검합니다. `gh api` 호출을 사용하므로 네트워크 접근이 발생합니다(규칙 기반 점검 — AI 진단 아님) |
156
156
 
157
+ > **드리프트 판정 기준**: `--mode status`는 설치된 워크플로우 파일이 "설치 시점 기본값 템플릿"과 바이트 단위로 일치하는지만 비교합니다 — 파일을 직접 편집했는지는 추적하지 않습니다. 대화형 설치에서 `@wizard ask` 질문(예: 배포 포트)에 기본값이 아닌 값으로 응답했다면, 파일을 전혀 수정하지 않았더라도 설치 직후부터 항상 "사용자가 수정한 워크플로우 파일"로 표시됩니다. 정상 동작이며, 파일을 직접 편집했는지 구분하려면 해당 값이 예상한 응답과 일치하는지 직접 확인하세요.
158
+
157
159
  `--dry-run`을 어떤 모드와도 함께 쓰면 실제로 파일을 바꾸지 않고 무엇이 바뀔지만 미리 보여줍니다(`full`/`version`/`workflows`/`revert` 전체 지원):
158
160
 
159
161
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-auto-wizard",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "One command DevOps: npx wizard that installs GitHub-native AI Release Automation into any project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,6 +20,8 @@ import { runWorkflows } from "./workflows.js";
20
20
  import { runRevert } from "./revert.js";
21
21
  import { runUninstallFlow } from "./uninstall.js";
22
22
  import * as prompts from "../ui/prompts.js";
23
+ import { runStatus, printStatus } from "./status.js";
24
+ import { runDoctor, printDoctorReport } from "./doctor.js";
23
25
 
24
26
  const CANCEL = prompts.CANCEL;
25
27
  const isCancel = (v) => v === CANCEL || typeof v === "symbol";
@@ -44,6 +46,10 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
44
46
  const mode = await io.selectMode();
45
47
  if (mode === CANCEL || mode == null) { io.cancelMessage?.("설치를 취소했습니다."); return 0; }
46
48
 
49
+ // status/doctor — 읽기 전용, 감지·breaking 게이트 불필요. CLI --mode status/doctor(index.js)와 동일하게 즉시 종료.
50
+ if (mode === "status") { printStatus(runStatus(payload, cwd)); return 0; }
51
+ if (mode === "doctor") { printDoctorReport(runDoctor(cwd)); return 0; }
52
+
47
53
  // revert 모드 — 확인 질문(기본 아니오) 후 payload 유래 파일 제거. 감지·breaking 게이트 불필요.
48
54
  if (mode === "revert") {
49
55
  const ok = await io.askYesNo("마법사가 설치한 워크플로우·스크립트를 제거할까요? (version.yml·README는 보존)", false);
@@ -27,13 +27,12 @@ export function detectTypes(root) {
27
27
  return detectTypesFromMarkers({ has: hasFile(root), read: readFile(root) });
28
28
  }
29
29
 
30
- // 버전 감지 — .sh detect_version 순서. jq 유무는 command 존재로 판정.
31
- export function detectVersion(root, { hasJq } = {}) {
30
+ // 버전 감지 — .sh detect_version 순서. jq package.json 파싱에 쓰인 적이 없어 게이트를 제거했다(이슈 #22 L4).
31
+ export function detectVersion(root, { warn = (m) => console.error(m) } = {}) {
32
32
  const read = readFile(root);
33
33
  const readJson = (rel) => { const c = read(rel); try { return c ? JSON.parse(c) : null; } catch { return null; } };
34
- const jq = hasJq ?? hasCommand("jq");
35
34
  const gitTag = gitOut(root, ["describe", "--tags", "--abbrev=0"]);
36
- return detectVersionFromFiles({ read, readJson, hasJq: jq, gitTag });
35
+ return detectVersionFromFiles({ read, readJson, gitTag, warn });
37
36
  }
38
37
 
39
38
  // 기본 브랜치 감지 — symbolic-ref → remote show → main.
@@ -56,13 +55,6 @@ export function detectRepoName(root) {
56
55
  return basename(root);
57
56
  }
58
57
 
59
- function hasCommand(cmd) {
60
- try {
61
- execFileSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
62
- return true;
63
- } catch { return false; }
64
- }
65
-
66
58
  // Spring application*.yml 탐색 (.sh resolve_spring_app_yml_dir/path L2767~2780 등가)
67
59
  // find {base} -path "*/src/main/resources/application*.yml" | head -1 의 fs 재귀 구현.
68
60
  // 반환: root 기준 상대경로 (예: "server/src/main/resources/application.yml") 또는 "".
@@ -35,9 +35,10 @@ export function detectTypesFromMarkers({ has, read }) {
35
35
  const VERSION_RE = /^\d+\.\d+\.\d+$/;
36
36
 
37
37
  // 버전 감지 (동작명세 §3.3) — 순서대로 첫 성공. read(relpath)=>string|null 주입.
38
- export function detectVersionFromFiles({ read, readJson, hasJq, gitTag }) {
38
+ // package.json은 이미 Node JSON.parse로 파싱을 마친 값이므로 jq 설치 여부와 무관하게 항상 사용한다(이슈 #22 L4).
39
+ export function detectVersionFromFiles({ read, readJson, gitTag, warn }) {
39
40
  const pkg = readJson?.("package.json");
40
- if (hasJq && pkg?.version && VERSION_RE.test(pkg.version)) return pkg.version;
41
+ if (pkg?.version && VERSION_RE.test(pkg.version)) return pkg.version;
41
42
  const grab = (content, re) => {
42
43
  for (const line of (content || "").split("\n")) {
43
44
  const m = line.match(re);
@@ -50,6 +51,7 @@ export function detectVersionFromFiles({ read, readJson, hasJq, gitTag }) {
50
51
  if ((v = grab(read("pubspec.yaml"), /^version:\s*(\d+\.\d+\.\d+)/))) return v;
51
52
  if ((v = grab(read("pyproject.toml"), /version\s*=\s*["']?(\d+\.\d+\.\d+)/))) return v;
52
53
  if (gitTag) { const t = String(gitTag).replace(/^v/, ""); if (VERSION_RE.test(t)) return t; }
54
+ warn?.("⚠️ 버전을 자동 감지하지 못해 기본값 0.0.1을 사용합니다 — --project-version으로 직접 지정하거나 version.yml을 확인하세요.");
53
55
  return "0.0.1";
54
56
  }
55
57
 
package/src/ui/ansi.js CHANGED
@@ -1,4 +1,4 @@
1
- // 공용 ANSI 헬퍼 — banner/status-cards가 공유 (readline-engine 내부 헬퍼와 독립, 의존성 0)
1
+ // 공용 ANSI 헬퍼 — banner/status-cards/summary가 공유 (readline-engine 내부 헬퍼와 독립, 의존성 0)
2
2
  const E = "\x1b[";
3
3
  export const A = {
4
4
  reset: `${E}0m`,
@@ -10,7 +10,15 @@ export const A = {
10
10
  magenta: `${E}35m`,
11
11
  gray: `${E}90m`,
12
12
  };
13
- export const paint = (s, color) => `${color}${s}${A.reset}`;
13
+
14
+ // NO_COLOR(https://no-color.org) 환경변수 또는 대상 스트림이 TTY가 아니면 색상을 끈다.
15
+ // no-color.org 규격상 NO_COLOR는 "값과 무관하게 존재 여부"만 본다 — NO_COLOR=""(빈 문자열)도
16
+ // "설정됨"으로 취급해야 하므로 truthy 체크(`!process.env.NO_COLOR`)가 아니라 존재 체크를 쓴다.
17
+ export function colorEnabled(stream = process.stdout) {
18
+ return process.env.NO_COLOR === undefined && !!stream.isTTY;
19
+ }
20
+
21
+ export const paint = (s, color, enabled = colorEnabled()) => (enabled ? `${color}${s}${A.reset}` : String(s));
14
22
 
15
23
  // 대략적 표시 폭 (CJK 2칸 · ANSI 시퀀스 0칸) — 박스 우변 정렬용
16
24
  export function visualWidth(s) {
package/src/ui/prompts.js CHANGED
@@ -15,6 +15,8 @@ export async function selectMode() {
15
15
  { value: "workflows", label: "워크플로우만 — 빌드·배포 GitHub Actions만 설치" },
16
16
  { value: "revert", label: "되돌리기 — 마법사가 설치한 워크플로우·스크립트 제거" },
17
17
  { value: "uninstall", label: "완전 삭제 — 마법사가 설치·수정한 모든 항목 제거(확인 후, README·gitignore·version.yml 포함)" },
18
+ { value: "status", label: "설치 상태 확인 — 읽기 전용, 버전·타입·드리프트 확인" },
19
+ { value: "doctor", label: "환경 진단 — 읽기 전용, gh CLI·권한·secret 설정 점검" },
18
20
  ],
19
21
  });
20
22
  }
@@ -15,7 +15,10 @@ const c = {
15
15
  reset: `${ESC}0m`, dim: `${ESC}2m`, bold: `${ESC}1m`,
16
16
  cyan: `${ESC}36m`, green: `${ESC}32m`, gray: `${ESC}90m`, yellow: `${ESC}33m`,
17
17
  };
18
- const paint = (s, color) => `${color}${s}${c.reset}`;
18
+ // NO_COLOR(https://no-color.org)/비TTY 가드 — ansi.js와 동일한 규칙(존재 여부만 체크, 값 무관)이지만
19
+ // 의존성 0 유지를 위해 자체 구현.
20
+ const colorEnabled = () => process.env.NO_COLOR === undefined && !!stdout.isTTY;
21
+ const paint = (s, color, enabled = colorEnabled()) => (enabled ? `${color}${s}${c.reset}` : String(s));
19
22
  const hideCursor = () => stdout.write(`${ESC}?25l`);
20
23
  const showCursor = () => stdout.write(`${ESC}?25h`);
21
24
 
@@ -55,15 +58,22 @@ function keySession(renderFn, onKey) {
55
58
 
56
59
  const cleanup = () => {
57
60
  stdin.removeListener("keypress", handler);
61
+ stdin.removeListener("end", onEnd);
58
62
  if (stdin.isTTY) stdin.setRawMode(wasRaw);
59
63
  stdin.pause();
60
64
  showCursor();
61
65
  };
62
66
 
67
+ // stdin 종료(EOF/Ctrl+D, SSH 연결 끊김 등) — 취소(ESC/Ctrl+C)와 동일하게 처리해 무한 대기를 방지한다.
68
+ const onEnd = () => {
69
+ cleanup();
70
+ resolve(CANCEL);
71
+ };
72
+
63
73
  const handler = (str, key) => {
64
74
  key = key || {};
65
- // 취소: Ctrl+C / ESC
66
- if ((key.ctrl && key.name === "c") || key.name === "escape") {
75
+ // 취소: Ctrl+C / Ctrl+D / ESC (raw mode에서는 Ctrl+D가 stdin "end"가 아니라 일반 keypress로 들어온다)
76
+ if ((key.ctrl && (key.name === "c" || key.name === "d")) || key.name === "escape") {
67
77
  cleanup();
68
78
  resolve(CANCEL);
69
79
  return;
@@ -77,6 +87,7 @@ function keySession(renderFn, onKey) {
77
87
  }
78
88
  };
79
89
  stdin.on("keypress", handler);
90
+ stdin.on("end", onEnd);
80
91
  renderFn(); // 최초 렌더
81
92
  });
82
93
  }
@@ -194,14 +205,22 @@ export async function text({ message, defaultValue = "" }) {
194
205
 
195
206
  const cleanup = () => {
196
207
  stdin.removeListener("keypress", handler);
208
+ stdin.removeListener("end", onEnd);
197
209
  if (stdin.isTTY) stdin.setRawMode(wasRaw);
198
210
  stdin.pause();
199
211
  stdout.write("\n");
200
212
  };
201
213
 
214
+ // stdin 종료(EOF/Ctrl+D) — 취소와 동일하게 처리해 무한 대기를 방지한다.
215
+ const onEnd = () => {
216
+ cleanup();
217
+ resolve(CANCEL);
218
+ };
219
+
202
220
  const handler = (str, key) => {
203
221
  key = key || {};
204
- if ((key.ctrl && key.name === "c") || key.name === "escape") { cleanup(); resolve(CANCEL); return; }
222
+ // 취소: Ctrl+C / Ctrl+D / ESC (raw mode에서는 Ctrl+D가 stdin "end" 아니라 일반 keypress로 들어온다)
223
+ if ((key.ctrl && (key.name === "c" || key.name === "d")) || key.name === "escape") { cleanup(); resolve(CANCEL); return; }
205
224
  if (key.name === "return" || key.name === "enter") {
206
225
  cleanup();
207
226
  resolve(buf.length ? buf : defaultValue);
@@ -212,6 +231,7 @@ export async function text({ message, defaultValue = "" }) {
212
231
  if (str && !key.ctrl && !key.meta && str.length === 1 && str >= " ") { buf += str; prompt(); return; }
213
232
  };
214
233
  stdin.on("keypress", handler);
234
+ stdin.on("end", onEnd);
215
235
  prompt();
216
236
  });
217
237
  }
package/src/ui/summary.js CHANGED
@@ -1,17 +1,15 @@
1
1
  // 완료 요약 출력 (.sh print_summary 등가). 전부 stderr.
2
2
  // ctx: { mode, types:[], version, copiedFiles:[], branches?, gitignoreUpdated? }
3
3
  import { WORKFLOW_PREFIX, WORKFLOW_COMMON_PREFIX } from "../core/paths.js";
4
+ import { paint, A, colorEnabled } from "./ansi.js";
4
5
 
5
6
  const SEPARATOR = "────────────────────────────────────────";
6
7
 
7
8
  export function printSummary(ctx) {
8
9
  const { mode, types = [], version = "", copiedFiles = [], branches = null, gitignoreUpdated = false } = ctx || {};
9
10
  const err = (s = "") => process.stderr.write(`${s}\n`);
10
- // 색상은 TTY일 때만 (.sh YELLOW/CYAN/NC 등가)
11
- const isTty = !!process.stderr.isTTY;
12
- const YELLOW = isTty ? "\x1b[1;33m" : "";
13
- const CYAN = isTty ? "\x1b[0;36m" : "";
14
- const NC = isTty ? "\x1b[0m" : "";
11
+ // 색상은 ansi.js의 공용 가드로 통일 (NO_COLOR + stderr TTY 여부)
12
+ const enabled = colorEnabled(process.stderr);
15
13
 
16
14
  err("");
17
15
  err(SEPARATOR);
@@ -102,7 +100,7 @@ export function printSummary(ctx) {
102
100
  // 필수 작업 안내
103
101
  err(SEPARATOR);
104
102
  err("");
105
- err(`${YELLOW}⚠️ 다음 작업을 확인해주세요:${NC}`);
103
+ err(paint(paint("⚠️ 다음 작업을 확인해주세요:", A.yellow, enabled), A.bold, enabled));
106
104
  err("");
107
105
  err(" 1️⃣ 릴리스 automerge용 PAT (선택 — 없으면 GITHUB_TOKEN 사용)");
108
106
  err(" → Repository Settings > Secrets > Actions");
@@ -114,6 +112,6 @@ export function printSummary(ctx) {
114
112
  err("");
115
113
  err(SEPARATOR);
116
114
  err("");
117
- err(`${CYAN}📖 워크플로우 구성과 릴리스 흐름은 README를 참고하세요.${NC}`);
115
+ err(paint("📖 워크플로우 구성과 릴리스 흐름은 README를 참고하세요.", A.cyan, enabled));
118
116
  err("");
119
117
  }