@byh3071/vhk 0.3.0 → 0.4.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/dist/index.js +1527 -303
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,118 +1,792 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
// node_modules/.pnpm/ignore@7.0.5/node_modules/ignore/index.js
|
|
29
|
+
var require_ignore = __commonJS({
|
|
30
|
+
"node_modules/.pnpm/ignore@7.0.5/node_modules/ignore/index.js"(exports, module) {
|
|
31
|
+
"use strict";
|
|
32
|
+
function makeArray(subject) {
|
|
33
|
+
return Array.isArray(subject) ? subject : [subject];
|
|
34
|
+
}
|
|
35
|
+
var UNDEFINED = void 0;
|
|
36
|
+
var EMPTY = "";
|
|
37
|
+
var SPACE = " ";
|
|
38
|
+
var ESCAPE = "\\";
|
|
39
|
+
var REGEX_TEST_BLANK_LINE = /^\s+$/;
|
|
40
|
+
var REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
|
|
41
|
+
var REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
|
|
42
|
+
var REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
|
|
43
|
+
var REGEX_SPLITALL_CRLF = /\r?\n/g;
|
|
44
|
+
var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
|
|
45
|
+
var REGEX_TEST_TRAILING_SLASH = /\/$/;
|
|
46
|
+
var SLASH = "/";
|
|
47
|
+
var TMP_KEY_IGNORE = "node-ignore";
|
|
48
|
+
if (typeof Symbol !== "undefined") {
|
|
49
|
+
TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
|
|
50
|
+
}
|
|
51
|
+
var KEY_IGNORE = TMP_KEY_IGNORE;
|
|
52
|
+
var define = (object, key, value) => {
|
|
53
|
+
Object.defineProperty(object, key, { value });
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
var REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
|
|
57
|
+
var RETURN_FALSE = () => false;
|
|
58
|
+
var sanitizeRange = (range) => range.replace(
|
|
59
|
+
REGEX_REGEXP_RANGE,
|
|
60
|
+
(match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) ? match : EMPTY
|
|
61
|
+
);
|
|
62
|
+
var cleanRangeBackSlash = (slashes) => {
|
|
63
|
+
const { length } = slashes;
|
|
64
|
+
return slashes.slice(0, length - length % 2);
|
|
65
|
+
};
|
|
66
|
+
var REPLACERS = [
|
|
67
|
+
[
|
|
68
|
+
// Remove BOM
|
|
69
|
+
// TODO:
|
|
70
|
+
// Other similar zero-width characters?
|
|
71
|
+
/^\uFEFF/,
|
|
72
|
+
() => EMPTY
|
|
73
|
+
],
|
|
74
|
+
// > Trailing spaces are ignored unless they are quoted with backslash ("\")
|
|
75
|
+
[
|
|
76
|
+
// (a\ ) -> (a )
|
|
77
|
+
// (a ) -> (a)
|
|
78
|
+
// (a ) -> (a)
|
|
79
|
+
// (a \ ) -> (a )
|
|
80
|
+
/((?:\\\\)*?)(\\?\s+)$/,
|
|
81
|
+
(_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY)
|
|
82
|
+
],
|
|
83
|
+
// Replace (\ ) with ' '
|
|
84
|
+
// (\ ) -> ' '
|
|
85
|
+
// (\\ ) -> '\\ '
|
|
86
|
+
// (\\\ ) -> '\\ '
|
|
87
|
+
[
|
|
88
|
+
/(\\+?)\s/g,
|
|
89
|
+
(_, m1) => {
|
|
90
|
+
const { length } = m1;
|
|
91
|
+
return m1.slice(0, length - length % 2) + SPACE;
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
// Escape metacharacters
|
|
95
|
+
// which is written down by users but means special for regular expressions.
|
|
96
|
+
// > There are 12 characters with special meanings:
|
|
97
|
+
// > - the backslash \,
|
|
98
|
+
// > - the caret ^,
|
|
99
|
+
// > - the dollar sign $,
|
|
100
|
+
// > - the period or dot .,
|
|
101
|
+
// > - the vertical bar or pipe symbol |,
|
|
102
|
+
// > - the question mark ?,
|
|
103
|
+
// > - the asterisk or star *,
|
|
104
|
+
// > - the plus sign +,
|
|
105
|
+
// > - the opening parenthesis (,
|
|
106
|
+
// > - the closing parenthesis ),
|
|
107
|
+
// > - and the opening square bracket [,
|
|
108
|
+
// > - the opening curly brace {,
|
|
109
|
+
// > These special characters are often called "metacharacters".
|
|
110
|
+
[
|
|
111
|
+
/[\\$.|*+(){^]/g,
|
|
112
|
+
(match) => `\\${match}`
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
// > a question mark (?) matches a single character
|
|
116
|
+
/(?!\\)\?/g,
|
|
117
|
+
() => "[^/]"
|
|
118
|
+
],
|
|
119
|
+
// leading slash
|
|
120
|
+
[
|
|
121
|
+
// > A leading slash matches the beginning of the pathname.
|
|
122
|
+
// > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
|
|
123
|
+
// A leading slash matches the beginning of the pathname
|
|
124
|
+
/^\//,
|
|
125
|
+
() => "^"
|
|
126
|
+
],
|
|
127
|
+
// replace special metacharacter slash after the leading slash
|
|
128
|
+
[
|
|
129
|
+
/\//g,
|
|
130
|
+
() => "\\/"
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
// > A leading "**" followed by a slash means match in all directories.
|
|
134
|
+
// > For example, "**/foo" matches file or directory "foo" anywhere,
|
|
135
|
+
// > the same as pattern "foo".
|
|
136
|
+
// > "**/foo/bar" matches file or directory "bar" anywhere that is directly
|
|
137
|
+
// > under directory "foo".
|
|
138
|
+
// Notice that the '*'s have been replaced as '\\*'
|
|
139
|
+
/^\^*\\\*\\\*\\\//,
|
|
140
|
+
// '**/foo' <-> 'foo'
|
|
141
|
+
() => "^(?:.*\\/)?"
|
|
142
|
+
],
|
|
143
|
+
// starting
|
|
144
|
+
[
|
|
145
|
+
// there will be no leading '/'
|
|
146
|
+
// (which has been replaced by section "leading slash")
|
|
147
|
+
// If starts with '**', adding a '^' to the regular expression also works
|
|
148
|
+
/^(?=[^^])/,
|
|
149
|
+
function startingReplacer() {
|
|
150
|
+
return !/\/(?!$)/.test(this) ? "(?:^|\\/)" : "^";
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
// two globstars
|
|
154
|
+
[
|
|
155
|
+
// Use lookahead assertions so that we could match more than one `'/**'`
|
|
156
|
+
/\\\/\\\*\\\*(?=\\\/|$)/g,
|
|
157
|
+
// Zero, one or several directories
|
|
158
|
+
// should not use '*', or it will be replaced by the next replacer
|
|
159
|
+
// Check if it is not the last `'/**'`
|
|
160
|
+
(_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
|
|
161
|
+
],
|
|
162
|
+
// normal intermediate wildcards
|
|
163
|
+
[
|
|
164
|
+
// Never replace escaped '*'
|
|
165
|
+
// ignore rule '\*' will match the path '*'
|
|
166
|
+
// 'abc.*/' -> go
|
|
167
|
+
// 'abc.*' -> skip this rule,
|
|
168
|
+
// coz trailing single wildcard will be handed by [trailing wildcard]
|
|
169
|
+
/(^|[^\\]+)(\\\*)+(?=.+)/g,
|
|
170
|
+
// '*.js' matches '.js'
|
|
171
|
+
// '*.js' doesn't match 'abc'
|
|
172
|
+
(_, p1, p2) => {
|
|
173
|
+
const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
|
|
174
|
+
return p1 + unescaped;
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
[
|
|
178
|
+
// unescape, revert step 3 except for back slash
|
|
179
|
+
// For example, if a user escape a '\\*',
|
|
180
|
+
// after step 3, the result will be '\\\\\\*'
|
|
181
|
+
/\\\\\\(?=[$.|*+(){^])/g,
|
|
182
|
+
() => ESCAPE
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
// '\\\\' -> '\\'
|
|
186
|
+
/\\\\/g,
|
|
187
|
+
() => ESCAPE
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
// > The range notation, e.g. [a-zA-Z],
|
|
191
|
+
// > can be used to match one of the characters in a range.
|
|
192
|
+
// `\` is escaped by step 3
|
|
193
|
+
/(\\)?\[([^\]/]*?)(\\*)($|\])/g,
|
|
194
|
+
(match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` : close === "]" ? endEscape.length % 2 === 0 ? `[${sanitizeRange(range)}${endEscape}]` : "[]" : "[]"
|
|
195
|
+
],
|
|
196
|
+
// ending
|
|
197
|
+
[
|
|
198
|
+
// 'js' will not match 'js.'
|
|
199
|
+
// 'ab' will not match 'abc'
|
|
200
|
+
/(?:[^*])$/,
|
|
201
|
+
// WTF!
|
|
202
|
+
// https://git-scm.com/docs/gitignore
|
|
203
|
+
// changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
|
|
204
|
+
// which re-fixes #24, #38
|
|
205
|
+
// > If there is a separator at the end of the pattern then the pattern
|
|
206
|
+
// > will only match directories, otherwise the pattern can match both
|
|
207
|
+
// > files and directories.
|
|
208
|
+
// 'js*' will not match 'a.js'
|
|
209
|
+
// 'js/' will not match 'a.js'
|
|
210
|
+
// 'js' will match 'a.js' and 'a.js/'
|
|
211
|
+
(match) => /\/$/.test(match) ? `${match}$` : `${match}(?=$|\\/$)`
|
|
212
|
+
]
|
|
213
|
+
];
|
|
214
|
+
var REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/;
|
|
215
|
+
var MODE_IGNORE = "regex";
|
|
216
|
+
var MODE_CHECK_IGNORE = "checkRegex";
|
|
217
|
+
var UNDERSCORE = "_";
|
|
218
|
+
var TRAILING_WILD_CARD_REPLACERS = {
|
|
219
|
+
[MODE_IGNORE](_, p1) {
|
|
220
|
+
const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
|
|
221
|
+
return `${prefix}(?=$|\\/$)`;
|
|
222
|
+
},
|
|
223
|
+
[MODE_CHECK_IGNORE](_, p1) {
|
|
224
|
+
const prefix = p1 ? `${p1}[^/]*` : "[^/]*";
|
|
225
|
+
return `${prefix}(?=$|\\/$)`;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var makeRegexPrefix = (pattern) => REPLACERS.reduce(
|
|
229
|
+
(prev, [matcher, replacer]) => prev.replace(matcher, replacer.bind(pattern)),
|
|
230
|
+
pattern
|
|
231
|
+
);
|
|
232
|
+
var isString = (subject) => typeof subject === "string";
|
|
233
|
+
var checkPattern = (pattern) => pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) && pattern.indexOf("#") !== 0;
|
|
234
|
+
var splitPattern = (pattern) => pattern.split(REGEX_SPLITALL_CRLF).filter(Boolean);
|
|
235
|
+
var IgnoreRule = class {
|
|
236
|
+
constructor(pattern, mark, body, ignoreCase, negative, prefix) {
|
|
237
|
+
this.pattern = pattern;
|
|
238
|
+
this.mark = mark;
|
|
239
|
+
this.negative = negative;
|
|
240
|
+
define(this, "body", body);
|
|
241
|
+
define(this, "ignoreCase", ignoreCase);
|
|
242
|
+
define(this, "regexPrefix", prefix);
|
|
243
|
+
}
|
|
244
|
+
get regex() {
|
|
245
|
+
const key = UNDERSCORE + MODE_IGNORE;
|
|
246
|
+
if (this[key]) {
|
|
247
|
+
return this[key];
|
|
248
|
+
}
|
|
249
|
+
return this._make(MODE_IGNORE, key);
|
|
250
|
+
}
|
|
251
|
+
get checkRegex() {
|
|
252
|
+
const key = UNDERSCORE + MODE_CHECK_IGNORE;
|
|
253
|
+
if (this[key]) {
|
|
254
|
+
return this[key];
|
|
255
|
+
}
|
|
256
|
+
return this._make(MODE_CHECK_IGNORE, key);
|
|
257
|
+
}
|
|
258
|
+
_make(mode, key) {
|
|
259
|
+
const str = this.regexPrefix.replace(
|
|
260
|
+
REGEX_REPLACE_TRAILING_WILDCARD,
|
|
261
|
+
// It does not need to bind pattern
|
|
262
|
+
TRAILING_WILD_CARD_REPLACERS[mode]
|
|
263
|
+
);
|
|
264
|
+
const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str);
|
|
265
|
+
return define(this, key, regex);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
var createRule = ({
|
|
269
|
+
pattern,
|
|
270
|
+
mark
|
|
271
|
+
}, ignoreCase) => {
|
|
272
|
+
let negative = false;
|
|
273
|
+
let body = pattern;
|
|
274
|
+
if (body.indexOf("!") === 0) {
|
|
275
|
+
negative = true;
|
|
276
|
+
body = body.substr(1);
|
|
277
|
+
}
|
|
278
|
+
body = body.replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!").replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#");
|
|
279
|
+
const regexPrefix = makeRegexPrefix(body);
|
|
280
|
+
return new IgnoreRule(
|
|
281
|
+
pattern,
|
|
282
|
+
mark,
|
|
283
|
+
body,
|
|
284
|
+
ignoreCase,
|
|
285
|
+
negative,
|
|
286
|
+
regexPrefix
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
var RuleManager = class {
|
|
290
|
+
constructor(ignoreCase) {
|
|
291
|
+
this._ignoreCase = ignoreCase;
|
|
292
|
+
this._rules = [];
|
|
293
|
+
}
|
|
294
|
+
_add(pattern) {
|
|
295
|
+
if (pattern && pattern[KEY_IGNORE]) {
|
|
296
|
+
this._rules = this._rules.concat(pattern._rules._rules);
|
|
297
|
+
this._added = true;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (isString(pattern)) {
|
|
301
|
+
pattern = {
|
|
302
|
+
pattern
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (checkPattern(pattern.pattern)) {
|
|
306
|
+
const rule = createRule(pattern, this._ignoreCase);
|
|
307
|
+
this._added = true;
|
|
308
|
+
this._rules.push(rule);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// @param {Array<string> | string | Ignore} pattern
|
|
312
|
+
add(pattern) {
|
|
313
|
+
this._added = false;
|
|
314
|
+
makeArray(
|
|
315
|
+
isString(pattern) ? splitPattern(pattern) : pattern
|
|
316
|
+
).forEach(this._add, this);
|
|
317
|
+
return this._added;
|
|
318
|
+
}
|
|
319
|
+
// Test one single path without recursively checking parent directories
|
|
320
|
+
//
|
|
321
|
+
// - checkUnignored `boolean` whether should check if the path is unignored,
|
|
322
|
+
// setting `checkUnignored` to `false` could reduce additional
|
|
323
|
+
// path matching.
|
|
324
|
+
// - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
|
|
325
|
+
// @returns {TestResult} true if a file is ignored
|
|
326
|
+
test(path14, checkUnignored, mode) {
|
|
327
|
+
let ignored = false;
|
|
328
|
+
let unignored = false;
|
|
329
|
+
let matchedRule;
|
|
330
|
+
this._rules.forEach((rule) => {
|
|
331
|
+
const { negative } = rule;
|
|
332
|
+
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const matched = rule[mode].test(path14);
|
|
336
|
+
if (!matched) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
ignored = !negative;
|
|
340
|
+
unignored = negative;
|
|
341
|
+
matchedRule = negative ? UNDEFINED : rule;
|
|
342
|
+
});
|
|
343
|
+
const ret = {
|
|
344
|
+
ignored,
|
|
345
|
+
unignored
|
|
346
|
+
};
|
|
347
|
+
if (matchedRule) {
|
|
348
|
+
ret.rule = matchedRule;
|
|
349
|
+
}
|
|
350
|
+
return ret;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
var throwError = (message, Ctor) => {
|
|
354
|
+
throw new Ctor(message);
|
|
355
|
+
};
|
|
356
|
+
var checkPath = (path14, originalPath, doThrow) => {
|
|
357
|
+
if (!isString(path14)) {
|
|
358
|
+
return doThrow(
|
|
359
|
+
`path must be a string, but got \`${originalPath}\``,
|
|
360
|
+
TypeError
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (!path14) {
|
|
364
|
+
return doThrow(`path must not be empty`, TypeError);
|
|
365
|
+
}
|
|
366
|
+
if (checkPath.isNotRelative(path14)) {
|
|
367
|
+
const r = "`path.relative()`d";
|
|
368
|
+
return doThrow(
|
|
369
|
+
`path should be a ${r} string, but got "${originalPath}"`,
|
|
370
|
+
RangeError
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return true;
|
|
374
|
+
};
|
|
375
|
+
var isNotRelative = (path14) => REGEX_TEST_INVALID_PATH.test(path14);
|
|
376
|
+
checkPath.isNotRelative = isNotRelative;
|
|
377
|
+
checkPath.convert = (p) => p;
|
|
378
|
+
var Ignore = class {
|
|
379
|
+
constructor({
|
|
380
|
+
ignorecase = true,
|
|
381
|
+
ignoreCase = ignorecase,
|
|
382
|
+
allowRelativePaths = false
|
|
383
|
+
} = {}) {
|
|
384
|
+
define(this, KEY_IGNORE, true);
|
|
385
|
+
this._rules = new RuleManager(ignoreCase);
|
|
386
|
+
this._strictPathCheck = !allowRelativePaths;
|
|
387
|
+
this._initCache();
|
|
388
|
+
}
|
|
389
|
+
_initCache() {
|
|
390
|
+
this._ignoreCache = /* @__PURE__ */ Object.create(null);
|
|
391
|
+
this._testCache = /* @__PURE__ */ Object.create(null);
|
|
392
|
+
}
|
|
393
|
+
add(pattern) {
|
|
394
|
+
if (this._rules.add(pattern)) {
|
|
395
|
+
this._initCache();
|
|
396
|
+
}
|
|
397
|
+
return this;
|
|
398
|
+
}
|
|
399
|
+
// legacy
|
|
400
|
+
addPattern(pattern) {
|
|
401
|
+
return this.add(pattern);
|
|
402
|
+
}
|
|
403
|
+
// @returns {TestResult}
|
|
404
|
+
_test(originalPath, cache, checkUnignored, slices) {
|
|
405
|
+
const path14 = originalPath && checkPath.convert(originalPath);
|
|
406
|
+
checkPath(
|
|
407
|
+
path14,
|
|
408
|
+
originalPath,
|
|
409
|
+
this._strictPathCheck ? throwError : RETURN_FALSE
|
|
410
|
+
);
|
|
411
|
+
return this._t(path14, cache, checkUnignored, slices);
|
|
412
|
+
}
|
|
413
|
+
checkIgnore(path14) {
|
|
414
|
+
if (!REGEX_TEST_TRAILING_SLASH.test(path14)) {
|
|
415
|
+
return this.test(path14);
|
|
416
|
+
}
|
|
417
|
+
const slices = path14.split(SLASH).filter(Boolean);
|
|
418
|
+
slices.pop();
|
|
419
|
+
if (slices.length) {
|
|
420
|
+
const parent = this._t(
|
|
421
|
+
slices.join(SLASH) + SLASH,
|
|
422
|
+
this._testCache,
|
|
423
|
+
true,
|
|
424
|
+
slices
|
|
425
|
+
);
|
|
426
|
+
if (parent.ignored) {
|
|
427
|
+
return parent;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return this._rules.test(path14, false, MODE_CHECK_IGNORE);
|
|
431
|
+
}
|
|
432
|
+
_t(path14, cache, checkUnignored, slices) {
|
|
433
|
+
if (path14 in cache) {
|
|
434
|
+
return cache[path14];
|
|
435
|
+
}
|
|
436
|
+
if (!slices) {
|
|
437
|
+
slices = path14.split(SLASH).filter(Boolean);
|
|
438
|
+
}
|
|
439
|
+
slices.pop();
|
|
440
|
+
if (!slices.length) {
|
|
441
|
+
return cache[path14] = this._rules.test(path14, checkUnignored, MODE_IGNORE);
|
|
442
|
+
}
|
|
443
|
+
const parent = this._t(
|
|
444
|
+
slices.join(SLASH) + SLASH,
|
|
445
|
+
cache,
|
|
446
|
+
checkUnignored,
|
|
447
|
+
slices
|
|
448
|
+
);
|
|
449
|
+
return cache[path14] = parent.ignored ? parent : this._rules.test(path14, checkUnignored, MODE_IGNORE);
|
|
450
|
+
}
|
|
451
|
+
ignores(path14) {
|
|
452
|
+
return this._test(path14, this._ignoreCache, false).ignored;
|
|
453
|
+
}
|
|
454
|
+
createFilter() {
|
|
455
|
+
return (path14) => !this.ignores(path14);
|
|
456
|
+
}
|
|
457
|
+
filter(paths) {
|
|
458
|
+
return makeArray(paths).filter(this.createFilter());
|
|
459
|
+
}
|
|
460
|
+
// @returns {TestResult}
|
|
461
|
+
test(path14) {
|
|
462
|
+
return this._test(path14, this._testCache, true);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
var factory = (options) => new Ignore(options);
|
|
466
|
+
var isPathValid = (path14) => checkPath(path14 && checkPath.convert(path14), path14, RETURN_FALSE);
|
|
467
|
+
var setupWindows = () => {
|
|
468
|
+
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
|
|
469
|
+
checkPath.convert = makePosix;
|
|
470
|
+
const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
471
|
+
checkPath.isNotRelative = (path14) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path14) || isNotRelative(path14);
|
|
472
|
+
};
|
|
473
|
+
if (
|
|
474
|
+
// Detect `process` so that it can run in browsers.
|
|
475
|
+
typeof process !== "undefined" && process.platform === "win32"
|
|
476
|
+
) {
|
|
477
|
+
setupWindows();
|
|
478
|
+
}
|
|
479
|
+
module.exports = factory;
|
|
480
|
+
factory.default = factory;
|
|
481
|
+
module.exports.isPathValid = isPathValid;
|
|
482
|
+
define(module.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
2
485
|
|
|
3
486
|
// src/index.ts
|
|
4
|
-
import { Command } from "commander";
|
|
487
|
+
import { Command, Help } from "commander";
|
|
488
|
+
import chalk11 from "chalk";
|
|
489
|
+
import inquirer5 from "inquirer";
|
|
5
490
|
|
|
6
|
-
// src/
|
|
7
|
-
|
|
8
|
-
|
|
491
|
+
// src/lib/nlp-router.ts
|
|
492
|
+
function normalize(input) {
|
|
493
|
+
return input.trim().toLowerCase().replace(/\s+/g, " ");
|
|
494
|
+
}
|
|
495
|
+
var RULES = [
|
|
496
|
+
{
|
|
497
|
+
command: "init",
|
|
498
|
+
explanation: "\uAC80\uC99D \uC2A4\uD0B5\uD558\uACE0 \uBC14\uB85C \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (vhk \uC2DC\uC791 --skip-gate)",
|
|
499
|
+
confidence: "high",
|
|
500
|
+
args: ["--skip-gate"],
|
|
501
|
+
test: (t) => /기획.*(끝|완료)|노션.*(기획|완료)|검증.*(스킵|건너)|gate.*(스킵|건너)|바로.*시작/.test(t)
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
command: "init",
|
|
505
|
+
explanation: "Notion\uC5D0\uC11C \uAC00\uC838\uC640 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (vhk \uC2DC\uC791 --from-notion)",
|
|
506
|
+
confidence: "low",
|
|
507
|
+
args: ["--from-notion"],
|
|
508
|
+
test: (t) => /노션|notion/.test(t) && /(시작|만들|import|가져)/.test(t)
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
command: "init",
|
|
512
|
+
explanation: "\uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (vhk \uC2DC\uC791)",
|
|
513
|
+
confidence: "high",
|
|
514
|
+
test: (t) => /프로젝트.*(만들|시작)|폴더.*만들|만들고\s*싶|하네스|초기화/.test(t) || /^시작$/.test(t)
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
command: "recap",
|
|
518
|
+
explanation: "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC (vhk \uC815\uB9AC)",
|
|
519
|
+
confidence: "high",
|
|
520
|
+
test: (t) => /오늘.*(정리|기록)|한\s*일|세션|회고|recap|정리해/.test(t)
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
command: "doctor",
|
|
524
|
+
explanation: "\uD658\uACBD \uC810\uAC80 (vhk doctor)",
|
|
525
|
+
confidence: "high",
|
|
526
|
+
test: (t) => /뭔가\s*안|안\s*돼|안돼|환경\s*(점검|진단)|진단|doctor|설치.*확인|왜\s*안/.test(t)
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
command: "gate",
|
|
530
|
+
explanation: "\uC544\uC774\uB514\uC5B4 \uAC80\uC99D (vhk \uAC80\uC99D)",
|
|
531
|
+
confidence: "high",
|
|
532
|
+
test: (t) => /아이디어|검증|gate|go\/refine|pain\s*point/.test(t)
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
command: "secure",
|
|
536
|
+
explanation: "\uBCF4\uC548 \uC2A4\uCE94 (vhk \uBCF4\uC548 scan)",
|
|
537
|
+
confidence: "high",
|
|
538
|
+
test: (t) => /보안|시크릿|비밀|키\s*유출|secure|scan/.test(t)
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
command: "check",
|
|
542
|
+
explanation: "\uADDC\uCE59 \uC810\uAC80 (vhk \uC810\uAC80)",
|
|
543
|
+
confidence: "high",
|
|
544
|
+
test: (t) => /규칙.*(점검|위반)|린트|check|위반/.test(t)
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
command: "sync",
|
|
548
|
+
explanation: "\uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654 (vhk \uADDC\uCE59)",
|
|
549
|
+
confidence: "high",
|
|
550
|
+
test: (t) => /규칙.*(맞|동기)|sync|cursorrules|claude\.md.*맞/.test(t)
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
command: "ship",
|
|
554
|
+
explanation: "\uBC30\uD3EC \uCCB4\uD06C + \uD68C\uACE0 (vhk ship)",
|
|
555
|
+
confidence: "high",
|
|
556
|
+
test: (t) => /배포|출시|릴리스|ship|빌드\s*전/.test(t)
|
|
557
|
+
}
|
|
558
|
+
];
|
|
559
|
+
function routeNaturalLanguage(input) {
|
|
560
|
+
const normalized = normalize(input);
|
|
561
|
+
if (!normalized) return null;
|
|
562
|
+
for (const rule of RULES) {
|
|
563
|
+
if (rule.test(normalized)) {
|
|
564
|
+
return {
|
|
565
|
+
command: rule.command,
|
|
566
|
+
explanation: rule.explanation,
|
|
567
|
+
confidence: rule.confidence,
|
|
568
|
+
args: rule.args
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
function extractNotionUrl(input) {
|
|
575
|
+
const m = input.match(/https?:\/\/[^\s]+/i);
|
|
576
|
+
return m?.[0];
|
|
577
|
+
}
|
|
9
578
|
|
|
10
579
|
// src/i18n/ko.ts
|
|
11
580
|
var ko = {
|
|
581
|
+
start: {
|
|
582
|
+
title: "\u{1F527} VHK \u2014 \uBB34\uC5C7\uC744 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
|
|
583
|
+
subtitle: "\uBC88\uD638\uB9CC \uACE0\uB974\uBA74 \uB429\uB2C8\uB2E4. \uBA85\uB839\uC5B4\uB97C \uC678\uC6B8 \uD544\uC694 \uC5C6\uC5B4\uC694.",
|
|
584
|
+
menuPrompt: "\uC5B4\uB5A4 \uC791\uC5C5\uC744 \uD560\uAE4C\uC694?",
|
|
585
|
+
choiceGate: "1) \uC0C8 \uC544\uC774\uB514\uC5B4 \uAC80\uC99D\uD558\uAE30",
|
|
586
|
+
choiceInit: "2) \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791\uD558\uAE30 \u2014 \uD3F4\uB354\xB7\uD30C\uC77C \uB9CC\uB4E4\uAE30",
|
|
587
|
+
choiceInitSkipGate: "3) \uAE30\uD68D\uC740 \uB05D\uB0AC\uC5B4\uC694 \u2014 \uBC14\uB85C \uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uAE30",
|
|
588
|
+
choiceRecap: "4) \uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD558\uAE30",
|
|
589
|
+
choiceSync: "5) \uADDC\uCE59 \uD30C\uC77C \uB9DE\uCD94\uAE30",
|
|
590
|
+
choiceCheck: "6) \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59 \uC810\uAC80\uD558\uAE30",
|
|
591
|
+
choiceSecure: "7) \uBE44\uBC00\uBC88\uD638\xB7\uD0A4 \uC720\uCD9C \uAC80\uC0AC\uD558\uAE30",
|
|
592
|
+
choiceExit: "\uB098\uAC00\uAE30",
|
|
593
|
+
goodbye: "\uB2E4\uC74C\uC5D0 \uB610 \uBD88\uB7EC\uC8FC\uC138\uC694."
|
|
594
|
+
},
|
|
12
595
|
gate: {
|
|
13
|
-
title: "\u{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
596
|
+
title: "\u{1F4A1} \uC544\uC774\uB514\uC5B4 \uAC80\uC99D",
|
|
597
|
+
welcome: "\uC0C8 \uC544\uC774\uB514\uC5B4\uB97C \uAC80\uC99D\uD569\uB2C8\uB2E4. \uC9C8\uBB38\uC5D0 \uB2F5\uD574\uC8FC\uC138\uC694.",
|
|
598
|
+
modePrompt: "\uC5B4\uB5BB\uAC8C \uAC80\uC99D\uD560\uAE4C\uC694?",
|
|
599
|
+
modeQuickLabel: "\u26A1 \uC9E7\uAC8C (\uD575\uC2EC 5\uBB38\uD56D) \u2014 \uB9C9 \uB5A0\uC62C\uB790\uC744 \uB54C",
|
|
600
|
+
modeFullLabel: "\u{1F50D} \uC790\uC138\uD788 (13\uBB38\uD56D) \u2014 \uAE30\uD68D\uC774 \uC5B4\uB290 \uC815\uB3C4 \uC7A1\uD614\uC744 \uB54C",
|
|
601
|
+
modeSkipLabel: "\u23ED\uFE0F \uAC74\uB108\uB6F0\uAE30 \u2014 \uB178\uC158\xB7\uBB38\uC11C\uC5D0 \uC774\uBBF8 \uAE30\uD68D\uD574 \uB460",
|
|
602
|
+
skipSourcePrompt: "\u{1F4C4} \uAE30\uD68D \uBB38\uC11C \uC704\uCE58 (\uB178\uC158 \uC8FC\uC18C, \uD30C\uC77C \uACBD\uB85C \uB4F1):",
|
|
603
|
+
skipGo: "\u2705 \uC2DC\uC791\uD574\uB3C4 \uB3FC\uC694! \uC774\uC81C \uD504\uB85C\uC81D\uD2B8\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694 (vhk init)",
|
|
20
604
|
skipSourceLabel: (source) => `\uAE30\uD68D \uBB38\uC11C: ${source}`,
|
|
21
|
-
quickHeader: "\u26A1 \
|
|
22
|
-
fullHeader: "\u{1F50D} \
|
|
605
|
+
quickHeader: "\u26A1 \uC9E7\uC740 \uAC80\uC99D",
|
|
606
|
+
fullHeader: "\u{1F50D} \uC790\uC138\uD55C \uAC80\uC99D",
|
|
23
607
|
modeCountSuffix: (total) => `\u2014 ${total}\uBB38\uD56D`,
|
|
24
|
-
idea: "\u{1F4A1} \
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
608
|
+
idea: "\u{1F4A1} \uC5B4\uB5A4 \uAC78 \uB9CC\uB4E4 \uAC74\uAC00\uC694? (\uD55C \uC904)",
|
|
609
|
+
ideaHint: '\uC608: "\uD300 \uD560 \uC77C\uC744 3\uCD08\uC5D0 \uCD94\uAC00\uD558\uB294 \uC571"',
|
|
610
|
+
painPoint: "\u{1F624} \uC774 \uBB38\uC81C, \uB204\uAC00 \uC5BC\uB9C8\uB098 \uC544\uD30C\uD574\uC694?",
|
|
611
|
+
painPointHint: '\uC608: "\uB9E4\uC77C \uC5D1\uC140\uC5D0 \uBCF5\uBD99\uD558\uB290\uB77C 30\uBD84\uC529 \uB0A0\uB9BC"',
|
|
612
|
+
edge: "\u{1F4AA} \uB098\uB9CC\uC758 \uAC15\uC810\uC740? (\uBE44\uC2B7\uD55C \uAC8C \uC788\uB294\uB370 \uC65C \uC774\uAC78?)",
|
|
613
|
+
edgeHint: '\uC608: "\uD55C\uAD6D\uC5B4\uB85C \uB41C \uAC00\uC774\uB4DC + \uBC14\uB85C \uC4F0\uB294 \uD15C\uD50C\uB9BF"',
|
|
614
|
+
checklistStart: "\u2500\u2500\u2500 \uC774\uC5B4\uC11C \uC9C8\uBB38\uD569\uB2C8\uB2E4 \u2500\u2500\u2500",
|
|
28
615
|
hintPrefix: " \u{1F4A1}",
|
|
29
|
-
verdictPrompt: (
|
|
30
|
-
statusPassChoice: "\u2705 \
|
|
31
|
-
statusHoldChoice: "\u{1F7E1} \uC544\uC9C1 \uBAA8\uB974\uACA0\
|
|
32
|
-
statusFailChoice: "\u{
|
|
33
|
-
statusPassLine: " \u2705 \
|
|
34
|
-
statusHoldLine: " \u{1F7E1} \uBCF4\uB958",
|
|
35
|
-
statusFailLine: " \u{
|
|
36
|
-
verdictTitle: "\u2550\u2550\u2550 \
|
|
37
|
-
ideaLabel: "\
|
|
38
|
-
painPointLabel: "
|
|
39
|
-
edgeLabel: "
|
|
40
|
-
countLine: (failCount, holdCount, total) => `\
|
|
41
|
-
go: "\
|
|
42
|
-
refine: "\u{
|
|
43
|
-
drop: "\u{
|
|
44
|
-
nextCommand: "\uB2E4\uC74C \
|
|
45
|
-
holdRemainHint: "\u{1F4A1} \uBCF4\uB958 \uD56D\uBAA9\uC740 \uAC1C\uBC1C\uD558\uBA74\uC11C \uCC44\uC6CC\
|
|
616
|
+
verdictPrompt: (_failIf) => " \u2192 \uC9C0\uAE08 \uC0C1\uD0DC\uB294?",
|
|
617
|
+
statusPassChoice: "\u2705 \uAD1C\uCC2E\uC544\uC694",
|
|
618
|
+
statusHoldChoice: "\u{1F7E1} \uC544\uC9C1 \uBAA8\uB974\uACA0\uC5B4\uC694 (\uB098\uC911\uC5D0 \uCC44\uC6CC\uB3C4 \uB429\uB2C8\uB2E4)",
|
|
619
|
+
statusFailChoice: "\u{1F504} \uBC94\uC704\uB97C \uC904\uC5EC\uBCFC\uAC8C\uC694",
|
|
620
|
+
statusPassLine: " \u2705 \uAD1C\uCC2E\uC544\uC694",
|
|
621
|
+
statusHoldLine: " \u{1F7E1} \uBCF4\uB958 \u2014 \uAC1C\uBC1C\uD558\uBA74\uC11C \uCC44\uC6CC\uB3C4 \uB429\uB2C8\uB2E4",
|
|
622
|
+
statusFailLine: " \u{1F504} \uBC94\uC704 \uC870\uC815\uC774 \uD544\uC694\uD574 \uBCF4\uC5EC\uC694",
|
|
623
|
+
verdictTitle: "\u2550\u2550\u2550 \uACB0\uACFC \u2550\u2550\u2550",
|
|
624
|
+
ideaLabel: "\uB9CC\uB4E4 \uAC83:",
|
|
625
|
+
painPointLabel: "\uC544\uD508 \uC810:",
|
|
626
|
+
edgeLabel: "\uB098\uB9CC\uC758 \uAC15\uC810:",
|
|
627
|
+
countLine: (failCount, holdCount, total) => `\uBC94\uC704 \uC870\uC815 ${failCount}\uAC1C \xB7 \uBCF4\uB958 ${holdCount}\uAC1C / ${total}\uBB38\uD56D`,
|
|
628
|
+
go: "\u2705 \uC2DC\uC791\uD574\uB3C4 \uB3FC\uC694! \uB2E4\uC74C \uB2E8\uACC4(\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uAE30)\uB85C \uB118\uC5B4\uAC00\uC138\uC694.",
|
|
629
|
+
refine: "\u{1F504} \uC870\uAE08 \uB354 \uB2E4\uB4EC\uC73C\uBA74 \uC88B\uACA0\uC5B4\uC694. \uC704 \uD56D\uBAA9\uC744 \uBCF4\uC644\uD574 \uBCF4\uC138\uC694.",
|
|
630
|
+
drop: "\u{1F4A1} \uB2E4\uB978 \uC544\uC774\uB514\uC5B4\uB97C \uAC80\uD1A0\uD574 \uBCF4\uB294 \uAC74 \uC5B4\uB5A8\uAE4C\uC694?",
|
|
631
|
+
nextCommand: "\uB2E4\uC74C: vhk init (\uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791\uD558\uAE30)",
|
|
632
|
+
holdRemainHint: "\u{1F4A1} \uBCF4\uB958\uD55C \uD56D\uBAA9\uC740 \uAC1C\uBC1C\uD558\uBA74\uC11C \uCC44\uC6CC\uB3C4 \uAD1C\uCC2E\uC544\uC694.",
|
|
633
|
+
failMessage: "\uC544\uC9C1 \uBAA8\uB974\uACA0\uC5B4\uC694 \u2192 \uAD1C\uCC2E\uC544\uC694, \uAC1C\uBC1C\uD558\uBA74\uC11C \uCC44\uC6CC\uB3C4 \uB429\uB2C8\uB2E4."
|
|
46
634
|
},
|
|
47
635
|
init: {
|
|
48
|
-
title: "\u{1F6E0}\uFE0F
|
|
49
|
-
skipGate: "\u23ED\uFE0F
|
|
50
|
-
projectName: "\u{1F4E6} \uD504\uB85C\uC81D\uD2B8 \uC774\uB984
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
636
|
+
title: "\u{1F6E0}\uFE0F \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791\uD558\uAE30",
|
|
637
|
+
skipGate: "\u23ED\uFE0F 1\uB2E8\uACC4(\uC544\uC774\uB514\uC5B4 \uAC80\uC99D) \uAC74\uB108\uB6F0\uAE30 \u2014 \uAE30\uD68D\xB7\uC124\uACC4\uAC00 \uC774\uBBF8 \uC788\uC5B4\uC694",
|
|
638
|
+
projectName: "\u{1F4E6} \uD504\uB85C\uC81D\uD2B8 \uC774\uB984\uC740?",
|
|
639
|
+
projectNameHint: '\uC608: "\uD300 \uD560 \uC77C \uC571"',
|
|
640
|
+
description: "\u{1F4DD} \uD55C \uC904\uB85C \uC124\uBA85\uD558\uBA74?",
|
|
641
|
+
descriptionHint: '\uC608: "3\uCD08 \uB9CC\uC5D0 \uD560 \uC77C \uCD94\uAC00"',
|
|
642
|
+
projectType: "\u{1F3D7}\uFE0F \uC5B4\uB5A4 \uC885\uB958\uC778\uAC00\uC694?",
|
|
643
|
+
confirmStack: "\uC774 \uAE30\uC220 \uBB36\uC74C\uC73C\uB85C \uC9C4\uD589\uD560\uAE4C\uC694?",
|
|
644
|
+
canceled: "\uCDE8\uC18C\uD588\uC5B4\uC694. \uAE30\uC220 \uBB36\uC74C\uC744 \uBC14\uAFB8\uB824\uBA74 \uB2E4\uC2DC vhk init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
|
|
645
|
+
recommendedStack: "\uCD94\uCC9C \uAE30\uC220 \uBB36\uC74C:",
|
|
646
|
+
filesGenerating: "\u{1F4C2} \uD544\uC694\uD55C \uD30C\uC77C \uB9CC\uB4DC\uB294 \uC911...",
|
|
647
|
+
overwrite: (filePath) => ` \u26A0\uFE0F ${filePath} \uD30C\uC77C\uC774 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?`,
|
|
648
|
+
skipped: (filePath) => `${filePath} \u2014 \uAC74\uB108\uB700`,
|
|
649
|
+
done: "\u{1F389} \uD504\uB85C\uC81D\uD2B8 \uBF08\uB300\uAC00 \uC900\uBE44\uB410\uC5B4\uC694!",
|
|
650
|
+
nextSteps: "\uB2E4\uC74C\uC5D0 \uD560 \uC77C:",
|
|
651
|
+
fillHint: "CLAUDE.md \xB7 .cursorrules\uC5D0\uC11C \u{1F449} \uC5EC\uAE30\uB97C \uCC44\uC6CC\uC8FC\uC138\uC694 \uD45C\uC2DC\uB97C \uCC3E\uC544 \uCC44\uC6B0\uC138\uC694",
|
|
652
|
+
prdHint: "docs/PRD.md\uC5D0 1\uCC28 \uBC84\uC804\uC5D0 \uB123\uC744 \uAE30\uB2A5\xB7\uBE7C\uB294 \uAE30\uB2A5\uC744 \uC801\uC5B4 \uBCF4\uC138\uC694",
|
|
653
|
+
notionFetching: "\u{1F4E1} \uB178\uC158 \uAE30\uD68D \uD398\uC774\uC9C0 \uBD88\uB7EC\uC624\uB294 \uC911...",
|
|
654
|
+
notionDone: (name) => `\uB178\uC158\uC5D0\uC11C \uAC00\uC838\uC624\uAE30 \uC644\uB8CC: ${name}`,
|
|
655
|
+
notionReviewHint: "docs/PRD.md\uB97C \uC77D\uACE0 \u{1F449} \uC5EC\uAE30\uB97C \uCC44\uC6CC\uC8FC\uC138\uC694 \uD56D\uBAA9\uC744 \uCC44\uC6B0\uC138\uC694",
|
|
656
|
+
gitHintLabel: "\uD130\uBBF8\uB110\uC5D0 \uBCF5\uC0AC\uD560 \uBA85\uB839 (\uC544\uB798 \uBC15\uC2A4 \uBCF5\uBD99):",
|
|
657
|
+
gitHintCommand: 'git init && git add . && git commit -m "feat: \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791"',
|
|
658
|
+
startDev: "\uC774\uC81C \uAC1C\uBC1C\uD574 \uBCF4\uC138\uC694! \u{1F680}",
|
|
659
|
+
commandsMdDone: "\u{1F4CB} COMMANDS.md \uC0DD\uC131",
|
|
660
|
+
scriptsDone: "\u{1F4E6} package.json scripts \uCD94\uAC00"
|
|
68
661
|
},
|
|
69
662
|
recap: {
|
|
70
|
-
title: "\u{1F4DD}
|
|
71
|
-
analyzing: "\u{1F4CA}
|
|
72
|
-
noRepo: "\u274C Git \
|
|
73
|
-
noChanges: "\u26A0\uFE0F \
|
|
74
|
-
summary: "\u{1F4DD} \uC774\uBC88
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
663
|
+
title: "\u{1F4DD} \uC624\uB298 \uD55C \uC77C \uC815\uB9AC",
|
|
664
|
+
analyzing: "\u{1F4CA} \uC624\uB298 \uBC14\uB010 \uD30C\uC77C\xB7\uCEE4\uBC0B\uC744 \uC0B4\uD3B4\uBCF4\uB294 \uC911...",
|
|
665
|
+
noRepo: "\u274C Git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2C8\uC5D0\uC694. \uBA3C\uC800 git init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
|
|
666
|
+
noChanges: "\u26A0\uFE0F \uC624\uB298 \uBC14\uB010 \uB0B4\uC6A9\uC774 \uC5C6\uC5B4\uC694.",
|
|
667
|
+
summary: "\u{1F4DD} \uC774\uBC88\uC5D0 \uBB58 \uD588\uB098\uC694? (1~3\uC904)",
|
|
668
|
+
summaryHint: '\uC608: "\uB85C\uADF8\uC778 \uD654\uBA74 \uB9CC\uB4E4\uACE0 \uBC84\uD2BC \uC0C9 \uACE0\uCE68"',
|
|
669
|
+
decisions: "\u{1F9ED} \uC815\uD55C \uACB0\uC815\uC774 \uC788\uB098\uC694? (\uC5C6\uC73C\uBA74 Enter)",
|
|
670
|
+
nextTodo: "\u23ED\uFE0F \uB2E4\uC74C\uC5D0 \uD560 \uC77C\uC740?",
|
|
671
|
+
blockers: "\u{1F6A7} \uB9C9\uD78C \uAC8C \uC788\uB098\uC694? (\uC5C6\uC73C\uBA74 Enter)",
|
|
672
|
+
done: "\u2705 \uC624\uB298 \uAE30\uB85D\uC744 \uC800\uC7A5\uD588\uC5B4\uC694!",
|
|
673
|
+
updateClaude: 'CLAUDE.md "\uC9C0\uAE08 \uC0C1\uD0DC"\uB3C4 \uAC19\uC774 \uACE0\uCE60\uAE4C\uC694?',
|
|
674
|
+
adrDetected: "\u{1F4D0} \uC4F0\uB294 \uAE30\uC220\xB7\uC124\uC815\uC774 \uBC14\uB010 \uAC83 \uAC19\uC544\uC694!",
|
|
675
|
+
createAdr: "\uC65C \uADF8\uB807\uAC8C \uD588\uB294\uC9C0 \uAE30\uB85D \uBB38\uC11C\uB97C \uB9CC\uB4E4\uAE4C\uC694?",
|
|
676
|
+
troubleDetected: "\u{1F527} \uBC84\uADF8\xB7\uC624\uB958\uB97C \uACE0\uCE5C \uCEE4\uBC0B\uC774 \uBCF4\uC5EC\uC694!",
|
|
677
|
+
createTroubleshoot: "\uC5B4\uB5BB\uAC8C \uACE0\uCCE4\uB294\uC9C0 \uBA54\uBAA8\uB97C \uB0A8\uAE38\uAE4C\uC694?"
|
|
84
678
|
},
|
|
85
679
|
check: {
|
|
86
|
-
title: "\u{1F50D}
|
|
87
|
-
noRules: "\u26A0\uFE0F RULES.md\
|
|
88
|
-
noAutoRules: "\u26A0\uFE0F \uC790\uB3D9
|
|
89
|
-
allPassed: "\u{1F389} \
|
|
90
|
-
summary: "\u{1F4CA} \
|
|
680
|
+
title: "\u{1F50D} \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59 \uC810\uAC80",
|
|
681
|
+
noRules: "\u26A0\uFE0F RULES.md \uD30C\uC77C\uC774 \uC5C6\uC5B4\uC694.",
|
|
682
|
+
noAutoRules: "\u26A0\uFE0F \uC790\uB3D9\uC73C\uB85C \uAC80\uC0AC\uD560 \uADDC\uCE59\uC774 \uC5C6\uC5B4\uC694.",
|
|
683
|
+
allPassed: "\u{1F389} \uADDC\uCE59\uC744 \uBAA8\uB450 \uC9C0\uCF30\uC5B4\uC694!",
|
|
684
|
+
summary: "\u{1F4CA} \uC810\uAC80 \uACB0\uACFC:"
|
|
685
|
+
},
|
|
686
|
+
doctor: {
|
|
687
|
+
title: "\u{1FA7A} \uAC1C\uBC1C \uD658\uACBD \uC810\uAC80",
|
|
688
|
+
allOk: "\u{1F389} \uAC1C\uBC1C \uD658\uACBD \uC900\uBE44 \uC644\uB8CC!",
|
|
689
|
+
missing: "\u26A0\uFE0F \uC77C\uBD80 \uB3C4\uAD6C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
690
|
+
missingHint: "\uC704 \uC548\uB0B4\uB97C \uB530\uB77C \uC124\uCE58\uD558\uC138\uC694.",
|
|
691
|
+
projectFiles: "\u{1F4C1} \uD504\uB85C\uC81D\uD2B8 \uD30C\uC77C \uD655\uC778:",
|
|
692
|
+
envNotIgnored: "\u26A0\uFE0F .env\uAC00 .gitignore\uC5D0 \uC5C6\uC74C! \uCD94\uAC00\uD558\uC138\uC694",
|
|
693
|
+
nextOkMessage: "\uD658\uACBD \uC810\uAC80 \uD1B5\uACFC! \uC774\uC81C \uD504\uB85C\uC81D\uD2B8\uB97C \uC2DC\uC791\uD558\uC138\uC694.",
|
|
694
|
+
nextRetryMessage: "\uC704 \uB3C4\uAD6C\uB97C \uC124\uCE58\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694."
|
|
695
|
+
},
|
|
696
|
+
nlp: {
|
|
697
|
+
matched: "\uC774\uAC8C \uB9DE\uB098\uC694?",
|
|
698
|
+
notMatched: "\uBB34\uC2A8 \uB73B\uC778\uC9C0 \uBAA8\uB974\uACA0\uC5B4\uC694. vhk\uB97C \uC785\uB825\uD558\uBA74 \uBA54\uB274\uC5D0\uC11C \uC120\uD0DD\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
699
|
+
menuHint: "vhk\uB97C \uC785\uB825\uD558\uBA74 \uBA54\uB274\uC5D0\uC11C \uC120\uD0DD\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
|
|
91
700
|
},
|
|
92
701
|
secure: {
|
|
93
|
-
title: "\u{1F512}
|
|
94
|
-
noGitignore: "\u26A0\uFE0F .gitignore\
|
|
95
|
-
noEnvInGitignore: "\u26A0\uFE0F .gitignore\uC5D0 .env\uAC00 \uC5C6\
|
|
96
|
-
scanning: "\u{1F50D} \uD30C\uC77C \
|
|
97
|
-
clean: "\u{1F389} \
|
|
98
|
-
summary: "\u{1F4CA} \
|
|
702
|
+
title: "\u{1F512} \uBE44\uBC00\uBC88\uD638\xB7\uD0A4 \uC720\uCD9C \uAC80\uC0AC",
|
|
703
|
+
noGitignore: "\u26A0\uFE0F .gitignore \uD30C\uC77C\uC774 \uC5C6\uC5B4\uC694!",
|
|
704
|
+
noEnvInGitignore: "\u26A0\uFE0F .gitignore\uC5D0 .env\uAC00 \uC5C6\uC5B4\uC694!",
|
|
705
|
+
scanning: "\u{1F50D} \uD30C\uC77C\uC744 \uC0B4\uD3B4\uBCF4\uB294 \uC911...",
|
|
706
|
+
clean: "\u{1F389} \uBE44\uBC00\uBC88\uD638\xB7\uD0A4\uAC00 \uCF54\uB4DC\uC5D0 \uBCF4\uC774\uC9C0 \uC54A\uC544\uC694!",
|
|
707
|
+
summary: "\u{1F4CA} \uAC80\uC0AC \uC694\uC57D:"
|
|
99
708
|
},
|
|
100
709
|
sync: {
|
|
101
|
-
title: "\u{1F504}
|
|
102
|
-
noRules: "\u26A0\uFE0F RULES.md\
|
|
103
|
-
cursorrulesDone: "\u2705 .cursorrules \
|
|
104
|
-
claudeDone: "\u2705 CLAUDE.md \
|
|
105
|
-
done: "\u{1F504} \
|
|
710
|
+
title: "\u{1F504} \uADDC\uCE59 \uD30C\uC77C \uB9DE\uCD94\uAE30",
|
|
711
|
+
noRules: "\u26A0\uFE0F RULES.md \uD30C\uC77C\uC774 \uC5C6\uC5B4\uC694.",
|
|
712
|
+
cursorrulesDone: "\u2705 .cursorrules \uB9DE\uCDA4 \uC644\uB8CC",
|
|
713
|
+
claudeDone: "\u2705 CLAUDE.md \uB9DE\uCDA4 \uC644\uB8CC",
|
|
714
|
+
done: "\u{1F504} \uB9DE\uCD94\uAE30 \uC644\uB8CC!"
|
|
715
|
+
},
|
|
716
|
+
ship: {
|
|
717
|
+
title: "\u{1F680} \uBC30\uD3EC \uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
|
|
718
|
+
checklist: "\u{1F4CB} \uBC30\uD3EC \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
|
|
719
|
+
retro: "\u{1F50D} \uBC30\uD3EC \uD68C\uACE0",
|
|
720
|
+
buildLogCreated: "\u2705 \uBE4C\uB4DC \uB85C\uADF8 \uC0DD\uC131 \uC644\uB8CC",
|
|
721
|
+
buildLogDone: (rel) => `\u2705 \uBE4C\uB4DC \uB85C\uADF8 \uC0DD\uC131 \uC644\uB8CC: ${rel}`,
|
|
722
|
+
questionWell: "\uC798\uB41C \uC810\uC740?",
|
|
723
|
+
questionWrong: "\uC5B4\uB824\uC6E0\uB358 \uC810\uC740?",
|
|
724
|
+
questionLearned: "\uBC30\uC6B4 \uC810\uC740?",
|
|
725
|
+
questionNext: "\uB2E4\uC74C \uBC84\uC804\uC5D0\uC11C \uD560 \uAC83\uC740?",
|
|
726
|
+
checkboxPrompt: "\uC644\uB8CC\uD55C \uD56D\uBAA9\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
|
|
727
|
+
incompleteHeader: "\u26A0\uFE0F \uC544\uC9C1 \uC644\uB8CC\uD558\uC9C0 \uC54A\uC740 \uD56D\uBAA9:",
|
|
728
|
+
proceedConfirm: "\uADF8\uB798\uB3C4 \uACC4\uC18D \uC9C4\uD589\uD560\uAE4C\uC694?",
|
|
729
|
+
allPassed: "\u2705 \uBAA8\uB4E0 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uD1B5\uACFC!",
|
|
730
|
+
retryMessage: "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uB97C \uB9C8\uCE5C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uBCF4\uC138\uC694.",
|
|
731
|
+
retryCursorHint: "\uBE4C\uB4DC\uD558\uACE0 \uD14C\uC2A4\uD2B8 \uB3CC\uB824\uC918",
|
|
732
|
+
versionPrompt: "\uBC30\uD3EC \uBC84\uC804\uC740?",
|
|
733
|
+
versionHint: "\uC608: 0.4.0",
|
|
734
|
+
emptySection: "(\uBBF8\uC791\uC131)",
|
|
735
|
+
emptyNext: "(\uBBF8\uC815)",
|
|
736
|
+
deployMessage: "\uBE4C\uB4DC \uB85C\uADF8\uB97C \uC800\uC7A5\uD588\uC5B4\uC694! \uC774\uC81C \uC2E4\uC81C \uBC30\uD3EC\uB97C \uC9C4\uD589\uD558\uC138\uC694.",
|
|
737
|
+
deployCursorHint: "\uBC30\uD3EC\uD574\uC918",
|
|
738
|
+
checkBuild: "\uBE4C\uB4DC\uAC00 \uC131\uACF5\uD588\uB098\uC694?",
|
|
739
|
+
hintBuild: "pnpm build",
|
|
740
|
+
checkTest: "\uBAA8\uB4E0 \uD14C\uC2A4\uD2B8\uAC00 \uD1B5\uACFC\uD588\uB098\uC694?",
|
|
741
|
+
hintTest: "pnpm test --run",
|
|
742
|
+
checkVersion: "package.json \uBC84\uC804\uC744 \uC62C\uB838\uB098\uC694?",
|
|
743
|
+
hintVersion: "version \uD544\uB4DC \uD655\uC778",
|
|
744
|
+
checkChangelog: "\uBCC0\uACBD \uB0B4\uC6A9\uC744 \uAE30\uB85D\uD588\uB098\uC694?",
|
|
745
|
+
hintChangelog: "README \uB610\uB294 CHANGELOG",
|
|
746
|
+
checkSecurity: "\uBCF4\uC548 \uC2A4\uCE94\uC744 \uB3CC\uB838\uB098\uC694?",
|
|
747
|
+
hintSecurity: "vhk \uBCF4\uC548 scan",
|
|
748
|
+
checkCommit: "\uBAA8\uB4E0 \uBCC0\uACBD\uC774 \uCEE4\uBC0B\uB418\uC5C8\uB098\uC694?",
|
|
749
|
+
hintCommit: "git status \uD655\uC778"
|
|
106
750
|
}
|
|
107
751
|
};
|
|
108
752
|
|
|
753
|
+
// src/commands/gate.ts
|
|
754
|
+
import inquirer from "inquirer";
|
|
755
|
+
import chalk2 from "chalk";
|
|
756
|
+
|
|
757
|
+
// src/lib/next-step.ts
|
|
758
|
+
import chalk from "chalk";
|
|
759
|
+
function printNextStep(step) {
|
|
760
|
+
console.log("");
|
|
761
|
+
console.log(chalk.cyan.bold("\u2501\u2501\u2501 \uB2E4\uC74C\uC5D0 \uC774\uAC83\uB9CC \uD558\uC138\uC694 \u2501\u2501\u2501"));
|
|
762
|
+
console.log("");
|
|
763
|
+
console.log(` ${step.message}`);
|
|
764
|
+
if (step.command) {
|
|
765
|
+
console.log("");
|
|
766
|
+
console.log(chalk.white.bgGray(" \uD130\uBBF8\uB110\uC5D0 \uBCF5\uBD99 "));
|
|
767
|
+
console.log(chalk.green(` ${step.command}`));
|
|
768
|
+
}
|
|
769
|
+
if (step.cursorHint) {
|
|
770
|
+
console.log("");
|
|
771
|
+
console.log(chalk.white.bgBlue(" Cursor\uC5D0\uAC8C \uB9D0\uD558\uAE30 "));
|
|
772
|
+
console.log(chalk.blue(` "${step.cursorHint}"`));
|
|
773
|
+
}
|
|
774
|
+
if (step.alternative) {
|
|
775
|
+
console.log("");
|
|
776
|
+
console.log(chalk.dim(` \uB610\uB294: ${step.alternative}`));
|
|
777
|
+
}
|
|
778
|
+
console.log("");
|
|
779
|
+
console.log(chalk.cyan.bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
780
|
+
console.log("");
|
|
781
|
+
}
|
|
782
|
+
|
|
109
783
|
// src/commands/gate.ts
|
|
110
784
|
var GATE_QUESTIONS = [
|
|
111
785
|
{ id: 1, stage: "\uBB38\uC81C \uC815\uC758", question: "\uC774 \uC544\uC774\uB514\uC5B4\uAC00 \uD574\uACB0\uD558\uB294 \uBB38\uC81C\uB97C \uD55C \uBB38\uC7A5\uC73C\uB85C \uB9D0\uD574\uBCF4\uC138\uC694.", failIf: "\uD55C \uBB38\uC7A5 \uBD88\uAC00 \u2192 \uBBF8\uC131\uC219", quick: true },
|
|
112
786
|
{ id: 2, stage: "\uD575\uC2EC \uAE30\uB2A5", question: "\uB531 1\uAC1C \uAE30\uB2A5\uB9CC \uACE0\uB974\uBA74?", failIf: "2\uAC1C \uC774\uC0C1 \u2192 \uBC94\uC704 \uCD08\uACFC", quick: true },
|
|
113
787
|
{ id: 3, stage: "\uC218\uC694 \uAC80\uC99D", question: "\uD0C0\uAC9F\uC774 \uBAA8\uC774\uB294 \uCEE4\uBBA4\uB2C8\uD2F0 3\uACF3\uC740?", failIf: "0\uACF3 \u2192 \uC2DC\uC7A5 \uC811\uC810 \uBD80\uC7AC", quick: true, hint: "\uC608: \uC778\uB514\uD574\uCEE4\uC2A4, \uD2B8\uC704\uD130 #buildinpublic, \uB514\uC2A4\uCF54\uB4DC" },
|
|
114
788
|
{ id: 4, stage: "\uAE30\uD68D", question: "v1 \uAE30\uB2A5 \uBAA9\uB85D 5\uAC1C \uC774\uB0B4\uB85C \uC815\uB9AC\uD574\uBCF4\uC138\uC694.", failIf: "5\uAC1C \uCD08\uACFC \u2192 \uC624\uBC84\uC5D4\uC9C0\uB2C8\uC5B4\uB9C1" },
|
|
115
|
-
{ id: 5, stage: "\uC124\uACC4", question: "\
|
|
789
|
+
{ id: 5, stage: "\uC124\uACC4", question: "\uB370\uC774\uD130\uB97C \uC800\uC7A5\uD560 \uACF3\uC774 \uB5A0\uC624\uB974\uC2DC\uB098\uC694?", failIf: "\uBAA8\uB974\uBA74 \uBCF4\uB958\uD574\uB3C4 \uB429\uB2C8\uB2E4", hint: '\uC608: \uC0AC\uC6A9\uC790 \uBAA9\uB85D\uC740 DB, \uB85C\uADF8\uC778\uC740 Supabase \u2014 \uBAA8\uB974\uBA74 "\uBCF4\uB958"' },
|
|
116
790
|
{ id: 6, stage: "\uAC1C\uBC1C", question: "3\uC77C \uC548\uC5D0 \uCF54\uC5B4 \uC644\uC131 \uAC00\uB2A5\uD55C\uAC00?", failIf: "\uBD88\uAC00\uB2A5 \u2192 \uBC94\uC704 \uCD95\uC18C \uD544\uC694", quick: true },
|
|
117
791
|
{ id: 7, stage: "\uB514\uC790\uC778", question: "\uB808\uD37C\uB7F0\uC2A4 UI 1\uAC1C \uC9C0\uC815\uD560 \uC218 \uC788\uB098?", failIf: "\uC5C6\uC73C\uBA74 \u2192 \uBC29\uD5A5 \uBBF8\uC815" },
|
|
118
792
|
{ id: 8, stage: "\uBC30\uD3EC", question: "\uBC30\uD3EC \uD50C\uB7AB\uD3FC + \uB3C4\uBA54\uC778 \uD655\uC815\uD588\uB098?", failIf: "\uBBF8\uC815 \u2192 \uBC30\uD3EC \uC9C0\uC5F0 \uC608\uACE0" },
|
|
@@ -128,7 +802,7 @@ function judgeGate(failCount, holdCount) {
|
|
|
128
802
|
return "DROP";
|
|
129
803
|
}
|
|
130
804
|
async function gate() {
|
|
131
|
-
console.log(
|
|
805
|
+
console.log(chalk2.bold(`
|
|
132
806
|
${ko.gate.title}
|
|
133
807
|
`));
|
|
134
808
|
const { mode } = await inquirer.prompt([{
|
|
@@ -147,23 +821,33 @@ ${ko.gate.title}
|
|
|
147
821
|
name: "source",
|
|
148
822
|
message: ko.gate.skipSourcePrompt
|
|
149
823
|
}]);
|
|
150
|
-
console.log(
|
|
824
|
+
console.log(chalk2.green.bold(`
|
|
151
825
|
${ko.gate.skipGo}`));
|
|
152
|
-
console.log(
|
|
826
|
+
console.log(chalk2.dim(ko.gate.skipSourceLabel(source)));
|
|
153
827
|
return;
|
|
154
828
|
}
|
|
155
829
|
const questions = mode === "quick" ? GATE_QUESTIONS.filter((q) => q.quick) : GATE_QUESTIONS;
|
|
156
830
|
const total = questions.length;
|
|
157
831
|
const header = mode === "quick" ? ko.gate.quickHeader : ko.gate.fullHeader;
|
|
158
|
-
console.log(
|
|
832
|
+
console.log(chalk2.dim(`
|
|
159
833
|
${header} ${ko.gate.modeCountSuffix(total)}
|
|
160
834
|
`));
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
835
|
+
console.log(chalk2.dim(`
|
|
836
|
+
${ko.gate.welcome}
|
|
837
|
+
`));
|
|
838
|
+
console.log(chalk2.dim(` ${ko.gate.ideaHint}`));
|
|
839
|
+
const { idea } = await inquirer.prompt([
|
|
840
|
+
{ type: "input", name: "idea", message: ko.gate.idea }
|
|
841
|
+
]);
|
|
842
|
+
console.log(chalk2.dim(` ${ko.gate.painPointHint}`));
|
|
843
|
+
const { painPoint } = await inquirer.prompt([
|
|
844
|
+
{ type: "input", name: "painPoint", message: ko.gate.painPoint }
|
|
845
|
+
]);
|
|
846
|
+
console.log(chalk2.dim(` ${ko.gate.edgeHint}`));
|
|
847
|
+
const { edge } = await inquirer.prompt([
|
|
164
848
|
{ type: "input", name: "edge", message: ko.gate.edge }
|
|
165
849
|
]);
|
|
166
|
-
console.log(
|
|
850
|
+
console.log(chalk2.dim(`
|
|
167
851
|
${ko.gate.checklistStart}
|
|
168
852
|
`));
|
|
169
853
|
let failCount = 0;
|
|
@@ -171,7 +855,7 @@ ${ko.gate.checklistStart}
|
|
|
171
855
|
const results = [];
|
|
172
856
|
for (let i = 0; i < questions.length; i++) {
|
|
173
857
|
const q = questions[i];
|
|
174
|
-
if (q.hint) console.log(
|
|
858
|
+
if (q.hint) console.log(chalk2.dim(`${ko.gate.hintPrefix} ${q.hint}`));
|
|
175
859
|
const { answer } = await inquirer.prompt([{
|
|
176
860
|
type: "input",
|
|
177
861
|
name: "answer",
|
|
@@ -190,34 +874,44 @@ ${ko.gate.checklistStart}
|
|
|
190
874
|
if (status === "fail") failCount++;
|
|
191
875
|
if (status === "hold") holdCount++;
|
|
192
876
|
results.push({ id: q.id, stage: q.stage, status, answer });
|
|
193
|
-
const icon = status === "pass" ?
|
|
877
|
+
const icon = status === "pass" ? chalk2.green(ko.gate.statusPassLine) : status === "hold" ? chalk2.yellow(ko.gate.statusHoldLine) : chalk2.red(ko.gate.statusFailLine);
|
|
194
878
|
console.log(icon);
|
|
195
879
|
}
|
|
196
|
-
console.log(
|
|
880
|
+
console.log(chalk2.bold(`
|
|
197
881
|
${ko.gate.verdictTitle}
|
|
198
882
|
`));
|
|
199
|
-
console.log(`${ko.gate.ideaLabel} ${
|
|
883
|
+
console.log(`${ko.gate.ideaLabel} ${chalk2.cyan(idea)}`);
|
|
200
884
|
console.log(`${ko.gate.painPointLabel} ${painPoint}`);
|
|
201
885
|
console.log(`${ko.gate.edgeLabel} ${edge}`);
|
|
202
886
|
console.log(`${ko.gate.countLine(failCount, holdCount, total)}
|
|
203
887
|
`);
|
|
204
888
|
const verdict = judgeGate(failCount, holdCount);
|
|
205
889
|
if (verdict === "GO") {
|
|
206
|
-
console.log(
|
|
207
|
-
console.log(chalk.green(ko.gate.nextCommand));
|
|
890
|
+
console.log(chalk2.green.bold(ko.gate.go));
|
|
208
891
|
if (holdCount > 0) {
|
|
209
|
-
console.log(
|
|
892
|
+
console.log(chalk2.yellow(ko.gate.holdRemainHint));
|
|
210
893
|
}
|
|
894
|
+
printNextStep({
|
|
895
|
+
message: "\uC544\uC774\uB514\uC5B4 \uD1B5\uACFC! \uC774\uC81C \uD504\uB85C\uC81D\uD2B8\uB97C \uB9CC\uB4E4\uC5B4\uBCF4\uC138\uC694.",
|
|
896
|
+
command: "vhk \uC2DC\uC791",
|
|
897
|
+
cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
|
|
898
|
+
});
|
|
211
899
|
} else if (verdict === "REFINE") {
|
|
212
|
-
console.log(
|
|
900
|
+
console.log(chalk2.yellow.bold(ko.gate.refine));
|
|
901
|
+
printNextStep({
|
|
902
|
+
message: "\uC870\uAE08 \uB354 \uB2E4\uB4EC\uC740 \uD6C4 \uB2E4\uC2DC \uAC80\uC99D\uD574\uBCF4\uC138\uC694.",
|
|
903
|
+
command: "vhk \uAC80\uC99D",
|
|
904
|
+
cursorHint: "\uC544\uC774\uB514\uC5B4 \uB2E4\uC2DC \uAC80\uC99D\uD574\uC918"
|
|
905
|
+
});
|
|
213
906
|
} else {
|
|
214
|
-
console.log(
|
|
907
|
+
console.log(chalk2.red.bold(ko.gate.drop));
|
|
215
908
|
}
|
|
216
909
|
}
|
|
217
910
|
|
|
218
911
|
// src/commands/init.ts
|
|
219
912
|
import inquirer2 from "inquirer";
|
|
220
|
-
import
|
|
913
|
+
import chalk4 from "chalk";
|
|
914
|
+
import fs2 from "fs";
|
|
221
915
|
import path2 from "path";
|
|
222
916
|
|
|
223
917
|
// src/templates/claude-md.ts
|
|
@@ -398,14 +1092,44 @@ function ADR_TEMPLATE() {
|
|
|
398
1092
|
].join("\n");
|
|
399
1093
|
}
|
|
400
1094
|
|
|
1095
|
+
// src/templates/commands-md.ts
|
|
1096
|
+
function COMMANDS_MD_TEMPLATE() {
|
|
1097
|
+
return [
|
|
1098
|
+
"# \u{1F4CB} \uD55C\uAD6D\uC5B4 \uBA85\uB839\uC5B4 \uAC00\uC774\uB4DC",
|
|
1099
|
+
"",
|
|
1100
|
+
"\uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uC790\uC8FC \uC4F0\uB294 \uBA85\uB839\uC5B4\uC785\uB2C8\uB2E4.",
|
|
1101
|
+
"Cursor\uC5D0\uAC8C \uD55C\uAD6D\uC5B4\uB85C \uB9D0\uD574\uB3C4 \uB429\uB2C8\uB2E4.",
|
|
1102
|
+
"",
|
|
1103
|
+
"## \uB9E4\uC77C \uC4F0\uB294 \uBA85\uB839\uC5B4",
|
|
1104
|
+
"",
|
|
1105
|
+
"| \uD558\uACE0 \uC2F6\uC740 \uAC83 | \uD130\uBBF8\uB110 \uBA85\uB839 | Cursor\uC5D0\uAC8C \uB9D0\uD558\uAE30 |",
|
|
1106
|
+
"|-------------|-----------|------------------|",
|
|
1107
|
+
'| \uC800\uC7A5 | `git add . && git commit -m "\uBA54\uC2DC\uC9C0"` | "\uC800\uC7A5\uD574" |',
|
|
1108
|
+
'| \uC624\uB298 \uC815\uB9AC | `vhk \uC815\uB9AC` | "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD574" |',
|
|
1109
|
+
'| \uADDC\uCE59 \uC810\uAC80 | `vhk \uC810\uAC80` | "\uADDC\uCE59 \uC810\uAC80\uD574" |',
|
|
1110
|
+
'| \uBCF4\uC548 \uC2A4\uCE94 | `vhk \uBCF4\uC548 scan` | "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824" |',
|
|
1111
|
+
'| \uBE4C\uB4DC+\uD14C\uC2A4\uD2B8 | `pnpm build; pnpm test --run` | "\uBE4C\uB4DC\uD558\uACE0 \uD14C\uC2A4\uD2B8 \uB3CC\uB824" |',
|
|
1112
|
+
'| \uBC30\uD3EC | `vhk \uBC30\uD3EC` | "\uBC30\uD3EC\uD574" |',
|
|
1113
|
+
"",
|
|
1114
|
+
"## \uD658\uACBD \uC810\uAC80",
|
|
1115
|
+
"",
|
|
1116
|
+
"```bash",
|
|
1117
|
+
"vhk doctor",
|
|
1118
|
+
"```",
|
|
1119
|
+
"",
|
|
1120
|
+
"---",
|
|
1121
|
+
"*Generated by `vhk init` \u2014 \uC218\uC815\uD574\uB3C4 \uB429\uB2C8\uB2E4*"
|
|
1122
|
+
].join("\n");
|
|
1123
|
+
}
|
|
1124
|
+
|
|
401
1125
|
// src/utils/logger.ts
|
|
402
|
-
import
|
|
1126
|
+
import chalk3 from "chalk";
|
|
403
1127
|
var log = {
|
|
404
|
-
success: (msg) => console.log(
|
|
405
|
-
error: (msg) => console.log(
|
|
406
|
-
warn: (msg) => console.log(
|
|
407
|
-
info: (msg) => console.log(
|
|
408
|
-
step: (msg) => console.log(
|
|
1128
|
+
success: (msg) => console.log(chalk3.green(`\u2705 ${msg}`)),
|
|
1129
|
+
error: (msg) => console.log(chalk3.red(`\u274C ${msg}`)),
|
|
1130
|
+
warn: (msg) => console.log(chalk3.yellow(`\u26A0\uFE0F ${msg}`)),
|
|
1131
|
+
info: (msg) => console.log(chalk3.blue(`\u2139\uFE0F ${msg}`)),
|
|
1132
|
+
step: (msg) => console.log(chalk3.bold(`
|
|
409
1133
|
\u25B8 ${msg}`))
|
|
410
1134
|
};
|
|
411
1135
|
|
|
@@ -610,11 +1334,11 @@ async function collectAnswers(options, defaults = {}) {
|
|
|
610
1334
|
async function init(options = {}) {
|
|
611
1335
|
const skipGate = Boolean(options.skipGate || options.fromNotion);
|
|
612
1336
|
if (skipGate) {
|
|
613
|
-
console.log(
|
|
1337
|
+
console.log(chalk4.dim(`
|
|
614
1338
|
${ko.init.skipGate}
|
|
615
1339
|
`));
|
|
616
1340
|
}
|
|
617
|
-
console.log(
|
|
1341
|
+
console.log(chalk4.bold(`
|
|
618
1342
|
${ko.init.title}
|
|
619
1343
|
`));
|
|
620
1344
|
let prdContent = {};
|
|
@@ -638,7 +1362,7 @@ ${ko.init.title}
|
|
|
638
1362
|
process.exit(1);
|
|
639
1363
|
}
|
|
640
1364
|
const stack = STACK_PRESETS[answers.type];
|
|
641
|
-
console.log(
|
|
1365
|
+
console.log(chalk4.dim(`
|
|
642
1366
|
${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
643
1367
|
`));
|
|
644
1368
|
if (!options.yes) {
|
|
@@ -673,22 +1397,31 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
673
1397
|
writeFile(fullPath, content);
|
|
674
1398
|
log.success(filePath);
|
|
675
1399
|
}
|
|
676
|
-
|
|
1400
|
+
await writeInitExtras(cwd);
|
|
1401
|
+
console.log(chalk4.bold.green(`
|
|
677
1402
|
${ko.init.done}`));
|
|
678
|
-
console.log(
|
|
1403
|
+
console.log(chalk4.dim(`
|
|
679
1404
|
${ko.init.nextSteps}`));
|
|
680
1405
|
if (options.fromNotion) {
|
|
681
1406
|
console.log(` 1. ${ko.init.notionReviewHint}`);
|
|
682
|
-
console.log(` 2. ${
|
|
1407
|
+
console.log(` 2. ${ko.init.gitHintLabel}`);
|
|
1408
|
+
console.log(` ${chalk4.cyan(ko.init.gitHintCommand)}`);
|
|
683
1409
|
console.log(` 3. ${ko.init.startDev}
|
|
684
1410
|
`);
|
|
685
1411
|
} else {
|
|
686
1412
|
console.log(` 1. ${ko.init.fillHint}`);
|
|
687
1413
|
console.log(` 2. ${ko.init.prdHint}`);
|
|
688
|
-
console.log(` 3. ${
|
|
1414
|
+
console.log(` 3. ${ko.init.gitHintLabel}`);
|
|
1415
|
+
console.log(` ${chalk4.cyan(ko.init.gitHintCommand)}`);
|
|
689
1416
|
console.log(` 4. ${ko.init.startDev}
|
|
690
1417
|
`);
|
|
691
1418
|
}
|
|
1419
|
+
printNextStep({
|
|
1420
|
+
message: "\uD504\uB85C\uC81D\uD2B8 \uBF08\uB300 \uC644\uC131! \uC774\uC81C \uAC1C\uBC1C\uC744 \uC2DC\uC791\uD558\uC138\uC694.",
|
|
1421
|
+
command: `cd ${cwd}`,
|
|
1422
|
+
cursorHint: "docs/PRD.md \uC5F4\uACE0 \uAC1C\uBC1C \uC2DC\uC791\uD574\uC918",
|
|
1423
|
+
alternative: "VS Code/Cursor\uC5D0\uC11C \uD3F4\uB354\uB97C \uC5F4\uC5B4\uB3C4 \uB429\uB2C8\uB2E4"
|
|
1424
|
+
});
|
|
692
1425
|
}
|
|
693
1426
|
function generateFiles(name, description, stack, prdContent = {}) {
|
|
694
1427
|
const stackStr = stack.join(" + ");
|
|
@@ -718,16 +1451,95 @@ function generateFiles(name, description, stack, prdContent = {}) {
|
|
|
718
1451
|
`
|
|
719
1452
|
};
|
|
720
1453
|
}
|
|
1454
|
+
var VHK_PACKAGE_SCRIPTS = {
|
|
1455
|
+
save: "git add . && git commit -m",
|
|
1456
|
+
check: "vhk check",
|
|
1457
|
+
scan: "vhk secure scan",
|
|
1458
|
+
recap: "vhk recap",
|
|
1459
|
+
ship: "vhk ship",
|
|
1460
|
+
doctor: "vhk doctor"
|
|
1461
|
+
};
|
|
1462
|
+
function enhancePackageScripts(projectDir) {
|
|
1463
|
+
const pkgPath = path2.join(projectDir, "package.json");
|
|
1464
|
+
if (!fs2.existsSync(pkgPath)) return false;
|
|
1465
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
1466
|
+
pkg.scripts = { ...pkg.scripts, ...VHK_PACKAGE_SCRIPTS };
|
|
1467
|
+
fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
async function writeInitExtras(projectDir) {
|
|
1471
|
+
const commandsPath = path2.join(projectDir, "COMMANDS.md");
|
|
1472
|
+
if (fileExists(commandsPath)) {
|
|
1473
|
+
const { overwrite } = await inquirer2.prompt([{
|
|
1474
|
+
type: "confirm",
|
|
1475
|
+
name: "overwrite",
|
|
1476
|
+
message: ko.init.overwrite("COMMANDS.md"),
|
|
1477
|
+
default: false
|
|
1478
|
+
}]);
|
|
1479
|
+
if (!overwrite) {
|
|
1480
|
+
log.warn(ko.init.skipped("COMMANDS.md"));
|
|
1481
|
+
} else {
|
|
1482
|
+
writeFile(commandsPath, COMMANDS_MD_TEMPLATE());
|
|
1483
|
+
log.success(ko.init.commandsMdDone);
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
writeFile(commandsPath, COMMANDS_MD_TEMPLATE());
|
|
1487
|
+
log.success(ko.init.commandsMdDone);
|
|
1488
|
+
}
|
|
1489
|
+
if (enhancePackageScripts(projectDir)) {
|
|
1490
|
+
log.success(ko.init.scriptsDone);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
721
1493
|
|
|
722
1494
|
// src/commands/recap.ts
|
|
723
1495
|
import inquirer3 from "inquirer";
|
|
724
|
-
import
|
|
725
|
-
import
|
|
726
|
-
import
|
|
1496
|
+
import chalk5 from "chalk";
|
|
1497
|
+
import fs5 from "fs";
|
|
1498
|
+
import path6 from "path";
|
|
727
1499
|
|
|
728
1500
|
// src/lib/git.ts
|
|
1501
|
+
import path4 from "path";
|
|
729
1502
|
import simpleGit from "simple-git";
|
|
1503
|
+
|
|
1504
|
+
// src/lib/check-secure.ts
|
|
1505
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
1506
|
+
import fs3 from "fs";
|
|
1507
|
+
import path3 from "path";
|
|
1508
|
+
function loadGitignore(rootDir) {
|
|
1509
|
+
const ig = (0, import_ignore.default)();
|
|
1510
|
+
const gitignorePath = path3.join(rootDir, ".gitignore");
|
|
1511
|
+
if (fs3.existsSync(gitignorePath)) {
|
|
1512
|
+
const content = fs3.readFileSync(gitignorePath, "utf-8");
|
|
1513
|
+
ig.add(content);
|
|
1514
|
+
}
|
|
1515
|
+
return ig;
|
|
1516
|
+
}
|
|
1517
|
+
function isPathIgnored(ig, relativePath) {
|
|
1518
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
1519
|
+
return ig.ignores(normalized);
|
|
1520
|
+
}
|
|
1521
|
+
function filterTrackedPaths(paths, rootDir = process.cwd()) {
|
|
1522
|
+
const ig = loadGitignore(rootDir);
|
|
1523
|
+
return paths.filter((p) => !isPathIgnored(ig, p.replace(/\\/g, "/")));
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// src/lib/git.ts
|
|
730
1527
|
var git = simpleGit();
|
|
1528
|
+
function isNoiseRecapPath(filePath) {
|
|
1529
|
+
const base = path4.basename(filePath);
|
|
1530
|
+
if (base.includes("${") || base.includes("`")) return true;
|
|
1531
|
+
if (/^\d+(\.\d+)?$/.test(base)) return true;
|
|
1532
|
+
if (/^(pnpm|vhk|npm|node|yarn)$/i.test(base)) return true;
|
|
1533
|
+
if (!filePath.includes("/") && !filePath.includes("\\") && !base.includes(".")) {
|
|
1534
|
+
if (!/^(README|LICENSE|Makefile|Dockerfile|CHANGELOG)$/i.test(base)) return true;
|
|
1535
|
+
}
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
function filterRecapFiles(files) {
|
|
1539
|
+
const paths = files.map((f) => f.file);
|
|
1540
|
+
const tracked = new Set(filterTrackedPaths(paths));
|
|
1541
|
+
return files.filter((f) => tracked.has(f.file) && !isNoiseRecapPath(f.file));
|
|
1542
|
+
}
|
|
731
1543
|
function fileStatus(workingDir) {
|
|
732
1544
|
if (workingDir === "?") return "new";
|
|
733
1545
|
if (workingDir === "D") return "deleted";
|
|
@@ -741,17 +1553,19 @@ async function getSessionDiff(since) {
|
|
|
741
1553
|
const statByFile = new Map(
|
|
742
1554
|
diffSummary.files.map((f) => [f.file, f])
|
|
743
1555
|
);
|
|
744
|
-
const files =
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1556
|
+
const files = filterRecapFiles(
|
|
1557
|
+
statusResult.files.map((f) => {
|
|
1558
|
+
const stat = statByFile.get(f.path);
|
|
1559
|
+
return {
|
|
1560
|
+
file: f.path,
|
|
1561
|
+
insertions: stat?.insertions ?? 0,
|
|
1562
|
+
deletions: stat?.deletions ?? 0,
|
|
1563
|
+
status: fileStatus(f.working_dir)
|
|
1564
|
+
};
|
|
1565
|
+
})
|
|
1566
|
+
);
|
|
753
1567
|
return {
|
|
754
|
-
filesChanged:
|
|
1568
|
+
filesChanged: files.length,
|
|
755
1569
|
insertions: diffSummary.insertions,
|
|
756
1570
|
deletions: diffSummary.deletions,
|
|
757
1571
|
files
|
|
@@ -778,8 +1592,8 @@ async function isGitRepo() {
|
|
|
778
1592
|
}
|
|
779
1593
|
|
|
780
1594
|
// src/lib/adr.ts
|
|
781
|
-
import
|
|
782
|
-
import
|
|
1595
|
+
import fs4 from "fs";
|
|
1596
|
+
import path5 from "path";
|
|
783
1597
|
var ADR_RULES = [
|
|
784
1598
|
{
|
|
785
1599
|
title: "\uC758\uC874\uC131 \uBCC0\uACBD",
|
|
@@ -822,20 +1636,20 @@ function detectAdrCandidates(diff) {
|
|
|
822
1636
|
return candidates;
|
|
823
1637
|
}
|
|
824
1638
|
function nextAdrNumber(adrDir) {
|
|
825
|
-
if (!
|
|
826
|
-
const nums =
|
|
1639
|
+
if (!fs4.existsSync(adrDir)) return 1;
|
|
1640
|
+
const nums = fs4.readdirSync(adrDir).map((name) => name.match(/^ADR-(\d+)/i)?.[1]).filter((n) => Boolean(n)).map((n) => parseInt(n, 10));
|
|
827
1641
|
return nums.length ? Math.max(...nums) + 1 : 1;
|
|
828
1642
|
}
|
|
829
1643
|
function slugify(title) {
|
|
830
1644
|
return title.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9가-힣-]/g, "").slice(0, 40) || "decision";
|
|
831
1645
|
}
|
|
832
1646
|
function createAdrFile(cwd, title, context, decision, consequences) {
|
|
833
|
-
const adrDir =
|
|
834
|
-
if (!
|
|
1647
|
+
const adrDir = path5.join(cwd, "docs", "adr");
|
|
1648
|
+
if (!fs4.existsSync(adrDir)) fs4.mkdirSync(adrDir, { recursive: true });
|
|
835
1649
|
const num = nextAdrNumber(adrDir);
|
|
836
1650
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
837
1651
|
const fileName = `ADR-${String(num).padStart(3, "0")}-${slugify(title)}.md`;
|
|
838
|
-
const filePath =
|
|
1652
|
+
const filePath = path5.join(adrDir, fileName);
|
|
839
1653
|
const content = [
|
|
840
1654
|
"---",
|
|
841
1655
|
`id: ADR-${String(num).padStart(3, "0")}`,
|
|
@@ -861,44 +1675,45 @@ function createAdrFile(cwd, title, context, decision, consequences) {
|
|
|
861
1675
|
"---",
|
|
862
1676
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
863
1677
|
].join("\n");
|
|
864
|
-
|
|
1678
|
+
fs4.writeFileSync(filePath, content, "utf-8");
|
|
865
1679
|
return filePath;
|
|
866
1680
|
}
|
|
867
1681
|
|
|
868
1682
|
// src/commands/recap.ts
|
|
869
1683
|
async function recap(options = {}) {
|
|
870
|
-
console.log(
|
|
1684
|
+
console.log(chalk5.bold(`
|
|
871
1685
|
${ko.recap.title}
|
|
872
1686
|
`));
|
|
873
1687
|
if (!await isGitRepo()) {
|
|
874
|
-
console.log(
|
|
1688
|
+
console.log(chalk5.red(ko.recap.noRepo));
|
|
875
1689
|
return;
|
|
876
1690
|
}
|
|
877
|
-
console.log(
|
|
1691
|
+
console.log(chalk5.dim(`${ko.recap.analyzing}
|
|
878
1692
|
`));
|
|
879
|
-
const
|
|
880
|
-
const
|
|
1693
|
+
const since = options.since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1694
|
+
const diff = await getSessionDiff(since);
|
|
1695
|
+
const commits = await getRecentCommits(10, since);
|
|
881
1696
|
if (diff.filesChanged === 0 && commits.length === 0) {
|
|
882
|
-
console.log(
|
|
1697
|
+
console.log(chalk5.yellow(ko.recap.noChanges));
|
|
883
1698
|
return;
|
|
884
1699
|
}
|
|
885
|
-
console.log(
|
|
886
|
-
console.log(` \uD30C\uC77C: ${
|
|
887
|
-
console.log(` \uCD94\uAC00: ${
|
|
1700
|
+
console.log(chalk5.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
|
|
1701
|
+
console.log(` \uD30C\uC77C: ${chalk5.cyan(String(diff.filesChanged))}\uAC1C \uBCC0\uACBD`);
|
|
1702
|
+
console.log(` \uCD94\uAC00: ${chalk5.green("+" + diff.insertions)} / \uC0AD\uC81C: ${chalk5.red("-" + diff.deletions)}`);
|
|
888
1703
|
if (diff.files.length > 0) {
|
|
889
|
-
console.log(
|
|
1704
|
+
console.log(chalk5.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
|
|
890
1705
|
diff.files.slice(0, 15).forEach((f) => {
|
|
891
|
-
const icon = f.status === "new" ?
|
|
1706
|
+
const icon = f.status === "new" ? chalk5.green("\u{1F195}") : f.status === "deleted" ? chalk5.red("\u{1F5D1}\uFE0F") : chalk5.yellow("\u270F\uFE0F");
|
|
892
1707
|
console.log(` ${icon} ${f.file}`);
|
|
893
1708
|
});
|
|
894
1709
|
if (diff.files.length > 15) {
|
|
895
|
-
console.log(
|
|
1710
|
+
console.log(chalk5.dim(` ... \uC678 ${diff.files.length - 15}\uAC1C`));
|
|
896
1711
|
}
|
|
897
1712
|
}
|
|
898
1713
|
if (commits.length > 0) {
|
|
899
|
-
console.log(
|
|
1714
|
+
console.log(chalk5.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
|
|
900
1715
|
commits.slice(0, 5).forEach((c) => {
|
|
901
|
-
console.log(
|
|
1716
|
+
console.log(chalk5.dim(` \u2022 ${c.message}`));
|
|
902
1717
|
});
|
|
903
1718
|
}
|
|
904
1719
|
console.log("");
|
|
@@ -927,12 +1742,12 @@ ${ko.recap.title}
|
|
|
927
1742
|
}
|
|
928
1743
|
]);
|
|
929
1744
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
930
|
-
const logDir =
|
|
931
|
-
if (!
|
|
932
|
-
const existing =
|
|
1745
|
+
const logDir = path6.join(process.cwd(), "docs", "log");
|
|
1746
|
+
if (!fs5.existsSync(logDir)) fs5.mkdirSync(logDir, { recursive: true });
|
|
1747
|
+
const existing = fs5.readdirSync(logDir).filter((f) => f.startsWith(today));
|
|
933
1748
|
const sessionNum = existing.length + 1;
|
|
934
1749
|
const fileName = `${today}-session-${sessionNum}.md`;
|
|
935
|
-
const filePath =
|
|
1750
|
+
const filePath = path6.join(logDir, fileName);
|
|
936
1751
|
const fileList = diff.files.map((f) => `| ${f.file} | ${f.status} |`).join("\n");
|
|
937
1752
|
const commitList = commits.slice(0, 10).map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
938
1753
|
const content = [
|
|
@@ -963,14 +1778,14 @@ ${ko.recap.title}
|
|
|
963
1778
|
"---",
|
|
964
1779
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
965
1780
|
].join("\n");
|
|
966
|
-
|
|
1781
|
+
fs5.writeFileSync(filePath, content, "utf-8");
|
|
967
1782
|
const adrCandidates = detectAdrCandidates(diff);
|
|
968
1783
|
if (adrCandidates.length > 0) {
|
|
969
|
-
console.log(
|
|
1784
|
+
console.log(chalk5.cyan.bold(`
|
|
970
1785
|
${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
971
1786
|
for (const candidate of adrCandidates) {
|
|
972
|
-
console.log(
|
|
973
|
-
candidate.files.forEach((f) => console.log(
|
|
1787
|
+
console.log(chalk5.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
|
|
1788
|
+
candidate.files.forEach((f) => console.log(chalk5.dim(` ${f}`)));
|
|
974
1789
|
}
|
|
975
1790
|
const { createAdr } = await inquirer3.prompt([{
|
|
976
1791
|
type: "confirm",
|
|
@@ -1000,17 +1815,17 @@ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
|
1000
1815
|
adrAnswers.decision,
|
|
1001
1816
|
adrAnswers.consequences
|
|
1002
1817
|
);
|
|
1003
|
-
console.log(
|
|
1818
|
+
console.log(chalk5.green(` \u2705 ADR \uC0DD\uC131: ${path6.relative(process.cwd(), adrPath)}`));
|
|
1004
1819
|
}
|
|
1005
1820
|
}
|
|
1006
1821
|
}
|
|
1007
1822
|
const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
|
|
1008
1823
|
const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
|
|
1009
1824
|
if (troubleCommits.length > 0) {
|
|
1010
|
-
console.log(
|
|
1825
|
+
console.log(chalk5.yellow.bold(`
|
|
1011
1826
|
${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
1012
1827
|
troubleCommits.forEach((c) => {
|
|
1013
|
-
console.log(
|
|
1828
|
+
console.log(chalk5.dim(` \u2022 ${c.message}`));
|
|
1014
1829
|
});
|
|
1015
1830
|
const { createTroubleshoot } = await inquirer3.prompt([{
|
|
1016
1831
|
type: "confirm",
|
|
@@ -1019,8 +1834,8 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
|
1019
1834
|
default: true
|
|
1020
1835
|
}]);
|
|
1021
1836
|
if (createTroubleshoot) {
|
|
1022
|
-
const tsDir =
|
|
1023
|
-
if (!
|
|
1837
|
+
const tsDir = path6.join(process.cwd(), "docs", "troubleshooting");
|
|
1838
|
+
if (!fs5.existsSync(tsDir)) fs5.mkdirSync(tsDir, { recursive: true });
|
|
1024
1839
|
const tsAnswers = await inquirer3.prompt([
|
|
1025
1840
|
{
|
|
1026
1841
|
type: "input",
|
|
@@ -1039,7 +1854,7 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
|
1039
1854
|
}
|
|
1040
1855
|
]);
|
|
1041
1856
|
const tsFileName = `${today}-${tsAnswers.problem.slice(0, 30).replace(/[^a-zA-Z0-9가-힣]/g, "-")}.md`;
|
|
1042
|
-
const tsFilePath =
|
|
1857
|
+
const tsFilePath = path6.join(tsDir, tsFileName);
|
|
1043
1858
|
const tsContent = [
|
|
1044
1859
|
`# \uD2B8\uB7EC\uBE14\uC288\uD305: ${tsAnswers.problem}`,
|
|
1045
1860
|
"",
|
|
@@ -1060,15 +1875,15 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
|
1060
1875
|
"---",
|
|
1061
1876
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
1062
1877
|
].join("\n");
|
|
1063
|
-
|
|
1064
|
-
console.log(
|
|
1878
|
+
fs5.writeFileSync(tsFilePath, tsContent, "utf-8");
|
|
1879
|
+
console.log(chalk5.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path6.relative(process.cwd(), tsFilePath)}`));
|
|
1065
1880
|
}
|
|
1066
1881
|
}
|
|
1067
|
-
console.log(
|
|
1882
|
+
console.log(chalk5.green.bold(`
|
|
1068
1883
|
${ko.recap.done}`));
|
|
1069
|
-
console.log(
|
|
1070
|
-
const claudeMdPath =
|
|
1071
|
-
if (
|
|
1884
|
+
console.log(chalk5.dim(` \u{1F4C4} ${path6.relative(process.cwd(), filePath)}`));
|
|
1885
|
+
const claudeMdPath = path6.join(process.cwd(), "CLAUDE.md");
|
|
1886
|
+
if (fs5.existsSync(claudeMdPath)) {
|
|
1072
1887
|
const { updateClaude } = await inquirer3.prompt([{
|
|
1073
1888
|
type: "confirm",
|
|
1074
1889
|
name: "updateClaude",
|
|
@@ -1076,7 +1891,7 @@ ${ko.recap.done}`));
|
|
|
1076
1891
|
default: true
|
|
1077
1892
|
}]);
|
|
1078
1893
|
if (updateClaude) {
|
|
1079
|
-
let claudeContent =
|
|
1894
|
+
let claudeContent = fs5.readFileSync(claudeMdPath, "utf-8");
|
|
1080
1895
|
claudeContent = claudeContent.replace(
|
|
1081
1896
|
/- \*\*마지막 업데이트:\*\*.*/,
|
|
1082
1897
|
`- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${today}`
|
|
@@ -1085,19 +1900,22 @@ ${ko.recap.done}`));
|
|
|
1085
1900
|
/- \*\*다음 액션:\*\*.*/,
|
|
1086
1901
|
`- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
|
|
1087
1902
|
);
|
|
1088
|
-
|
|
1089
|
-
console.log(
|
|
1903
|
+
fs5.writeFileSync(claudeMdPath, claudeContent, "utf-8");
|
|
1904
|
+
console.log(chalk5.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
|
|
1090
1905
|
}
|
|
1091
1906
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1907
|
+
const gitSaveCmd = process.platform === "win32" ? 'git add .; git commit -m "recap: \uC138\uC158 \uAE30\uB85D"' : 'git add . && git commit -m "recap: \uC138\uC158 \uAE30\uB85D"';
|
|
1908
|
+
printNextStep({
|
|
1909
|
+
message: "\uC624\uB298 \uAE30\uB85D \uC644\uB8CC! \uC800\uC7A5\uD558\uACE0 \uC2F6\uC73C\uBA74:",
|
|
1910
|
+
command: gitSaveCmd,
|
|
1911
|
+
cursorHint: "\uC800\uC7A5\uD574\uC918"
|
|
1912
|
+
});
|
|
1095
1913
|
}
|
|
1096
1914
|
|
|
1097
1915
|
// src/commands/sync.ts
|
|
1098
|
-
import
|
|
1099
|
-
import
|
|
1100
|
-
import
|
|
1916
|
+
import chalk6 from "chalk";
|
|
1917
|
+
import fs6 from "fs";
|
|
1918
|
+
import path7 from "path";
|
|
1101
1919
|
var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
|
|
1102
1920
|
var CLAUDE_MD_KEYS = ["\uAE30\uB85D", "\uB85C\uADF8", "ADR", "\uD2B8\uB7EC\uBE14\uC288\uD305", "TIL", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8"];
|
|
1103
1921
|
function parseRulesMd(content) {
|
|
@@ -1165,59 +1983,64 @@ function toClaudeMd(sections, existing) {
|
|
|
1165
1983
|
return lines.join("\n");
|
|
1166
1984
|
}
|
|
1167
1985
|
async function sync() {
|
|
1168
|
-
console.log(
|
|
1986
|
+
console.log(chalk6.bold(`
|
|
1169
1987
|
${ko.sync.title}
|
|
1170
1988
|
`));
|
|
1171
1989
|
const cwd = process.cwd();
|
|
1172
|
-
const rulesPath =
|
|
1173
|
-
if (!
|
|
1174
|
-
console.log(
|
|
1175
|
-
console.log(
|
|
1176
|
-
console.log(
|
|
1990
|
+
const rulesPath = path7.join(cwd, "RULES.md");
|
|
1991
|
+
if (!fs6.existsSync(rulesPath)) {
|
|
1992
|
+
console.log(chalk6.yellow(ko.sync.noRules));
|
|
1993
|
+
console.log(chalk6.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
|
|
1994
|
+
console.log(chalk6.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
|
|
1177
1995
|
console.log("");
|
|
1178
|
-
console.log(
|
|
1179
|
-
console.log(
|
|
1180
|
-
console.log(
|
|
1181
|
-
console.log(
|
|
1182
|
-
console.log(
|
|
1183
|
-
console.log(
|
|
1996
|
+
console.log(chalk6.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
|
|
1997
|
+
console.log(chalk6.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
|
|
1998
|
+
console.log(chalk6.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
|
|
1999
|
+
console.log(chalk6.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
|
|
2000
|
+
console.log(chalk6.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
|
|
2001
|
+
console.log(chalk6.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
|
|
1184
2002
|
return;
|
|
1185
2003
|
}
|
|
1186
|
-
const rulesContent =
|
|
2004
|
+
const rulesContent = fs6.readFileSync(rulesPath, "utf-8");
|
|
1187
2005
|
const sections = parseRulesMd(rulesContent);
|
|
1188
|
-
console.log(
|
|
2006
|
+
console.log(chalk6.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
|
|
1189
2007
|
const firstLine = rulesContent.split("\n")[0];
|
|
1190
2008
|
const projectName = firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
|
|
1191
|
-
const cursorrulesPath =
|
|
1192
|
-
|
|
1193
|
-
console.log(
|
|
1194
|
-
const claudePath =
|
|
1195
|
-
const existingClaude =
|
|
2009
|
+
const cursorrulesPath = path7.join(cwd, ".cursorrules");
|
|
2010
|
+
fs6.writeFileSync(cursorrulesPath, toCursorrules(sections, projectName), "utf-8");
|
|
2011
|
+
console.log(chalk6.green(` ${ko.sync.cursorrulesDone}`));
|
|
2012
|
+
const claudePath = path7.join(cwd, "CLAUDE.md");
|
|
2013
|
+
const existingClaude = fs6.existsSync(claudePath) ? fs6.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
|
|
1196
2014
|
|
|
1197
2015
|
## \uD604\uC7AC \uC0C1\uD0DC
|
|
1198
2016
|
- **Phase:** __FILL__
|
|
1199
2017
|
- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C
|
|
1200
2018
|
- **\uB2E4\uC74C \uC561\uC158:** __FILL__
|
|
1201
2019
|
- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
|
|
1202
|
-
|
|
1203
|
-
console.log(
|
|
1204
|
-
console.log(
|
|
2020
|
+
fs6.writeFileSync(claudePath, toClaudeMd(sections, existingClaude), "utf-8");
|
|
2021
|
+
console.log(chalk6.green(` ${ko.sync.claudeDone}`));
|
|
2022
|
+
console.log(chalk6.bold.green(`
|
|
1205
2023
|
${ko.sync.done}`));
|
|
1206
|
-
console.log(
|
|
1207
|
-
console.log(
|
|
2024
|
+
console.log(chalk6.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md (\uC790\uB3D9 \uC0DD\uC131)"));
|
|
2025
|
+
console.log(chalk6.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
|
|
2026
|
+
printNextStep({
|
|
2027
|
+
message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
|
|
2028
|
+
command: "vhk \uC810\uAC80",
|
|
2029
|
+
cursorHint: "\uADDC\uCE59 \uC810\uAC80\uD574\uC918"
|
|
2030
|
+
});
|
|
1208
2031
|
}
|
|
1209
2032
|
|
|
1210
2033
|
// src/commands/check.ts
|
|
1211
|
-
import
|
|
1212
|
-
import
|
|
1213
|
-
import
|
|
2034
|
+
import chalk7 from "chalk";
|
|
2035
|
+
import path9 from "path";
|
|
2036
|
+
import fs8 from "fs";
|
|
1214
2037
|
|
|
1215
2038
|
// src/lib/rules-parser.ts
|
|
1216
|
-
import
|
|
1217
|
-
import
|
|
2039
|
+
import fs7 from "fs";
|
|
2040
|
+
import path8 from "path";
|
|
1218
2041
|
function parseRules(rulesPath) {
|
|
1219
|
-
if (!
|
|
1220
|
-
const content =
|
|
2042
|
+
if (!fs7.existsSync(rulesPath)) return [];
|
|
2043
|
+
const content = fs7.readFileSync(rulesPath, "utf-8");
|
|
1221
2044
|
const lines = content.split("\n");
|
|
1222
2045
|
const rules = [];
|
|
1223
2046
|
let currentSection = "";
|
|
@@ -1287,17 +2110,17 @@ function createNamingRule(id, section, desc, convention) {
|
|
|
1287
2110
|
description: desc,
|
|
1288
2111
|
check: (cwd) => {
|
|
1289
2112
|
const violations = [];
|
|
1290
|
-
const srcDir =
|
|
1291
|
-
if (!
|
|
2113
|
+
const srcDir = path8.join(cwd, "src");
|
|
2114
|
+
if (!fs7.existsSync(srcDir)) return violations;
|
|
1292
2115
|
walkFiles(srcDir, (filePath) => {
|
|
1293
|
-
const name =
|
|
2116
|
+
const name = path8.basename(filePath, path8.extname(filePath));
|
|
1294
2117
|
if (convention === "kebab-case" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
1295
2118
|
if (!["index", "vite.config", "tsconfig"].includes(name)) {
|
|
1296
2119
|
violations.push({
|
|
1297
2120
|
ruleId: id,
|
|
1298
2121
|
severity: "warning",
|
|
1299
2122
|
message: `\uD30C\uC77C\uBA85\uC774 kebab-case\uAC00 \uC544\uB2D8: ${name}`,
|
|
1300
|
-
file:
|
|
2123
|
+
file: path8.relative(cwd, filePath)
|
|
1301
2124
|
});
|
|
1302
2125
|
}
|
|
1303
2126
|
}
|
|
@@ -1313,8 +2136,8 @@ function createStructureRule(id, section, desc, expectedPath) {
|
|
|
1313
2136
|
type: "structure",
|
|
1314
2137
|
description: desc,
|
|
1315
2138
|
check: (cwd) => {
|
|
1316
|
-
const fullPath =
|
|
1317
|
-
if (!
|
|
2139
|
+
const fullPath = path8.join(cwd, expectedPath);
|
|
2140
|
+
if (!fs7.existsSync(fullPath)) {
|
|
1318
2141
|
return [{
|
|
1319
2142
|
ruleId: id,
|
|
1320
2143
|
severity: "error",
|
|
@@ -1334,11 +2157,11 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
1334
2157
|
pattern: new RegExp(escapeRegex(pattern), "i"),
|
|
1335
2158
|
check: (cwd) => {
|
|
1336
2159
|
const violations = [];
|
|
1337
|
-
const srcDir =
|
|
1338
|
-
if (!
|
|
2160
|
+
const srcDir = path8.join(cwd, "src");
|
|
2161
|
+
if (!fs7.existsSync(srcDir)) return violations;
|
|
1339
2162
|
const regex = new RegExp(escapeRegex(pattern), "i");
|
|
1340
2163
|
walkFiles(srcDir, (filePath) => {
|
|
1341
|
-
const fileContent =
|
|
2164
|
+
const fileContent = fs7.readFileSync(filePath, "utf-8");
|
|
1342
2165
|
const fileLines = fileContent.split("\n");
|
|
1343
2166
|
fileLines.forEach((line, idx) => {
|
|
1344
2167
|
if (regex.test(line)) {
|
|
@@ -1346,7 +2169,7 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
1346
2169
|
ruleId: id,
|
|
1347
2170
|
severity: type === "banned" ? "error" : "warning",
|
|
1348
2171
|
message: type === "banned" ? `\uAE08\uC9C0 \uD328\uD134 \uBC1C\uACAC: \`${pattern}\`` : `\uD544\uC218 \uD328\uD134 \uB204\uB77D: \`${pattern}\``,
|
|
1349
|
-
file:
|
|
2172
|
+
file: path8.relative(cwd, filePath),
|
|
1350
2173
|
line: idx + 1
|
|
1351
2174
|
});
|
|
1352
2175
|
}
|
|
@@ -1358,9 +2181,9 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
1358
2181
|
};
|
|
1359
2182
|
}
|
|
1360
2183
|
function walkFiles(dir, callback) {
|
|
1361
|
-
const entries =
|
|
2184
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
1362
2185
|
for (const entry of entries) {
|
|
1363
|
-
const fullPath =
|
|
2186
|
+
const fullPath = path8.join(dir, entry.name);
|
|
1364
2187
|
if (entry.isDirectory()) {
|
|
1365
2188
|
if (!["node_modules", ".git", "dist", ".next"].includes(entry.name)) {
|
|
1366
2189
|
walkFiles(fullPath, callback);
|
|
@@ -1376,22 +2199,22 @@ function escapeRegex(str) {
|
|
|
1376
2199
|
|
|
1377
2200
|
// src/commands/check.ts
|
|
1378
2201
|
async function check() {
|
|
1379
|
-
console.log(
|
|
2202
|
+
console.log(chalk7.bold(`
|
|
1380
2203
|
${ko.check.title}
|
|
1381
2204
|
`));
|
|
1382
2205
|
const cwd = process.cwd();
|
|
1383
|
-
const rulesPath =
|
|
1384
|
-
if (!
|
|
1385
|
-
console.log(
|
|
1386
|
-
console.log(
|
|
2206
|
+
const rulesPath = path9.join(cwd, "RULES.md");
|
|
2207
|
+
if (!fs8.existsSync(rulesPath)) {
|
|
2208
|
+
console.log(chalk7.yellow(ko.check.noRules));
|
|
2209
|
+
console.log(chalk7.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
|
|
1387
2210
|
return;
|
|
1388
2211
|
}
|
|
1389
2212
|
const rules = parseRules(rulesPath);
|
|
1390
|
-
console.log(
|
|
2213
|
+
console.log(chalk7.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
|
|
1391
2214
|
`));
|
|
1392
2215
|
if (rules.length === 0) {
|
|
1393
|
-
console.log(
|
|
1394
|
-
console.log(
|
|
2216
|
+
console.log(chalk7.yellow(ko.check.noAutoRules));
|
|
2217
|
+
console.log(chalk7.dim(" RULES.md\uC5D0 \uD30C\uC77C \uC774\uB984\xB7\uD3F4\uB354 \uADDC\uCE59\uC744 \uC801\uC73C\uBA74 \uC790\uB3D9\uC73C\uB85C \uC810\uAC80\uD574\uC694."));
|
|
1395
2218
|
return;
|
|
1396
2219
|
}
|
|
1397
2220
|
const allViolations = [];
|
|
@@ -1399,13 +2222,13 @@ ${ko.check.title}
|
|
|
1399
2222
|
for (const rule of rules) {
|
|
1400
2223
|
const violations = rule.check(cwd);
|
|
1401
2224
|
if (violations.length === 0) {
|
|
1402
|
-
console.log(
|
|
2225
|
+
console.log(chalk7.green(` \u2705 ${rule.id}`) + chalk7.dim(` \u2014 ${rule.description.slice(0, 60)}`));
|
|
1403
2226
|
passCount++;
|
|
1404
2227
|
} else {
|
|
1405
|
-
console.log(
|
|
2228
|
+
console.log(chalk7.red(` \u274C ${rule.id}`) + chalk7.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
|
|
1406
2229
|
violations.forEach((v) => {
|
|
1407
|
-
const loc = v.file ?
|
|
1408
|
-
const icon = v.severity === "error" ?
|
|
2230
|
+
const loc = v.file ? chalk7.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
|
|
2231
|
+
const icon = v.severity === "error" ? chalk7.red("\u2716") : v.severity === "warning" ? chalk7.yellow("\u26A0") : chalk7.blue("\u2139");
|
|
1409
2232
|
console.log(` ${icon} ${v.message}${loc}`);
|
|
1410
2233
|
});
|
|
1411
2234
|
allViolations.push(...violations);
|
|
@@ -1415,14 +2238,22 @@ ${ko.check.title}
|
|
|
1415
2238
|
const errors = allViolations.filter((v) => v.severity === "error").length;
|
|
1416
2239
|
const warnings = allViolations.filter((v) => v.severity === "warning").length;
|
|
1417
2240
|
if (allViolations.length === 0) {
|
|
1418
|
-
console.log(
|
|
2241
|
+
console.log(chalk7.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
|
|
2242
|
+
printNextStep({
|
|
2243
|
+
message: "\uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC! \uBCF4\uC548 \uC2A4\uCE94\uB3C4 \uD574\uBCFC\uAE4C\uC694?",
|
|
2244
|
+
command: "vhk \uBCF4\uC548 scan",
|
|
2245
|
+
cursorHint: "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824\uC918"
|
|
2246
|
+
});
|
|
1419
2247
|
} else {
|
|
1420
|
-
console.log(
|
|
1421
|
-
console.log(` \uADDC\uCE59: ${
|
|
1422
|
-
if (errors > 0) console.log(` ${
|
|
1423
|
-
if (warnings > 0) console.log(` ${
|
|
1424
|
-
|
|
1425
|
-
|
|
2248
|
+
console.log(chalk7.bold(ko.check.summary));
|
|
2249
|
+
console.log(` \uADDC\uCE59: ${chalk7.cyan(String(rules.length))}\uAC1C | \uD1B5\uACFC: ${chalk7.green(String(passCount))}\uAC1C | \uC704\uBC18: ${chalk7.red(String(allViolations.length))}\uAC74`);
|
|
2250
|
+
if (errors > 0) console.log(` ${chalk7.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
|
|
2251
|
+
if (warnings > 0) console.log(` ${chalk7.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
|
|
2252
|
+
printNextStep({
|
|
2253
|
+
message: "\uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
|
|
2254
|
+
command: "vhk \uC810\uAC80",
|
|
2255
|
+
cursorHint: "\uC704\uBC18 \uD56D\uBAA9 \uC218\uC815\uD574\uC918"
|
|
2256
|
+
});
|
|
1426
2257
|
}
|
|
1427
2258
|
if (errors > 0) {
|
|
1428
2259
|
process.exitCode = 1;
|
|
@@ -1430,9 +2261,9 @@ ${ko.check.title}
|
|
|
1430
2261
|
}
|
|
1431
2262
|
|
|
1432
2263
|
// src/commands/secure.ts
|
|
1433
|
-
import
|
|
1434
|
-
import
|
|
1435
|
-
import
|
|
2264
|
+
import chalk8 from "chalk";
|
|
2265
|
+
import fs10 from "fs";
|
|
2266
|
+
import path11 from "path";
|
|
1436
2267
|
|
|
1437
2268
|
// src/lib/secret-patterns.ts
|
|
1438
2269
|
var SECRET_PATTERNS = [
|
|
@@ -1497,9 +2328,32 @@ function maskSecret(value) {
|
|
|
1497
2328
|
return value.slice(0, visible) + "****";
|
|
1498
2329
|
}
|
|
1499
2330
|
|
|
1500
|
-
// src/
|
|
1501
|
-
|
|
1502
|
-
|
|
2331
|
+
// src/lib/scan-files.ts
|
|
2332
|
+
import fs9 from "fs";
|
|
2333
|
+
import path10 from "path";
|
|
2334
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
2335
|
+
"node_modules",
|
|
2336
|
+
".git",
|
|
2337
|
+
"dist",
|
|
2338
|
+
".next",
|
|
2339
|
+
".nuxt",
|
|
2340
|
+
"build",
|
|
2341
|
+
"coverage",
|
|
2342
|
+
"out",
|
|
2343
|
+
".turbo",
|
|
2344
|
+
".vercel",
|
|
2345
|
+
".cache",
|
|
2346
|
+
".pnpm",
|
|
2347
|
+
".idea",
|
|
2348
|
+
".claude",
|
|
2349
|
+
".cursor"
|
|
2350
|
+
]);
|
|
2351
|
+
var SKIP_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
2352
|
+
"pnpm-lock.yaml",
|
|
2353
|
+
"package-lock.json",
|
|
2354
|
+
"yarn.lock"
|
|
2355
|
+
]);
|
|
2356
|
+
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1503
2357
|
".ts",
|
|
1504
2358
|
".tsx",
|
|
1505
2359
|
".js",
|
|
@@ -1509,40 +2363,80 @@ var SCAN_EXTENSIONS = [
|
|
|
1509
2363
|
".json",
|
|
1510
2364
|
".yaml",
|
|
1511
2365
|
".yml",
|
|
1512
|
-
".toml"
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
2366
|
+
".toml"
|
|
2367
|
+
]);
|
|
2368
|
+
var MAX_SCAN_FILE_BYTES = 512 * 1024;
|
|
2369
|
+
function isScannableFileName(fileName) {
|
|
2370
|
+
if (SKIP_FILE_NAMES.has(fileName)) return false;
|
|
2371
|
+
if (fileName.startsWith(".env")) return true;
|
|
2372
|
+
return SCAN_EXTENSIONS.has(path10.extname(fileName).toLowerCase());
|
|
2373
|
+
}
|
|
2374
|
+
function walkProjectFiles(rootDir, onFile, ig = loadGitignore(rootDir)) {
|
|
2375
|
+
function walk(dir) {
|
|
2376
|
+
let entries;
|
|
2377
|
+
try {
|
|
2378
|
+
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
2379
|
+
} catch {
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
for (const entry of entries) {
|
|
2383
|
+
const fullPath = path10.join(dir, entry.name);
|
|
2384
|
+
const rel = path10.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
2385
|
+
if (entry.isDirectory()) {
|
|
2386
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
2387
|
+
if (isPathIgnored(ig, `${rel}/`)) continue;
|
|
2388
|
+
walk(fullPath);
|
|
2389
|
+
continue;
|
|
2390
|
+
}
|
|
2391
|
+
if (!isScannableFileName(entry.name)) continue;
|
|
2392
|
+
if (isPathIgnored(ig, rel)) continue;
|
|
2393
|
+
let size = 0;
|
|
2394
|
+
try {
|
|
2395
|
+
size = fs9.statSync(fullPath).size;
|
|
2396
|
+
} catch {
|
|
2397
|
+
continue;
|
|
2398
|
+
}
|
|
2399
|
+
if (size > MAX_SCAN_FILE_BYTES) continue;
|
|
2400
|
+
onFile(fullPath, rel);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
walk(rootDir);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// src/commands/secure.ts
|
|
2407
|
+
var MAX_FINDINGS = 200;
|
|
2408
|
+
var MAX_LINE_CHARS = 4e3;
|
|
1518
2409
|
async function secure() {
|
|
1519
|
-
console.log(
|
|
2410
|
+
console.log(chalk8.bold(`
|
|
1520
2411
|
${ko.secure.title}
|
|
1521
2412
|
`));
|
|
1522
2413
|
const cwd = process.cwd();
|
|
1523
2414
|
const findings = [];
|
|
1524
2415
|
let scannedFiles = 0;
|
|
1525
|
-
|
|
1526
|
-
const
|
|
2416
|
+
let truncated = false;
|
|
2417
|
+
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
2418
|
+
const hasGitignore = fs10.existsSync(gitignorePath);
|
|
1527
2419
|
if (!hasGitignore) {
|
|
1528
|
-
console.log(
|
|
1529
|
-
console.log(
|
|
2420
|
+
console.log(chalk8.yellow(` ${ko.secure.noGitignore}`));
|
|
2421
|
+
console.log(chalk8.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
|
|
1530
2422
|
} else {
|
|
1531
|
-
const gitignoreContent =
|
|
2423
|
+
const gitignoreContent = fs10.readFileSync(gitignorePath, "utf-8");
|
|
1532
2424
|
if (!gitignoreContent.includes(".env")) {
|
|
1533
|
-
console.log(
|
|
1534
|
-
console.log(
|
|
2425
|
+
console.log(chalk8.yellow(` ${ko.secure.noEnvInGitignore}`));
|
|
2426
|
+
console.log(chalk8.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
|
|
1535
2427
|
}
|
|
1536
2428
|
}
|
|
1537
|
-
console.log(
|
|
2429
|
+
console.log(chalk8.dim(` ${ko.secure.scanning}
|
|
1538
2430
|
`));
|
|
1539
|
-
|
|
2431
|
+
walkProjectFiles(cwd, (filePath, relPath) => {
|
|
1540
2432
|
scannedFiles++;
|
|
1541
|
-
const content =
|
|
2433
|
+
const content = fs10.readFileSync(filePath, "utf-8");
|
|
1542
2434
|
const lines = content.split("\n");
|
|
1543
|
-
const relPath = path8.relative(cwd, filePath);
|
|
1544
2435
|
for (const pattern of SECRET_PATTERNS) {
|
|
2436
|
+
if (truncated) break;
|
|
1545
2437
|
lines.forEach((line, idx) => {
|
|
2438
|
+
if (truncated) return;
|
|
2439
|
+
if (line.length > MAX_LINE_CHARS) return;
|
|
1546
2440
|
const trimmed = line.trim();
|
|
1547
2441
|
if (trimmed.startsWith("//") && trimmed.includes("example")) return;
|
|
1548
2442
|
if (trimmed.startsWith("#") && trimmed.includes("example")) return;
|
|
@@ -1557,80 +2451,410 @@ ${ko.secure.title}
|
|
|
1557
2451
|
line: idx + 1,
|
|
1558
2452
|
match: maskSecret(match[0])
|
|
1559
2453
|
});
|
|
2454
|
+
if (findings.length >= MAX_FINDINGS) {
|
|
2455
|
+
truncated = true;
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
1560
2458
|
}
|
|
1561
2459
|
});
|
|
1562
2460
|
}
|
|
1563
2461
|
});
|
|
1564
|
-
console.log(
|
|
1565
|
-
|
|
2462
|
+
console.log(chalk8.dim(` \u{1F4C2} ${scannedFiles}\uAC1C \uD30C\uC77C \uC2A4\uCE94 \uC644\uB8CC (lock\xB7node_modules\xB7>${MAX_SCAN_FILE_BYTES / 1024}KB \uC81C\uC678)`));
|
|
2463
|
+
if (truncated) {
|
|
2464
|
+
console.log(chalk8.yellow(` \u26A0\uFE0F \uACB0\uACFC ${MAX_FINDINGS}\uAC74\uC5D0\uC11C \uCD9C\uB825\uC744 \uC81C\uD55C\uD588\uC2B5\uB2C8\uB2E4. lock \uD30C\uC77C \uB4F1\uC740 \uC790\uB3D9 \uC81C\uC678\uB429\uB2C8\uB2E4.`));
|
|
2465
|
+
}
|
|
2466
|
+
console.log("");
|
|
1566
2467
|
if (findings.length === 0) {
|
|
1567
|
-
console.log(
|
|
1568
|
-
|
|
2468
|
+
console.log(chalk8.green.bold(` ${ko.secure.clean}`));
|
|
2469
|
+
printNextStep({
|
|
2470
|
+
message: "\uBCF4\uC548 \uC774\uC0C1 \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
|
|
2471
|
+
command: "vhk \uC815\uB9AC",
|
|
2472
|
+
cursorHint: "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD574\uC918"
|
|
2473
|
+
});
|
|
1569
2474
|
return;
|
|
1570
2475
|
}
|
|
1571
2476
|
const critical = findings.filter((f) => f.severity === "critical");
|
|
1572
2477
|
const high = findings.filter((f) => f.severity === "high");
|
|
1573
2478
|
const medium = findings.filter((f) => f.severity === "medium");
|
|
1574
2479
|
if (critical.length > 0) {
|
|
1575
|
-
console.log(
|
|
2480
|
+
console.log(chalk8.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
|
|
1576
2481
|
critical.forEach((f) => {
|
|
1577
|
-
console.log(
|
|
1578
|
-
console.log(
|
|
2482
|
+
console.log(chalk8.red(` \u2716 ${f.patternName}`));
|
|
2483
|
+
console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
1579
2484
|
});
|
|
1580
2485
|
console.log("");
|
|
1581
2486
|
}
|
|
1582
2487
|
if (high.length > 0) {
|
|
1583
|
-
console.log(
|
|
2488
|
+
console.log(chalk8.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
|
|
1584
2489
|
high.forEach((f) => {
|
|
1585
|
-
console.log(
|
|
1586
|
-
console.log(
|
|
2490
|
+
console.log(chalk8.yellow(` \u26A0 ${f.patternName}`));
|
|
2491
|
+
console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
1587
2492
|
});
|
|
1588
2493
|
console.log("");
|
|
1589
2494
|
}
|
|
1590
2495
|
if (medium.length > 0) {
|
|
1591
|
-
console.log(
|
|
2496
|
+
console.log(chalk8.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
|
|
1592
2497
|
medium.forEach((f) => {
|
|
1593
|
-
console.log(
|
|
1594
|
-
console.log(
|
|
2498
|
+
console.log(chalk8.blue(` \u2139 ${f.patternName}`));
|
|
2499
|
+
console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
1595
2500
|
});
|
|
1596
2501
|
console.log("");
|
|
1597
2502
|
}
|
|
1598
|
-
console.log(
|
|
1599
|
-
console.log(` \uCD1D ${
|
|
2503
|
+
console.log(chalk8.bold(` ${ko.secure.summary}`));
|
|
2504
|
+
console.log(` \uCD1D ${chalk8.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
|
|
1600
2505
|
console.log("");
|
|
1601
|
-
console.log(
|
|
1602
|
-
console.log(
|
|
1603
|
-
console.log(
|
|
1604
|
-
console.log(
|
|
2506
|
+
console.log(chalk8.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
|
|
2507
|
+
console.log(chalk8.dim(" 1. \uD574\uB2F9 \uD30C\uC77C\uC5D0\uC11C \uC2DC\uD06C\uB9BF\uC744 \uC81C\uAC70\uD558\uACE0 \uD658\uACBD\uBCC0\uC218\uB85C \uC774\uB3D9"));
|
|
2508
|
+
console.log(chalk8.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
|
|
2509
|
+
console.log(chalk8.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
|
|
1605
2510
|
if (critical.length > 0) {
|
|
1606
2511
|
process.exitCode = 1;
|
|
1607
2512
|
}
|
|
1608
2513
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
2514
|
+
|
|
2515
|
+
// src/commands/doctor.ts
|
|
2516
|
+
import chalk9 from "chalk";
|
|
2517
|
+
import { execSync } from "child_process";
|
|
2518
|
+
import fs11 from "fs";
|
|
2519
|
+
import path12 from "path";
|
|
2520
|
+
import { fileURLToPath } from "url";
|
|
2521
|
+
function checkCommand(name, command, hint) {
|
|
2522
|
+
try {
|
|
2523
|
+
const version = execSync(`${command} --version`, { encoding: "utf-8" }).trim().split("\n")[0];
|
|
2524
|
+
return { name, command, version, ok: true, hint };
|
|
2525
|
+
} catch {
|
|
2526
|
+
return { name, command, ok: false, hint };
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
function getVhkVersion() {
|
|
2530
|
+
const dir = path12.dirname(fileURLToPath(import.meta.url));
|
|
2531
|
+
const candidates = [
|
|
2532
|
+
path12.join(dir, "../package.json"),
|
|
2533
|
+
path12.join(dir, "../../package.json")
|
|
2534
|
+
];
|
|
2535
|
+
for (const pkgPath of candidates) {
|
|
2536
|
+
try {
|
|
2537
|
+
if (fs11.existsSync(pkgPath)) {
|
|
2538
|
+
const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
|
|
2539
|
+
return pkg.version;
|
|
1616
2540
|
}
|
|
2541
|
+
} catch {
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
return void 0;
|
|
2546
|
+
}
|
|
2547
|
+
async function doctor() {
|
|
2548
|
+
console.log(chalk9.bold(`
|
|
2549
|
+
${ko.doctor.title}
|
|
2550
|
+
`));
|
|
2551
|
+
const checks = [
|
|
2552
|
+
checkCommand("Node.js", "node", "\uC124\uCE58: https://nodejs.org (LTS \uAD8C\uC7A5)"),
|
|
2553
|
+
checkCommand("npm", "npm", "Node.js \uC124\uCE58 \uC2DC \uD568\uAED8 \uC124\uCE58\uB429\uB2C8\uB2E4"),
|
|
2554
|
+
checkCommand("pnpm", "pnpm", "\uC124\uCE58: npm i -g pnpm"),
|
|
2555
|
+
checkCommand("Git", "git", "\uC124\uCE58: https://git-scm.com")
|
|
2556
|
+
];
|
|
2557
|
+
let allOk = true;
|
|
2558
|
+
for (const check2 of checks) {
|
|
2559
|
+
if (check2.ok) {
|
|
2560
|
+
console.log(chalk9.green(` \u2705 ${check2.name}`) + chalk9.dim(` \u2014 ${check2.version}`));
|
|
1617
2561
|
} else {
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
2562
|
+
console.log(chalk9.red(` \u274C ${check2.name} \uC5C6\uC74C`));
|
|
2563
|
+
console.log(chalk9.dim(` \u2192 ${check2.hint}`));
|
|
2564
|
+
allOk = false;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
console.log("");
|
|
2568
|
+
const vhkVersion = getVhkVersion();
|
|
2569
|
+
if (vhkVersion) {
|
|
2570
|
+
console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(` \u2014 v${vhkVersion}`));
|
|
2571
|
+
} else {
|
|
2572
|
+
console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(" \u2014 \uC124\uCE58\uB428"));
|
|
2573
|
+
}
|
|
2574
|
+
console.log("");
|
|
2575
|
+
console.log(chalk9.bold(` ${ko.doctor.projectFiles}`));
|
|
2576
|
+
const cwd = process.cwd();
|
|
2577
|
+
const projectFiles = [
|
|
2578
|
+
{ name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
|
|
2579
|
+
{ name: "COMMANDS.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
|
|
2580
|
+
{ name: "package.json", hint: "\uD504\uB85C\uC81D\uD2B8 \uD3F4\uB354\uC5D0\uC11C \uC2E4\uD589\uD558\uC138\uC694" },
|
|
2581
|
+
{ name: ".gitignore", hint: "\uBCF4\uC548\uC744 \uC704\uD574 \uCD94\uAC00 \uAD8C\uC7A5" },
|
|
2582
|
+
{ name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
|
|
2583
|
+
];
|
|
2584
|
+
for (const file of projectFiles) {
|
|
2585
|
+
const exists = fs11.existsSync(path12.join(cwd, file.name));
|
|
2586
|
+
if (exists) {
|
|
2587
|
+
console.log(chalk9.green(` \u2705 ${file.name}`));
|
|
2588
|
+
if (file.name === ".env") {
|
|
2589
|
+
const gitignorePath = path12.join(cwd, ".gitignore");
|
|
2590
|
+
if (fs11.existsSync(gitignorePath)) {
|
|
2591
|
+
const gitignore = fs11.readFileSync(gitignorePath, "utf-8");
|
|
2592
|
+
if (!gitignore.includes(".env")) {
|
|
2593
|
+
console.log(chalk9.yellow(` ${ko.doctor.envNotIgnored}`));
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
1621
2596
|
}
|
|
2597
|
+
} else {
|
|
2598
|
+
console.log(chalk9.dim(` \u2B1A ${file.name}`) + chalk9.dim(` \u2014 ${file.hint}`));
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
console.log("");
|
|
2602
|
+
if (allOk) {
|
|
2603
|
+
console.log(chalk9.green.bold(` ${ko.doctor.allOk}`));
|
|
2604
|
+
printNextStep({
|
|
2605
|
+
message: ko.doctor.nextOkMessage,
|
|
2606
|
+
command: "vhk \uC2DC\uC791",
|
|
2607
|
+
cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
|
|
2608
|
+
});
|
|
2609
|
+
} else {
|
|
2610
|
+
console.log(chalk9.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
|
|
2611
|
+
printNextStep({
|
|
2612
|
+
message: ko.doctor.nextRetryMessage,
|
|
2613
|
+
command: "vhk doctor",
|
|
2614
|
+
cursorHint: "\uD658\uACBD \uB2E4\uC2DC \uC810\uAC80\uD574\uC918"
|
|
2615
|
+
});
|
|
2616
|
+
process.exitCode = 1;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
// src/commands/ship.ts
|
|
2621
|
+
import chalk10 from "chalk";
|
|
2622
|
+
import inquirer4 from "inquirer";
|
|
2623
|
+
import fs12 from "fs";
|
|
2624
|
+
import path13 from "path";
|
|
2625
|
+
var CHECKLIST = [
|
|
2626
|
+
{ id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
|
|
2627
|
+
{ id: "test", questionKey: "checkTest", hintKey: "hintTest" },
|
|
2628
|
+
{ id: "version", questionKey: "checkVersion", hintKey: "hintVersion" },
|
|
2629
|
+
{ id: "changelog", questionKey: "checkChangelog", hintKey: "hintChangelog" },
|
|
2630
|
+
{ id: "security", questionKey: "checkSecurity", hintKey: "hintSecurity" },
|
|
2631
|
+
{ id: "commit", questionKey: "checkCommit", hintKey: "hintCommit" }
|
|
2632
|
+
];
|
|
2633
|
+
function sanitizeVersion(version) {
|
|
2634
|
+
return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
|
|
2635
|
+
}
|
|
2636
|
+
async function ship() {
|
|
2637
|
+
console.log(chalk10.bold(`
|
|
2638
|
+
${ko.ship.title}
|
|
2639
|
+
`));
|
|
2640
|
+
const cwd = process.cwd();
|
|
2641
|
+
console.log(chalk10.cyan.bold(` ${ko.ship.checklist}
|
|
2642
|
+
`));
|
|
2643
|
+
const { passed } = await inquirer4.prompt([{
|
|
2644
|
+
type: "checkbox",
|
|
2645
|
+
name: "passed",
|
|
2646
|
+
message: ko.ship.checkboxPrompt,
|
|
2647
|
+
choices: CHECKLIST.map((c) => ({
|
|
2648
|
+
name: `${ko.ship[c.questionKey]} ${chalk10.dim(`(${ko.ship[c.hintKey]})`)}`,
|
|
2649
|
+
value: c.id
|
|
2650
|
+
}))
|
|
2651
|
+
}]);
|
|
2652
|
+
const allPassed = passed.length === CHECKLIST.length;
|
|
2653
|
+
const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
|
|
2654
|
+
if (!allPassed) {
|
|
2655
|
+
console.log(chalk10.yellow(`
|
|
2656
|
+
${ko.ship.incompleteHeader}`));
|
|
2657
|
+
skipped.forEach((s) => {
|
|
2658
|
+
console.log(chalk10.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
|
|
2659
|
+
console.log(chalk10.dim(` \u2192 ${ko.ship[s.hintKey]}`));
|
|
2660
|
+
});
|
|
2661
|
+
const { proceed } = await inquirer4.prompt([{
|
|
2662
|
+
type: "confirm",
|
|
2663
|
+
name: "proceed",
|
|
2664
|
+
message: ko.ship.proceedConfirm,
|
|
2665
|
+
default: false
|
|
2666
|
+
}]);
|
|
2667
|
+
if (!proceed) {
|
|
2668
|
+
printNextStep({
|
|
2669
|
+
message: ko.ship.retryMessage,
|
|
2670
|
+
command: "vhk \uBC30\uD3EC",
|
|
2671
|
+
cursorHint: ko.ship.retryCursorHint
|
|
2672
|
+
});
|
|
2673
|
+
return;
|
|
1622
2674
|
}
|
|
2675
|
+
} else {
|
|
2676
|
+
console.log(chalk10.green(`
|
|
2677
|
+
${ko.ship.allPassed}
|
|
2678
|
+
`));
|
|
1623
2679
|
}
|
|
2680
|
+
console.log(chalk10.cyan.bold(` ${ko.ship.retro}
|
|
2681
|
+
`));
|
|
2682
|
+
console.log(chalk10.dim(` ${ko.ship.versionHint}`));
|
|
2683
|
+
const retro = await inquirer4.prompt([
|
|
2684
|
+
{ type: "input", name: "version", message: ko.ship.versionPrompt },
|
|
2685
|
+
{ type: "input", name: "whatWentWell", message: ko.ship.questionWell },
|
|
2686
|
+
{ type: "input", name: "whatWentWrong", message: ko.ship.questionWrong },
|
|
2687
|
+
{ type: "input", name: "learned", message: ko.ship.questionLearned },
|
|
2688
|
+
{ type: "input", name: "nextVersion", message: ko.ship.questionNext }
|
|
2689
|
+
]);
|
|
2690
|
+
const buildLogDir = path13.join(cwd, "docs", "build-log");
|
|
2691
|
+
if (!fs12.existsSync(buildLogDir)) fs12.mkdirSync(buildLogDir, { recursive: true });
|
|
2692
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2693
|
+
const versionSlug = sanitizeVersion(retro.version);
|
|
2694
|
+
const fileName = `${today}-v${versionSlug}.md`;
|
|
2695
|
+
const filePath = path13.join(buildLogDir, fileName);
|
|
2696
|
+
const content = [
|
|
2697
|
+
`# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
|
|
2698
|
+
"",
|
|
2699
|
+
`**\uBC30\uD3EC\uC77C:** ${today}`,
|
|
2700
|
+
`**\uBC84\uC804:** ${versionSlug}`,
|
|
2701
|
+
"",
|
|
2702
|
+
"## \uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
|
|
2703
|
+
...CHECKLIST.map(
|
|
2704
|
+
(c) => `- [${passed.includes(c.id) ? "x" : " "}] ${ko.ship[c.questionKey]}`
|
|
2705
|
+
),
|
|
2706
|
+
"",
|
|
2707
|
+
"## \uD68C\uACE0",
|
|
2708
|
+
"",
|
|
2709
|
+
"### \u2705 \uC798\uB41C \uC810",
|
|
2710
|
+
retro.whatWentWell || ko.ship.emptySection,
|
|
2711
|
+
"",
|
|
2712
|
+
"### \u274C \uC5B4\uB824\uC6E0\uB358 \uC810",
|
|
2713
|
+
retro.whatWentWrong || ko.ship.emptySection,
|
|
2714
|
+
"",
|
|
2715
|
+
"### \u{1F4A1} \uBC30\uC6B4 \uC810",
|
|
2716
|
+
retro.learned || ko.ship.emptySection,
|
|
2717
|
+
"",
|
|
2718
|
+
"### \u{1F52E} \uB2E4\uC74C \uBC84\uC804",
|
|
2719
|
+
retro.nextVersion || ko.ship.emptyNext,
|
|
2720
|
+
"",
|
|
2721
|
+
"---",
|
|
2722
|
+
`*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
2723
|
+
].join("\n");
|
|
2724
|
+
fs12.writeFileSync(filePath, content, "utf-8");
|
|
2725
|
+
console.log(chalk10.green(`
|
|
2726
|
+
${ko.ship.buildLogDone(path13.relative(cwd, filePath))}`));
|
|
2727
|
+
printNextStep({
|
|
2728
|
+
message: ko.ship.deployMessage,
|
|
2729
|
+
command: "npm publish --access=public",
|
|
2730
|
+
cursorHint: ko.ship.deployCursorHint,
|
|
2731
|
+
alternative: `GitHub \uD0DC\uADF8\uB3C4 \uB9CC\uB4E4\uBA74 \uC88B\uC544\uC694: git tag v${versionSlug}`
|
|
2732
|
+
});
|
|
1624
2733
|
}
|
|
1625
2734
|
|
|
1626
2735
|
// src/index.ts
|
|
1627
2736
|
var program = new Command();
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
2737
|
+
var defaultHelp = new Help();
|
|
2738
|
+
var KO_ALIASES = {
|
|
2739
|
+
gate: "\uAC80\uC99D",
|
|
2740
|
+
init: "\uC2DC\uC791",
|
|
2741
|
+
recap: "\uC815\uB9AC",
|
|
2742
|
+
sync: "\uADDC\uCE59",
|
|
2743
|
+
check: "\uC810\uAC80",
|
|
2744
|
+
secure: "\uBCF4\uC548",
|
|
2745
|
+
ship: "\uBC30\uD3EC",
|
|
2746
|
+
doctor: "\uD658\uACBD"
|
|
2747
|
+
};
|
|
2748
|
+
program.name("vhk").description("VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58 (\uD55C\uAD6D\uC5B4\uB85C \uC548\uB0B4\uD569\uB2C8\uB2E4)").version("0.4.0");
|
|
2749
|
+
program.configureHelp({
|
|
2750
|
+
formatHelp(cmd, helper) {
|
|
2751
|
+
if (cmd.parent) {
|
|
2752
|
+
return defaultHelp.formatHelp(cmd, helper);
|
|
2753
|
+
}
|
|
2754
|
+
const subs = helper.visibleCommands(cmd).filter((c) => c.name() !== "help");
|
|
2755
|
+
const terms = subs.map((c) => `${c.name()} (${KO_ALIASES[c.name()]})`);
|
|
2756
|
+
const termWidth = Math.max(...terms.map((t) => t.length), 0);
|
|
2757
|
+
const lines = [
|
|
2758
|
+
helper.commandDescription(cmd),
|
|
2759
|
+
"",
|
|
2760
|
+
"\uBA85\uB839\uC5B4:",
|
|
2761
|
+
...subs.map((sub, i) => {
|
|
2762
|
+
const term = terms[i].padEnd(termWidth + 2);
|
|
2763
|
+
return ` ${term}${sub.description()}`;
|
|
2764
|
+
})
|
|
2765
|
+
];
|
|
2766
|
+
return lines.join("\n") + "\n";
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
program.command("gate").alias("\uAC80\uC99D").alias("\uC544\uC774\uB514\uC5B4").description("\uC544\uC774\uB514\uC5B4 \uAC80\uC99D \u2192 \uC2DC\uC791\uD574\uB3C4 \uB3FC\uC694 / \uB2E4\uB4EC\uAE30 / \uB2E4\uB978 \uC544\uC774\uB514\uC5B4").action(gate);
|
|
2770
|
+
program.command("init").alias("\uC2DC\uC791").alias("\uB9CC\uB4E4\uAE30").description("\uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791\uD558\uAE30 \u2014 \uD3F4\uB354 + \uD558\uB124\uC2A4 \uD30C\uC77C \uC790\uB3D9 \uC0DD\uC131").option("--skip-gate", "gate \uAC80\uC99D \uC2A4\uD0B5").option("--from-notion <url>", "Notion PRD \uD398\uC774\uC9C0\uC5D0\uC11C import").option("--name <name>", "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984").option("--description <desc>", "\uD55C \uC904 \uC124\uBA85").option("--type <type>", "\uD504\uB85C\uC81D\uD2B8 \uC720\uD615 (webapp|extension|cli|notion|mobile)").option("-y, --yes", "\uC2A4\uD0DD \uD655\uC778 \uC2A4\uD0B5").action(init);
|
|
2771
|
+
program.command("recap").alias("\uC815\uB9AC").alias("\uC624\uB298").description("\uC624\uB298 \uD55C \uC77C \uC815\uB9AC + ADR/\uD2B8\uB7EC\uBE14\uC288\uD305 \uC790\uB3D9 \uBD84\uB9AC").option("--since <date>", "\uBD84\uC11D \uC2DC\uC791\uC77C (YYYY-MM-DD)").action(recap);
|
|
2772
|
+
program.command("sync").alias("\uB9DE\uCD94\uAE30").alias("\uADDC\uCE59").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654").action(sync);
|
|
2773
|
+
program.command("check").alias("\uC810\uAC80").alias("\uB9B0\uD2B8").description("RULES.md \uADDC\uCE59 \uC810\uAC80 \u2014 \uCF54\uB4DC \uC704\uBC18 \uAC80\uC0AC").action(check);
|
|
2774
|
+
var secureCmd = program.command("secure").alias("\uBCF4\uC548").description("\uBCF4\uC548 \uB3C4\uAD6C \uBAA8\uC74C");
|
|
2775
|
+
secureCmd.command("scan").alias("\uC2A4\uCE94").description("\uC2DC\uD06C\uB9BF/\uD0A4 \uC720\uCD9C \uC2A4\uCE94").action(secure);
|
|
2776
|
+
program.command("ship").alias("\uBC30\uD3EC").alias("\uB9B4\uB9AC\uC988").description("\uBC30\uD3EC \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 + \uD68C\uACE0 + \uBE4C\uB4DC \uB85C\uADF8 \uC0DD\uC131").action(ship);
|
|
2777
|
+
program.command("doctor").alias("\uD658\uACBD").alias("\uC9C4\uB2E8").description("\uAC1C\uBC1C \uD658\uACBD \uC810\uAC80 \u2014 Node/Git/npm \uC0C1\uD0DC \uD655\uC778").action(doctor);
|
|
2778
|
+
program.on("command:*", async (operands) => {
|
|
2779
|
+
const input = operands.join(" ");
|
|
2780
|
+
const route = routeNaturalLanguage(input);
|
|
2781
|
+
if (route) {
|
|
2782
|
+
console.log("");
|
|
2783
|
+
console.log(chalk11.cyan(` \u{1F4AC} "${input}"`));
|
|
2784
|
+
console.log(chalk11.cyan(` \u2192 ${route.explanation}`));
|
|
2785
|
+
if (route.confidence === "low") {
|
|
2786
|
+
const { confirm } = await inquirer5.prompt([{
|
|
2787
|
+
type: "confirm",
|
|
2788
|
+
name: "confirm",
|
|
2789
|
+
message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
|
|
2790
|
+
default: true
|
|
2791
|
+
}]);
|
|
2792
|
+
if (!confirm) {
|
|
2793
|
+
console.log(chalk11.dim(` ${ko.nlp.menuHint}`));
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
console.log("");
|
|
2798
|
+
switch (route.command) {
|
|
2799
|
+
case "gate":
|
|
2800
|
+
return gate();
|
|
2801
|
+
case "init":
|
|
2802
|
+
return init({
|
|
2803
|
+
skipGate: route.args?.includes("--skip-gate"),
|
|
2804
|
+
fromNotion: route.args?.includes("--from-notion") ? extractNotionUrl(input) : void 0
|
|
2805
|
+
});
|
|
2806
|
+
case "recap":
|
|
2807
|
+
return recap({});
|
|
2808
|
+
case "sync":
|
|
2809
|
+
return sync();
|
|
2810
|
+
case "check":
|
|
2811
|
+
return check();
|
|
2812
|
+
case "secure":
|
|
2813
|
+
return secure();
|
|
2814
|
+
case "ship":
|
|
2815
|
+
return ship();
|
|
2816
|
+
case "doctor":
|
|
2817
|
+
return doctor();
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
console.log(chalk11.yellow(`
|
|
2821
|
+
\u2753 "${input}" \u2014 ${ko.nlp.notMatched}
|
|
2822
|
+
`));
|
|
2823
|
+
});
|
|
2824
|
+
program.action(async () => {
|
|
2825
|
+
console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
|
|
2826
|
+
const { choice } = await inquirer5.prompt([{
|
|
2827
|
+
type: "list",
|
|
2828
|
+
name: "choice",
|
|
2829
|
+
message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
|
|
2830
|
+
choices: [
|
|
2831
|
+
{ name: "\u{1F4A1} \uC0C8 \uC544\uC774\uB514\uC5B4 \uAC80\uC99D\uD558\uAE30", value: "gate" },
|
|
2832
|
+
{ name: "\u{1F4E6} \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791\uD558\uAE30", value: "init" },
|
|
2833
|
+
{ name: "\u{1F4DD} \uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD558\uAE30", value: "recap" },
|
|
2834
|
+
{ name: "\u{1F50D} \uADDC\uCE59 \uD30C\uC77C \uC810\uAC80\uD558\uAE30", value: "check" },
|
|
2835
|
+
{ name: "\u{1F512} \uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB9AC\uAE30", value: "secure" },
|
|
2836
|
+
{ name: "\u{1F504} \uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654", value: "sync" },
|
|
2837
|
+
{ name: "\u{1F680} \uBC30\uD3EC\uD558\uAE30", value: "ship" },
|
|
2838
|
+
{ name: "\u{1FA7A} \uD658\uACBD \uC810\uAC80\uD558\uAE30", value: "doctor" }
|
|
2839
|
+
]
|
|
2840
|
+
}]);
|
|
2841
|
+
switch (choice) {
|
|
2842
|
+
case "gate":
|
|
2843
|
+
return gate();
|
|
2844
|
+
case "init":
|
|
2845
|
+
return init({ skipGate: false });
|
|
2846
|
+
case "recap":
|
|
2847
|
+
return recap({});
|
|
2848
|
+
case "check":
|
|
2849
|
+
return check();
|
|
2850
|
+
case "secure":
|
|
2851
|
+
return secure();
|
|
2852
|
+
case "sync":
|
|
2853
|
+
return sync();
|
|
2854
|
+
case "doctor":
|
|
2855
|
+
return doctor();
|
|
2856
|
+
case "ship":
|
|
2857
|
+
return ship();
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
1636
2860
|
program.parse();
|