@byh3071/vhk 1.0.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -23
- package/dist/chunk-6S3JYYZ3.js +2155 -0
- package/dist/index.js +1246 -1530
- package/dist/mcp/index.js +1 -1
- package/package.json +14 -15
- package/dist/chunk-SD7O6HO7.js +0 -826
package/dist/index.js
CHANGED
|
@@ -1,479 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
MAX_SCAN_FILE_BYTES,
|
|
4
|
+
MAX_SECRET_FINDINGS,
|
|
5
|
+
audit,
|
|
6
|
+
deploy,
|
|
5
7
|
env,
|
|
6
8
|
envCheck,
|
|
9
|
+
filterSevereFindings,
|
|
10
|
+
filterTrackedPaths,
|
|
11
|
+
getVhkVersion,
|
|
7
12
|
ko,
|
|
8
13
|
printNextStep,
|
|
14
|
+
printSecurityWarnings,
|
|
15
|
+
publish,
|
|
9
16
|
readJsonFile,
|
|
10
17
|
safeExecFile,
|
|
11
|
-
|
|
18
|
+
scanProjectForSecrets,
|
|
12
19
|
startMcpServer,
|
|
13
20
|
t
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
|
|
16
|
-
// node_modules/.pnpm/ignore@7.0.5/node_modules/ignore/index.js
|
|
17
|
-
var require_ignore = __commonJS({
|
|
18
|
-
"node_modules/.pnpm/ignore@7.0.5/node_modules/ignore/index.js"(exports, module) {
|
|
19
|
-
"use strict";
|
|
20
|
-
function makeArray(subject) {
|
|
21
|
-
return Array.isArray(subject) ? subject : [subject];
|
|
22
|
-
}
|
|
23
|
-
var UNDEFINED = void 0;
|
|
24
|
-
var EMPTY = "";
|
|
25
|
-
var SPACE = " ";
|
|
26
|
-
var ESCAPE = "\\";
|
|
27
|
-
var REGEX_TEST_BLANK_LINE = /^\s+$/;
|
|
28
|
-
var REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
|
|
29
|
-
var REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
|
|
30
|
-
var REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
|
|
31
|
-
var REGEX_SPLITALL_CRLF = /\r?\n/g;
|
|
32
|
-
var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
|
|
33
|
-
var REGEX_TEST_TRAILING_SLASH = /\/$/;
|
|
34
|
-
var SLASH = "/";
|
|
35
|
-
var TMP_KEY_IGNORE = "node-ignore";
|
|
36
|
-
if (typeof Symbol !== "undefined") {
|
|
37
|
-
TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
|
|
38
|
-
}
|
|
39
|
-
var KEY_IGNORE = TMP_KEY_IGNORE;
|
|
40
|
-
var define = (object, key, value) => {
|
|
41
|
-
Object.defineProperty(object, key, { value });
|
|
42
|
-
return value;
|
|
43
|
-
};
|
|
44
|
-
var REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
|
|
45
|
-
var RETURN_FALSE = () => false;
|
|
46
|
-
var sanitizeRange = (range) => range.replace(
|
|
47
|
-
REGEX_REGEXP_RANGE,
|
|
48
|
-
(match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) ? match : EMPTY
|
|
49
|
-
);
|
|
50
|
-
var cleanRangeBackSlash = (slashes) => {
|
|
51
|
-
const { length } = slashes;
|
|
52
|
-
return slashes.slice(0, length - length % 2);
|
|
53
|
-
};
|
|
54
|
-
var REPLACERS = [
|
|
55
|
-
[
|
|
56
|
-
// Remove BOM
|
|
57
|
-
// TODO:
|
|
58
|
-
// Other similar zero-width characters?
|
|
59
|
-
/^\uFEFF/,
|
|
60
|
-
() => EMPTY
|
|
61
|
-
],
|
|
62
|
-
// > Trailing spaces are ignored unless they are quoted with backslash ("\")
|
|
63
|
-
[
|
|
64
|
-
// (a\ ) -> (a )
|
|
65
|
-
// (a ) -> (a)
|
|
66
|
-
// (a ) -> (a)
|
|
67
|
-
// (a \ ) -> (a )
|
|
68
|
-
/((?:\\\\)*?)(\\?\s+)$/,
|
|
69
|
-
(_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY)
|
|
70
|
-
],
|
|
71
|
-
// Replace (\ ) with ' '
|
|
72
|
-
// (\ ) -> ' '
|
|
73
|
-
// (\\ ) -> '\\ '
|
|
74
|
-
// (\\\ ) -> '\\ '
|
|
75
|
-
[
|
|
76
|
-
/(\\+?)\s/g,
|
|
77
|
-
(_, m1) => {
|
|
78
|
-
const { length } = m1;
|
|
79
|
-
return m1.slice(0, length - length % 2) + SPACE;
|
|
80
|
-
}
|
|
81
|
-
],
|
|
82
|
-
// Escape metacharacters
|
|
83
|
-
// which is written down by users but means special for regular expressions.
|
|
84
|
-
// > There are 12 characters with special meanings:
|
|
85
|
-
// > - the backslash \,
|
|
86
|
-
// > - the caret ^,
|
|
87
|
-
// > - the dollar sign $,
|
|
88
|
-
// > - the period or dot .,
|
|
89
|
-
// > - the vertical bar or pipe symbol |,
|
|
90
|
-
// > - the question mark ?,
|
|
91
|
-
// > - the asterisk or star *,
|
|
92
|
-
// > - the plus sign +,
|
|
93
|
-
// > - the opening parenthesis (,
|
|
94
|
-
// > - the closing parenthesis ),
|
|
95
|
-
// > - and the opening square bracket [,
|
|
96
|
-
// > - the opening curly brace {,
|
|
97
|
-
// > These special characters are often called "metacharacters".
|
|
98
|
-
[
|
|
99
|
-
/[\\$.|*+(){^]/g,
|
|
100
|
-
(match) => `\\${match}`
|
|
101
|
-
],
|
|
102
|
-
[
|
|
103
|
-
// > a question mark (?) matches a single character
|
|
104
|
-
/(?!\\)\?/g,
|
|
105
|
-
() => "[^/]"
|
|
106
|
-
],
|
|
107
|
-
// leading slash
|
|
108
|
-
[
|
|
109
|
-
// > A leading slash matches the beginning of the pathname.
|
|
110
|
-
// > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
|
|
111
|
-
// A leading slash matches the beginning of the pathname
|
|
112
|
-
/^\//,
|
|
113
|
-
() => "^"
|
|
114
|
-
],
|
|
115
|
-
// replace special metacharacter slash after the leading slash
|
|
116
|
-
[
|
|
117
|
-
/\//g,
|
|
118
|
-
() => "\\/"
|
|
119
|
-
],
|
|
120
|
-
[
|
|
121
|
-
// > A leading "**" followed by a slash means match in all directories.
|
|
122
|
-
// > For example, "**/foo" matches file or directory "foo" anywhere,
|
|
123
|
-
// > the same as pattern "foo".
|
|
124
|
-
// > "**/foo/bar" matches file or directory "bar" anywhere that is directly
|
|
125
|
-
// > under directory "foo".
|
|
126
|
-
// Notice that the '*'s have been replaced as '\\*'
|
|
127
|
-
/^\^*\\\*\\\*\\\//,
|
|
128
|
-
// '**/foo' <-> 'foo'
|
|
129
|
-
() => "^(?:.*\\/)?"
|
|
130
|
-
],
|
|
131
|
-
// starting
|
|
132
|
-
[
|
|
133
|
-
// there will be no leading '/'
|
|
134
|
-
// (which has been replaced by section "leading slash")
|
|
135
|
-
// If starts with '**', adding a '^' to the regular expression also works
|
|
136
|
-
/^(?=[^^])/,
|
|
137
|
-
function startingReplacer() {
|
|
138
|
-
return !/\/(?!$)/.test(this) ? "(?:^|\\/)" : "^";
|
|
139
|
-
}
|
|
140
|
-
],
|
|
141
|
-
// two globstars
|
|
142
|
-
[
|
|
143
|
-
// Use lookahead assertions so that we could match more than one `'/**'`
|
|
144
|
-
/\\\/\\\*\\\*(?=\\\/|$)/g,
|
|
145
|
-
// Zero, one or several directories
|
|
146
|
-
// should not use '*', or it will be replaced by the next replacer
|
|
147
|
-
// Check if it is not the last `'/**'`
|
|
148
|
-
(_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
|
|
149
|
-
],
|
|
150
|
-
// normal intermediate wildcards
|
|
151
|
-
[
|
|
152
|
-
// Never replace escaped '*'
|
|
153
|
-
// ignore rule '\*' will match the path '*'
|
|
154
|
-
// 'abc.*/' -> go
|
|
155
|
-
// 'abc.*' -> skip this rule,
|
|
156
|
-
// coz trailing single wildcard will be handed by [trailing wildcard]
|
|
157
|
-
/(^|[^\\]+)(\\\*)+(?=.+)/g,
|
|
158
|
-
// '*.js' matches '.js'
|
|
159
|
-
// '*.js' doesn't match 'abc'
|
|
160
|
-
(_, p1, p2) => {
|
|
161
|
-
const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
|
|
162
|
-
return p1 + unescaped;
|
|
163
|
-
}
|
|
164
|
-
],
|
|
165
|
-
[
|
|
166
|
-
// unescape, revert step 3 except for back slash
|
|
167
|
-
// For example, if a user escape a '\\*',
|
|
168
|
-
// after step 3, the result will be '\\\\\\*'
|
|
169
|
-
/\\\\\\(?=[$.|*+(){^])/g,
|
|
170
|
-
() => ESCAPE
|
|
171
|
-
],
|
|
172
|
-
[
|
|
173
|
-
// '\\\\' -> '\\'
|
|
174
|
-
/\\\\/g,
|
|
175
|
-
() => ESCAPE
|
|
176
|
-
],
|
|
177
|
-
[
|
|
178
|
-
// > The range notation, e.g. [a-zA-Z],
|
|
179
|
-
// > can be used to match one of the characters in a range.
|
|
180
|
-
// `\` is escaped by step 3
|
|
181
|
-
/(\\)?\[([^\]/]*?)(\\*)($|\])/g,
|
|
182
|
-
(match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` : close === "]" ? endEscape.length % 2 === 0 ? `[${sanitizeRange(range)}${endEscape}]` : "[]" : "[]"
|
|
183
|
-
],
|
|
184
|
-
// ending
|
|
185
|
-
[
|
|
186
|
-
// 'js' will not match 'js.'
|
|
187
|
-
// 'ab' will not match 'abc'
|
|
188
|
-
/(?:[^*])$/,
|
|
189
|
-
// WTF!
|
|
190
|
-
// https://git-scm.com/docs/gitignore
|
|
191
|
-
// changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
|
|
192
|
-
// which re-fixes #24, #38
|
|
193
|
-
// > If there is a separator at the end of the pattern then the pattern
|
|
194
|
-
// > will only match directories, otherwise the pattern can match both
|
|
195
|
-
// > files and directories.
|
|
196
|
-
// 'js*' will not match 'a.js'
|
|
197
|
-
// 'js/' will not match 'a.js'
|
|
198
|
-
// 'js' will match 'a.js' and 'a.js/'
|
|
199
|
-
(match) => /\/$/.test(match) ? `${match}$` : `${match}(?=$|\\/$)`
|
|
200
|
-
]
|
|
201
|
-
];
|
|
202
|
-
var REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/;
|
|
203
|
-
var MODE_IGNORE = "regex";
|
|
204
|
-
var MODE_CHECK_IGNORE = "checkRegex";
|
|
205
|
-
var UNDERSCORE = "_";
|
|
206
|
-
var TRAILING_WILD_CARD_REPLACERS = {
|
|
207
|
-
[MODE_IGNORE](_, p1) {
|
|
208
|
-
const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
|
|
209
|
-
return `${prefix}(?=$|\\/$)`;
|
|
210
|
-
},
|
|
211
|
-
[MODE_CHECK_IGNORE](_, p1) {
|
|
212
|
-
const prefix = p1 ? `${p1}[^/]*` : "[^/]*";
|
|
213
|
-
return `${prefix}(?=$|\\/$)`;
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
var makeRegexPrefix = (pattern) => REPLACERS.reduce(
|
|
217
|
-
(prev, [matcher, replacer]) => prev.replace(matcher, replacer.bind(pattern)),
|
|
218
|
-
pattern
|
|
219
|
-
);
|
|
220
|
-
var isString = (subject) => typeof subject === "string";
|
|
221
|
-
var checkPattern = (pattern) => pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) && pattern.indexOf("#") !== 0;
|
|
222
|
-
var splitPattern = (pattern) => pattern.split(REGEX_SPLITALL_CRLF).filter(Boolean);
|
|
223
|
-
var IgnoreRule = class {
|
|
224
|
-
constructor(pattern, mark, body, ignoreCase, negative, prefix) {
|
|
225
|
-
this.pattern = pattern;
|
|
226
|
-
this.mark = mark;
|
|
227
|
-
this.negative = negative;
|
|
228
|
-
define(this, "body", body);
|
|
229
|
-
define(this, "ignoreCase", ignoreCase);
|
|
230
|
-
define(this, "regexPrefix", prefix);
|
|
231
|
-
}
|
|
232
|
-
get regex() {
|
|
233
|
-
const key = UNDERSCORE + MODE_IGNORE;
|
|
234
|
-
if (this[key]) {
|
|
235
|
-
return this[key];
|
|
236
|
-
}
|
|
237
|
-
return this._make(MODE_IGNORE, key);
|
|
238
|
-
}
|
|
239
|
-
get checkRegex() {
|
|
240
|
-
const key = UNDERSCORE + MODE_CHECK_IGNORE;
|
|
241
|
-
if (this[key]) {
|
|
242
|
-
return this[key];
|
|
243
|
-
}
|
|
244
|
-
return this._make(MODE_CHECK_IGNORE, key);
|
|
245
|
-
}
|
|
246
|
-
_make(mode, key) {
|
|
247
|
-
const str = this.regexPrefix.replace(
|
|
248
|
-
REGEX_REPLACE_TRAILING_WILDCARD,
|
|
249
|
-
// It does not need to bind pattern
|
|
250
|
-
TRAILING_WILD_CARD_REPLACERS[mode]
|
|
251
|
-
);
|
|
252
|
-
const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str);
|
|
253
|
-
return define(this, key, regex);
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
var createRule = ({
|
|
257
|
-
pattern,
|
|
258
|
-
mark
|
|
259
|
-
}, ignoreCase) => {
|
|
260
|
-
let negative = false;
|
|
261
|
-
let body = pattern;
|
|
262
|
-
if (body.indexOf("!") === 0) {
|
|
263
|
-
negative = true;
|
|
264
|
-
body = body.substr(1);
|
|
265
|
-
}
|
|
266
|
-
body = body.replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!").replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#");
|
|
267
|
-
const regexPrefix = makeRegexPrefix(body);
|
|
268
|
-
return new IgnoreRule(
|
|
269
|
-
pattern,
|
|
270
|
-
mark,
|
|
271
|
-
body,
|
|
272
|
-
ignoreCase,
|
|
273
|
-
negative,
|
|
274
|
-
regexPrefix
|
|
275
|
-
);
|
|
276
|
-
};
|
|
277
|
-
var RuleManager = class {
|
|
278
|
-
constructor(ignoreCase) {
|
|
279
|
-
this._ignoreCase = ignoreCase;
|
|
280
|
-
this._rules = [];
|
|
281
|
-
}
|
|
282
|
-
_add(pattern) {
|
|
283
|
-
if (pattern && pattern[KEY_IGNORE]) {
|
|
284
|
-
this._rules = this._rules.concat(pattern._rules._rules);
|
|
285
|
-
this._added = true;
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
if (isString(pattern)) {
|
|
289
|
-
pattern = {
|
|
290
|
-
pattern
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
if (checkPattern(pattern.pattern)) {
|
|
294
|
-
const rule = createRule(pattern, this._ignoreCase);
|
|
295
|
-
this._added = true;
|
|
296
|
-
this._rules.push(rule);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// @param {Array<string> | string | Ignore} pattern
|
|
300
|
-
add(pattern) {
|
|
301
|
-
this._added = false;
|
|
302
|
-
makeArray(
|
|
303
|
-
isString(pattern) ? splitPattern(pattern) : pattern
|
|
304
|
-
).forEach(this._add, this);
|
|
305
|
-
return this._added;
|
|
306
|
-
}
|
|
307
|
-
// Test one single path without recursively checking parent directories
|
|
308
|
-
//
|
|
309
|
-
// - checkUnignored `boolean` whether should check if the path is unignored,
|
|
310
|
-
// setting `checkUnignored` to `false` could reduce additional
|
|
311
|
-
// path matching.
|
|
312
|
-
// - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
|
|
313
|
-
// @returns {TestResult} true if a file is ignored
|
|
314
|
-
test(path15, checkUnignored, mode) {
|
|
315
|
-
let ignored = false;
|
|
316
|
-
let unignored = false;
|
|
317
|
-
let matchedRule;
|
|
318
|
-
this._rules.forEach((rule) => {
|
|
319
|
-
const { negative } = rule;
|
|
320
|
-
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const matched = rule[mode].test(path15);
|
|
324
|
-
if (!matched) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
ignored = !negative;
|
|
328
|
-
unignored = negative;
|
|
329
|
-
matchedRule = negative ? UNDEFINED : rule;
|
|
330
|
-
});
|
|
331
|
-
const ret = {
|
|
332
|
-
ignored,
|
|
333
|
-
unignored
|
|
334
|
-
};
|
|
335
|
-
if (matchedRule) {
|
|
336
|
-
ret.rule = matchedRule;
|
|
337
|
-
}
|
|
338
|
-
return ret;
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
var throwError = (message, Ctor) => {
|
|
342
|
-
throw new Ctor(message);
|
|
343
|
-
};
|
|
344
|
-
var checkPath = (path15, originalPath, doThrow) => {
|
|
345
|
-
if (!isString(path15)) {
|
|
346
|
-
return doThrow(
|
|
347
|
-
`path must be a string, but got \`${originalPath}\``,
|
|
348
|
-
TypeError
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
if (!path15) {
|
|
352
|
-
return doThrow(`path must not be empty`, TypeError);
|
|
353
|
-
}
|
|
354
|
-
if (checkPath.isNotRelative(path15)) {
|
|
355
|
-
const r = "`path.relative()`d";
|
|
356
|
-
return doThrow(
|
|
357
|
-
`path should be a ${r} string, but got "${originalPath}"`,
|
|
358
|
-
RangeError
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
return true;
|
|
362
|
-
};
|
|
363
|
-
var isNotRelative = (path15) => REGEX_TEST_INVALID_PATH.test(path15);
|
|
364
|
-
checkPath.isNotRelative = isNotRelative;
|
|
365
|
-
checkPath.convert = (p) => p;
|
|
366
|
-
var Ignore = class {
|
|
367
|
-
constructor({
|
|
368
|
-
ignorecase = true,
|
|
369
|
-
ignoreCase = ignorecase,
|
|
370
|
-
allowRelativePaths = false
|
|
371
|
-
} = {}) {
|
|
372
|
-
define(this, KEY_IGNORE, true);
|
|
373
|
-
this._rules = new RuleManager(ignoreCase);
|
|
374
|
-
this._strictPathCheck = !allowRelativePaths;
|
|
375
|
-
this._initCache();
|
|
376
|
-
}
|
|
377
|
-
_initCache() {
|
|
378
|
-
this._ignoreCache = /* @__PURE__ */ Object.create(null);
|
|
379
|
-
this._testCache = /* @__PURE__ */ Object.create(null);
|
|
380
|
-
}
|
|
381
|
-
add(pattern) {
|
|
382
|
-
if (this._rules.add(pattern)) {
|
|
383
|
-
this._initCache();
|
|
384
|
-
}
|
|
385
|
-
return this;
|
|
386
|
-
}
|
|
387
|
-
// legacy
|
|
388
|
-
addPattern(pattern) {
|
|
389
|
-
return this.add(pattern);
|
|
390
|
-
}
|
|
391
|
-
// @returns {TestResult}
|
|
392
|
-
_test(originalPath, cache, checkUnignored, slices) {
|
|
393
|
-
const path15 = originalPath && checkPath.convert(originalPath);
|
|
394
|
-
checkPath(
|
|
395
|
-
path15,
|
|
396
|
-
originalPath,
|
|
397
|
-
this._strictPathCheck ? throwError : RETURN_FALSE
|
|
398
|
-
);
|
|
399
|
-
return this._t(path15, cache, checkUnignored, slices);
|
|
400
|
-
}
|
|
401
|
-
checkIgnore(path15) {
|
|
402
|
-
if (!REGEX_TEST_TRAILING_SLASH.test(path15)) {
|
|
403
|
-
return this.test(path15);
|
|
404
|
-
}
|
|
405
|
-
const slices = path15.split(SLASH).filter(Boolean);
|
|
406
|
-
slices.pop();
|
|
407
|
-
if (slices.length) {
|
|
408
|
-
const parent = this._t(
|
|
409
|
-
slices.join(SLASH) + SLASH,
|
|
410
|
-
this._testCache,
|
|
411
|
-
true,
|
|
412
|
-
slices
|
|
413
|
-
);
|
|
414
|
-
if (parent.ignored) {
|
|
415
|
-
return parent;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return this._rules.test(path15, false, MODE_CHECK_IGNORE);
|
|
419
|
-
}
|
|
420
|
-
_t(path15, cache, checkUnignored, slices) {
|
|
421
|
-
if (path15 in cache) {
|
|
422
|
-
return cache[path15];
|
|
423
|
-
}
|
|
424
|
-
if (!slices) {
|
|
425
|
-
slices = path15.split(SLASH).filter(Boolean);
|
|
426
|
-
}
|
|
427
|
-
slices.pop();
|
|
428
|
-
if (!slices.length) {
|
|
429
|
-
return cache[path15] = this._rules.test(path15, checkUnignored, MODE_IGNORE);
|
|
430
|
-
}
|
|
431
|
-
const parent = this._t(
|
|
432
|
-
slices.join(SLASH) + SLASH,
|
|
433
|
-
cache,
|
|
434
|
-
checkUnignored,
|
|
435
|
-
slices
|
|
436
|
-
);
|
|
437
|
-
return cache[path15] = parent.ignored ? parent : this._rules.test(path15, checkUnignored, MODE_IGNORE);
|
|
438
|
-
}
|
|
439
|
-
ignores(path15) {
|
|
440
|
-
return this._test(path15, this._ignoreCache, false).ignored;
|
|
441
|
-
}
|
|
442
|
-
createFilter() {
|
|
443
|
-
return (path15) => !this.ignores(path15);
|
|
444
|
-
}
|
|
445
|
-
filter(paths) {
|
|
446
|
-
return makeArray(paths).filter(this.createFilter());
|
|
447
|
-
}
|
|
448
|
-
// @returns {TestResult}
|
|
449
|
-
test(path15) {
|
|
450
|
-
return this._test(path15, this._testCache, true);
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
var factory = (options) => new Ignore(options);
|
|
454
|
-
var isPathValid = (path15) => checkPath(path15 && checkPath.convert(path15), path15, RETURN_FALSE);
|
|
455
|
-
var setupWindows = () => {
|
|
456
|
-
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
|
|
457
|
-
checkPath.convert = makePosix;
|
|
458
|
-
const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
459
|
-
checkPath.isNotRelative = (path15) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path15) || isNotRelative(path15);
|
|
460
|
-
};
|
|
461
|
-
if (
|
|
462
|
-
// Detect `process` so that it can run in browsers.
|
|
463
|
-
typeof process !== "undefined" && process.platform === "win32"
|
|
464
|
-
) {
|
|
465
|
-
setupWindows();
|
|
466
|
-
}
|
|
467
|
-
module.exports = factory;
|
|
468
|
-
factory.default = factory;
|
|
469
|
-
module.exports.isPathValid = isPathValid;
|
|
470
|
-
define(module.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
|
|
471
|
-
}
|
|
472
|
-
});
|
|
21
|
+
} from "./chunk-6S3JYYZ3.js";
|
|
473
22
|
|
|
474
23
|
// src/index.ts
|
|
475
24
|
import { Command, Help } from "commander";
|
|
476
|
-
import
|
|
25
|
+
import inquirer12 from "inquirer";
|
|
477
26
|
|
|
478
27
|
// src/lib/nlp-router.ts
|
|
479
28
|
function normalize(input) {
|
|
@@ -492,24 +41,23 @@ function matchesKeywords(text, command) {
|
|
|
492
41
|
}
|
|
493
42
|
var RULES = [
|
|
494
43
|
{
|
|
495
|
-
command: "
|
|
496
|
-
explanation: "\
|
|
497
|
-
confidence: "high",
|
|
498
|
-
args: ["--skip-gate"],
|
|
499
|
-
test: (t2) => /기획.*(끝|완료)|노션.*(기획|완료)|검증.*(스킵|건너)|gate.*(스킵|건너)|바로.*시작/.test(t2)
|
|
500
|
-
},
|
|
501
|
-
{
|
|
502
|
-
command: "init",
|
|
503
|
-
explanation: "Notion\uC5D0\uC11C \uAC00\uC838\uC640 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (vhk \uC2DC\uC791 --from-notion)",
|
|
44
|
+
command: "start",
|
|
45
|
+
explanation: "\uB178\uC158\uC5D0\uC11C \uAC00\uC838\uC640 \uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 \uB9C8\uBC95\uC0AC (vhk start --from-notion)",
|
|
504
46
|
confidence: "low",
|
|
505
47
|
args: ["--from-notion"],
|
|
506
48
|
test: (t2) => /노션|notion/.test(t2) && /(시작|만들|import|가져)/.test(t2)
|
|
507
49
|
},
|
|
50
|
+
{
|
|
51
|
+
command: "start",
|
|
52
|
+
explanation: "\uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 \uB9C8\uBC95\uC0AC \u2014 git+\uBB38\uC11C+MCP+\uCEE8\uD14D\uC2A4\uD2B8 (vhk start)",
|
|
53
|
+
confidence: "high",
|
|
54
|
+
test: (t2) => /프로젝트.*(만들|시작)|폴더.*만들|만들고\s*싶|새\s*프로젝트|^시작$|마법사|기획.*(끝|완료)|검증.*(스킵|건너)|gate.*(스킵|건너)|바로.*시작/.test(t2) && !/디자인|design|팔레트|palette|테마|theme|레퍼런스|reference|다크\s*모드|라이트\s*모드|색상\s*모드|브리핑|brief|컨텍스트|context|맥락|기억|memory|^초기화$|하네스.*만/.test(t2)
|
|
55
|
+
},
|
|
508
56
|
{
|
|
509
57
|
command: "init",
|
|
510
|
-
explanation: "\
|
|
58
|
+
explanation: "\uBB38\uC11C/\uD558\uB124\uC2A4 \uD30C\uC77C\uB9CC \uC0DD\uC131 (vhk init) \u2014 git/MCP/context\uB294 \uC81C\uC678",
|
|
511
59
|
confidence: "high",
|
|
512
|
-
test: (t2) =>
|
|
60
|
+
test: (t2) => /^init$|^초기화$|하네스\s*만|문서\s*만\s*만들|init\s*만/.test(t2)
|
|
513
61
|
},
|
|
514
62
|
{
|
|
515
63
|
command: "mcp-init",
|
|
@@ -678,6 +226,36 @@ var RULES = [
|
|
|
678
226
|
explanation: "npm \uBC30\uD3EC (vhk publish)",
|
|
679
227
|
confidence: "high",
|
|
680
228
|
test: (t2) => /^출시$|출시\s*해|^publish$|퍼블리시|npm\s*(배포|출시)|버전\s*올|^릴리즈$|^release$/.test(t2) && !/체크|준비|회고/.test(t2)
|
|
229
|
+
},
|
|
230
|
+
// NLP 규칙은 한국어 표현만 매칭. 영문 `goal <sub>` 은 commander 가 직접 처리하도록
|
|
231
|
+
// 가로채기 금지 — vhk goal list / next / check / done 그대로 동작.
|
|
232
|
+
{
|
|
233
|
+
command: "goal",
|
|
234
|
+
explanation: "\uB2E4\uC74C goal \uC790\uB3D9 \uC120\uD0DD (vhk goal next)",
|
|
235
|
+
confidence: "high",
|
|
236
|
+
args: ["next"],
|
|
237
|
+
test: (t2) => /다음\s*목표|목표\s*다음/.test(t2)
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
command: "goal",
|
|
241
|
+
explanation: "\uBAA9\uD45C \uAC8C\uC774\uD2B8 \uAC80\uC99D (vhk goal check)",
|
|
242
|
+
confidence: "high",
|
|
243
|
+
args: ["check"],
|
|
244
|
+
test: (t2) => /목표\s*(점검|검증|체크)/.test(t2)
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
command: "goal",
|
|
248
|
+
explanation: "\uBAA9\uD45C \uC644\uB8CC \uCC98\uB9AC (vhk goal done)",
|
|
249
|
+
confidence: "high",
|
|
250
|
+
args: ["done"],
|
|
251
|
+
test: (t2) => /목표\s*(완료|마감)/.test(t2)
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
command: "goal",
|
|
255
|
+
explanation: "\uBAA9\uD45C \uBAA9\uB85D (vhk goal list)",
|
|
256
|
+
confidence: "high",
|
|
257
|
+
args: ["list"],
|
|
258
|
+
test: (t2) => /목표\s*(목록|리스트)/.test(t2)
|
|
681
259
|
}
|
|
682
260
|
];
|
|
683
261
|
function routeNaturalLanguage(input) {
|
|
@@ -705,8 +283,11 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
|
|
|
705
283
|
"gate",
|
|
706
284
|
"\uAC80\uC99D",
|
|
707
285
|
"\uC544\uC774\uB514\uC5B4",
|
|
708
|
-
"
|
|
286
|
+
"start",
|
|
709
287
|
"\uC2DC\uC791",
|
|
288
|
+
"\uC0C8\uD504\uB85C\uC81D\uD2B8",
|
|
289
|
+
"init",
|
|
290
|
+
"\uCD08\uAE30\uD654",
|
|
710
291
|
"\uB9CC\uB4E4\uAE30",
|
|
711
292
|
"recap",
|
|
712
293
|
"\uC815\uB9AC",
|
|
@@ -771,6 +352,14 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
|
|
|
771
352
|
"\uAE30\uC5B5",
|
|
772
353
|
"brief",
|
|
773
354
|
"\uBE0C\uB9AC\uD551",
|
|
355
|
+
"goal",
|
|
356
|
+
"\uBAA9\uD45C",
|
|
357
|
+
"blocker",
|
|
358
|
+
"\uBE14\uB85C\uCEE4",
|
|
359
|
+
"learn",
|
|
360
|
+
"\uAD50\uD6C8",
|
|
361
|
+
"resume",
|
|
362
|
+
"\uC7AC\uAC1C",
|
|
774
363
|
"help"
|
|
775
364
|
]);
|
|
776
365
|
function isOptionToken(token) {
|
|
@@ -794,8 +383,8 @@ function detectNaturalLanguageInput(argv) {
|
|
|
794
383
|
}
|
|
795
384
|
|
|
796
385
|
// src/lib/nlp-run.ts
|
|
797
|
-
import
|
|
798
|
-
import
|
|
386
|
+
import chalk26 from "chalk";
|
|
387
|
+
import inquirer11 from "inquirer";
|
|
799
388
|
|
|
800
389
|
// src/commands/gate.ts
|
|
801
390
|
import inquirer from "inquirer";
|
|
@@ -929,9 +518,9 @@ ${ko.gate.verdictTitle}
|
|
|
929
518
|
|
|
930
519
|
// src/commands/init.ts
|
|
931
520
|
import inquirer2 from "inquirer";
|
|
932
|
-
import
|
|
933
|
-
import
|
|
934
|
-
import
|
|
521
|
+
import chalk3 from "chalk";
|
|
522
|
+
import fs2 from "fs";
|
|
523
|
+
import path2 from "path";
|
|
935
524
|
|
|
936
525
|
// src/templates/claude-md.ts
|
|
937
526
|
function CLAUDE_MD_TEMPLATE(name, _stack) {
|
|
@@ -1119,6 +708,14 @@ function COMMANDS_MD_TEMPLATE() {
|
|
|
1119
708
|
"\uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uC790\uC8FC \uC4F0\uB294 \uBA85\uB839\uC5B4\uC785\uB2C8\uB2E4.",
|
|
1120
709
|
"Cursor\uC5D0\uAC8C \uD55C\uAD6D\uC5B4\uB85C \uB9D0\uD574\uB3C4 \uB429\uB2C8\uB2E4.",
|
|
1121
710
|
"",
|
|
711
|
+
"## \uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791",
|
|
712
|
+
"",
|
|
713
|
+
"| \uD558\uACE0 \uC2F6\uC740 \uAC83 | \uD130\uBBF8\uB110 \uBA85\uB839 | Cursor\uC5D0\uAC8C \uB9D0\uD558\uAE30 |",
|
|
714
|
+
"|-------------|-----------|------------------|",
|
|
715
|
+
'| \uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (\uC62C\uC778\uC6D0) | `vhk start` | "\uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791\uD574\uC918" |',
|
|
716
|
+
"",
|
|
717
|
+
"> `vhk start` \uD558\uB098\uB85C `git init` + \uBB38\uC11C \uC0DD\uC131 + Cursor MCP \uB4F1\uB85D + AI \uCEE8\uD14D\uC2A4\uD2B8 \uC0DD\uC131\uC744 \uD55C \uBC88\uC5D0 \uCC98\uB9AC\uD569\uB2C8\uB2E4. \uBA85\uB839\uC5B4 \uC678\uC6B8 \uD544\uC694 \uC5C6\uC5B4\uC694.",
|
|
718
|
+
"",
|
|
1122
719
|
"## \uB9E4\uC77C \uC4F0\uB294 \uBA85\uB839\uC5B4",
|
|
1123
720
|
"",
|
|
1124
721
|
"| \uD558\uACE0 \uC2F6\uC740 \uAC83 | \uD130\uBBF8\uB110 \uBA85\uB839 | Cursor\uC5D0\uAC8C \uB9D0\uD558\uAE30 |",
|
|
@@ -1141,113 +738,27 @@ function COMMANDS_MD_TEMPLATE() {
|
|
|
1141
738
|
].join("\n");
|
|
1142
739
|
}
|
|
1143
740
|
|
|
1144
|
-
// src/lib/check-secure.ts
|
|
1145
|
-
var import_ignore = __toESM(require_ignore(), 1);
|
|
1146
|
-
import fs from "fs";
|
|
1147
|
-
import path from "path";
|
|
1148
|
-
import chalk2 from "chalk";
|
|
1149
|
-
function loadGitignore(rootDir) {
|
|
1150
|
-
const ig = (0, import_ignore.default)();
|
|
1151
|
-
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
1152
|
-
if (fs.existsSync(gitignorePath)) {
|
|
1153
|
-
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
1154
|
-
ig.add(content);
|
|
1155
|
-
}
|
|
1156
|
-
return ig;
|
|
1157
|
-
}
|
|
1158
|
-
function isPathIgnored(ig, relativePath) {
|
|
1159
|
-
const normalized = relativePath.replace(/\\/g, "/");
|
|
1160
|
-
return ig.ignores(normalized);
|
|
1161
|
-
}
|
|
1162
|
-
function findExposedSensitiveFiles(rootDir, ig = loadGitignore(rootDir), maxDepth = 8) {
|
|
1163
|
-
const exposed = [];
|
|
1164
|
-
function walk(dir, depth) {
|
|
1165
|
-
if (depth > maxDepth) return;
|
|
1166
|
-
let entries;
|
|
1167
|
-
try {
|
|
1168
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1169
|
-
} catch {
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
for (const entry of entries) {
|
|
1173
|
-
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
1174
|
-
const fullPath = path.join(dir, entry.name);
|
|
1175
|
-
const rel = path.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
1176
|
-
if (entry.isDirectory()) {
|
|
1177
|
-
if (!isPathIgnored(ig, rel + "/")) walk(fullPath, depth + 1);
|
|
1178
|
-
continue;
|
|
1179
|
-
}
|
|
1180
|
-
if (isSensitiveName(entry.name) && !isPathIgnored(ig, rel)) {
|
|
1181
|
-
exposed.push(rel);
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
walk(rootDir, 0);
|
|
1186
|
-
return exposed;
|
|
1187
|
-
}
|
|
1188
|
-
function isSensitiveName(name) {
|
|
1189
|
-
const lower = name.toLowerCase();
|
|
1190
|
-
if (lower === ".env" || lower.startsWith(".env.")) return true;
|
|
1191
|
-
if (lower.endsWith(".pem") || lower.endsWith(".key")) return true;
|
|
1192
|
-
if (lower === "credentials.json" || lower === "secrets.json") return true;
|
|
1193
|
-
if (lower.startsWith("id_rsa")) return true;
|
|
1194
|
-
return false;
|
|
1195
|
-
}
|
|
1196
|
-
function checkProjectSecurity(rootDir = process.cwd()) {
|
|
1197
|
-
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
1198
|
-
const missingGitignore = !fs.existsSync(gitignorePath);
|
|
1199
|
-
const ig = loadGitignore(rootDir);
|
|
1200
|
-
const exposedPaths = findExposedSensitiveFiles(rootDir, ig);
|
|
1201
|
-
const warnings = [];
|
|
1202
|
-
if (missingGitignore) {
|
|
1203
|
-
warnings.push(".gitignore \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBBFC\uAC10\uD55C \uD30C\uC77C\uC774 \uC2E4\uC218\uB85C \uC62C\uB77C\uAC08 \uC218 \uC788\uC5B4\uC694.");
|
|
1204
|
-
}
|
|
1205
|
-
if (exposedPaths.length > 0) {
|
|
1206
|
-
warnings.push(
|
|
1207
|
-
`ignore\uB418\uC9C0 \uC54A\uC740 \uBBFC\uAC10 \uD30C\uC77C ${exposedPaths.length}\uAC1C: ${exposedPaths.join(", ")}`
|
|
1208
|
-
);
|
|
1209
|
-
}
|
|
1210
|
-
return {
|
|
1211
|
-
ok: !missingGitignore && exposedPaths.length === 0,
|
|
1212
|
-
missingGitignore,
|
|
1213
|
-
exposedPaths,
|
|
1214
|
-
warnings
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
function printSecurityWarnings(rootDir = process.cwd()) {
|
|
1218
|
-
const result = checkProjectSecurity(rootDir);
|
|
1219
|
-
if (result.ok) return true;
|
|
1220
|
-
for (const w of result.warnings) {
|
|
1221
|
-
console.log(chalk2.yellow(` \u26A0\uFE0F ${w}`));
|
|
1222
|
-
}
|
|
1223
|
-
return false;
|
|
1224
|
-
}
|
|
1225
|
-
function filterTrackedPaths(paths, rootDir = process.cwd()) {
|
|
1226
|
-
const ig = loadGitignore(rootDir);
|
|
1227
|
-
return paths.filter((p) => !isPathIgnored(ig, p.replace(/\\/g, "/")));
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
741
|
// src/utils/logger.ts
|
|
1231
|
-
import
|
|
742
|
+
import chalk2 from "chalk";
|
|
1232
743
|
var log = {
|
|
1233
|
-
success: (msg) => console.log(
|
|
1234
|
-
error: (msg) => console.log(
|
|
1235
|
-
warn: (msg) => console.log(
|
|
1236
|
-
info: (msg) => console.log(
|
|
1237
|
-
step: (msg) => console.log(
|
|
744
|
+
success: (msg) => console.log(chalk2.green(`\u2705 ${msg}`)),
|
|
745
|
+
error: (msg) => console.log(chalk2.red(`\u274C ${msg}`)),
|
|
746
|
+
warn: (msg) => console.log(chalk2.yellow(`\u26A0\uFE0F ${msg}`)),
|
|
747
|
+
info: (msg) => console.log(chalk2.blue(`\u2139\uFE0F ${msg}`)),
|
|
748
|
+
step: (msg) => console.log(chalk2.bold(`
|
|
1238
749
|
\u25B8 ${msg}`))
|
|
1239
750
|
};
|
|
1240
751
|
|
|
1241
752
|
// src/utils/file.ts
|
|
1242
|
-
import
|
|
1243
|
-
import
|
|
753
|
+
import fs from "fs";
|
|
754
|
+
import path from "path";
|
|
1244
755
|
function writeFile(filePath, content) {
|
|
1245
|
-
const dir =
|
|
1246
|
-
if (!
|
|
1247
|
-
|
|
756
|
+
const dir = path.dirname(filePath);
|
|
757
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
758
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
1248
759
|
}
|
|
1249
760
|
function fileExists(filePath) {
|
|
1250
|
-
return
|
|
761
|
+
return fs.existsSync(filePath);
|
|
1251
762
|
}
|
|
1252
763
|
|
|
1253
764
|
// src/lib/notion-import.ts
|
|
@@ -1439,11 +950,11 @@ async function collectAnswers(options, defaults = {}) {
|
|
|
1439
950
|
async function init(options = {}) {
|
|
1440
951
|
const skipGate = Boolean(options.skipGate || options.fromNotion);
|
|
1441
952
|
if (skipGate) {
|
|
1442
|
-
console.log(
|
|
953
|
+
console.log(chalk3.dim(`
|
|
1443
954
|
${ko.init.skipGate}
|
|
1444
955
|
`));
|
|
1445
956
|
}
|
|
1446
|
-
console.log(
|
|
957
|
+
console.log(chalk3.bold(`
|
|
1447
958
|
${ko.init.title}
|
|
1448
959
|
`));
|
|
1449
960
|
printSecurityWarnings();
|
|
@@ -1468,7 +979,7 @@ ${ko.init.title}
|
|
|
1468
979
|
process.exit(1);
|
|
1469
980
|
}
|
|
1470
981
|
const stack = STACK_PRESETS[answers.type];
|
|
1471
|
-
console.log(
|
|
982
|
+
console.log(chalk3.dim(`
|
|
1472
983
|
${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
1473
984
|
`));
|
|
1474
985
|
if (!options.yes) {
|
|
@@ -1487,7 +998,7 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1487
998
|
const files = generateFiles(answers.name, answers.description, stack, prdContent);
|
|
1488
999
|
log.step(ko.init.filesGenerating);
|
|
1489
1000
|
for (const [filePath, content] of Object.entries(files)) {
|
|
1490
|
-
const fullPath =
|
|
1001
|
+
const fullPath = path2.join(cwd, filePath);
|
|
1491
1002
|
if (fileExists(fullPath)) {
|
|
1492
1003
|
const { overwrite } = await inquirer2.prompt([{
|
|
1493
1004
|
type: "confirm",
|
|
@@ -1504,21 +1015,21 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1504
1015
|
log.success(filePath);
|
|
1505
1016
|
}
|
|
1506
1017
|
await writeInitExtras(cwd);
|
|
1507
|
-
console.log(
|
|
1018
|
+
console.log(chalk3.bold.green(`
|
|
1508
1019
|
${ko.init.done}`));
|
|
1509
|
-
console.log(
|
|
1020
|
+
console.log(chalk3.dim(`
|
|
1510
1021
|
${ko.init.nextSteps}`));
|
|
1511
1022
|
if (options.fromNotion) {
|
|
1512
1023
|
console.log(` 1. ${ko.init.notionReviewHint}`);
|
|
1513
1024
|
console.log(` 2. ${ko.init.gitHintLabel}`);
|
|
1514
|
-
console.log(` ${
|
|
1025
|
+
console.log(` ${chalk3.cyan(ko.init.gitHintCommand)}`);
|
|
1515
1026
|
console.log(` 3. ${ko.init.startDev}
|
|
1516
1027
|
`);
|
|
1517
1028
|
} else {
|
|
1518
1029
|
console.log(` 1. ${ko.init.fillHint}`);
|
|
1519
1030
|
console.log(` 2. ${ko.init.prdHint}`);
|
|
1520
1031
|
console.log(` 3. ${ko.init.gitHintLabel}`);
|
|
1521
|
-
console.log(` ${
|
|
1032
|
+
console.log(` ${chalk3.cyan(ko.init.gitHintCommand)}`);
|
|
1522
1033
|
console.log(` 4. ${ko.init.startDev}
|
|
1523
1034
|
`);
|
|
1524
1035
|
}
|
|
@@ -1566,15 +1077,15 @@ var VHK_PACKAGE_SCRIPTS = {
|
|
|
1566
1077
|
doctor: "vhk doctor"
|
|
1567
1078
|
};
|
|
1568
1079
|
function enhancePackageScripts(projectDir) {
|
|
1569
|
-
const pkgPath =
|
|
1570
|
-
if (!
|
|
1080
|
+
const pkgPath = path2.join(projectDir, "package.json");
|
|
1081
|
+
if (!fs2.existsSync(pkgPath)) return false;
|
|
1571
1082
|
const pkg = readJsonFile(pkgPath);
|
|
1572
1083
|
pkg.scripts = { ...VHK_PACKAGE_SCRIPTS, ...pkg.scripts };
|
|
1573
|
-
|
|
1084
|
+
fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1574
1085
|
return true;
|
|
1575
1086
|
}
|
|
1576
1087
|
async function writeInitExtras(projectDir) {
|
|
1577
|
-
const commandsPath =
|
|
1088
|
+
const commandsPath = path2.join(projectDir, "COMMANDS.md");
|
|
1578
1089
|
if (fileExists(commandsPath)) {
|
|
1579
1090
|
const { overwrite } = await inquirer2.prompt([{
|
|
1580
1091
|
type: "confirm",
|
|
@@ -1599,16 +1110,16 @@ async function writeInitExtras(projectDir) {
|
|
|
1599
1110
|
|
|
1600
1111
|
// src/commands/recap.ts
|
|
1601
1112
|
import inquirer3 from "inquirer";
|
|
1602
|
-
import
|
|
1603
|
-
import
|
|
1604
|
-
import
|
|
1113
|
+
import chalk4 from "chalk";
|
|
1114
|
+
import fs4 from "fs";
|
|
1115
|
+
import path5 from "path";
|
|
1605
1116
|
|
|
1606
1117
|
// src/lib/git.ts
|
|
1607
|
-
import
|
|
1608
|
-
import simpleGit from "simple-git";
|
|
1118
|
+
import path3 from "path";
|
|
1119
|
+
import { simpleGit } from "simple-git";
|
|
1609
1120
|
var git = simpleGit();
|
|
1610
1121
|
function isNoiseRecapPath(filePath) {
|
|
1611
|
-
const base =
|
|
1122
|
+
const base = path3.basename(filePath);
|
|
1612
1123
|
if (base.includes("${") || base.includes("`")) return true;
|
|
1613
1124
|
if (/^\d+(\.\d+)?$/.test(base)) return true;
|
|
1614
1125
|
if (/^(pnpm|vhk|npm|node|yarn)$/i.test(base)) return true;
|
|
@@ -1647,7 +1158,16 @@ async function getSessionDiff(since) {
|
|
|
1647
1158
|
const sinceDate = since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1648
1159
|
try {
|
|
1649
1160
|
const diffSummary = await git.diffSummary([`--since=${sinceDate}`]);
|
|
1650
|
-
|
|
1161
|
+
const normalized = diffSummary.files.map((f) => ({
|
|
1162
|
+
file: f.file,
|
|
1163
|
+
insertions: "insertions" in f ? f.insertions : 0,
|
|
1164
|
+
deletions: "deletions" in f ? f.deletions : 0
|
|
1165
|
+
}));
|
|
1166
|
+
return buildSessionDiffFromSummary({
|
|
1167
|
+
insertions: diffSummary.insertions,
|
|
1168
|
+
deletions: diffSummary.deletions,
|
|
1169
|
+
files: normalized
|
|
1170
|
+
});
|
|
1651
1171
|
} catch {
|
|
1652
1172
|
return { filesChanged: 0, insertions: 0, deletions: 0, files: [] };
|
|
1653
1173
|
}
|
|
@@ -1685,8 +1205,8 @@ async function hasAnyCommits() {
|
|
|
1685
1205
|
}
|
|
1686
1206
|
|
|
1687
1207
|
// src/lib/adr.ts
|
|
1688
|
-
import
|
|
1689
|
-
import
|
|
1208
|
+
import fs3 from "fs";
|
|
1209
|
+
import path4 from "path";
|
|
1690
1210
|
var ADR_RULES = [
|
|
1691
1211
|
{
|
|
1692
1212
|
title: "\uC758\uC874\uC131 \uBCC0\uACBD",
|
|
@@ -1729,20 +1249,20 @@ function detectAdrCandidates(diff2) {
|
|
|
1729
1249
|
return candidates;
|
|
1730
1250
|
}
|
|
1731
1251
|
function nextAdrNumber(adrDir) {
|
|
1732
|
-
if (!
|
|
1733
|
-
const nums =
|
|
1252
|
+
if (!fs3.existsSync(adrDir)) return 1;
|
|
1253
|
+
const nums = fs3.readdirSync(adrDir).map((name) => name.match(/^ADR-(\d+)/i)?.[1]).filter((n) => Boolean(n)).map((n) => parseInt(n, 10));
|
|
1734
1254
|
return nums.length ? Math.max(...nums) + 1 : 1;
|
|
1735
1255
|
}
|
|
1736
1256
|
function slugify(title) {
|
|
1737
1257
|
return title.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9가-힣-]/g, "").slice(0, 40) || "decision";
|
|
1738
1258
|
}
|
|
1739
1259
|
function createAdrFile(cwd, title, context2, decision, consequences) {
|
|
1740
|
-
const adrDir =
|
|
1741
|
-
if (!
|
|
1260
|
+
const adrDir = path4.join(cwd, "docs", "adr");
|
|
1261
|
+
if (!fs3.existsSync(adrDir)) fs3.mkdirSync(adrDir, { recursive: true });
|
|
1742
1262
|
const num = nextAdrNumber(adrDir);
|
|
1743
1263
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1744
1264
|
const fileName = `ADR-${String(num).padStart(3, "0")}-${slugify(title)}.md`;
|
|
1745
|
-
const filePath =
|
|
1265
|
+
const filePath = path4.join(adrDir, fileName);
|
|
1746
1266
|
const content = [
|
|
1747
1267
|
"---",
|
|
1748
1268
|
`id: ADR-${String(num).padStart(3, "0")}`,
|
|
@@ -1768,51 +1288,51 @@ function createAdrFile(cwd, title, context2, decision, consequences) {
|
|
|
1768
1288
|
"---",
|
|
1769
1289
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
1770
1290
|
].join("\n");
|
|
1771
|
-
|
|
1291
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
1772
1292
|
return filePath;
|
|
1773
1293
|
}
|
|
1774
1294
|
|
|
1775
1295
|
// src/commands/recap.ts
|
|
1776
1296
|
async function recap(options = {}) {
|
|
1777
|
-
console.log(
|
|
1297
|
+
console.log(chalk4.bold(`
|
|
1778
1298
|
${ko.recap.title}
|
|
1779
1299
|
`));
|
|
1780
1300
|
if (!await isGitRepo()) {
|
|
1781
|
-
console.log(
|
|
1301
|
+
console.log(chalk4.red(ko.recap.noRepo));
|
|
1782
1302
|
return;
|
|
1783
1303
|
}
|
|
1784
1304
|
if (!await hasAnyCommits()) {
|
|
1785
|
-
console.log(
|
|
1786
|
-
console.log(
|
|
1305
|
+
console.log(chalk4.yellow("\u26A0\uFE0F \uC544\uC9C1 \uCEE4\uBC0B\uC774 \uC5C6\uC5B4\uC694."));
|
|
1306
|
+
console.log(chalk4.gray(" \uD30C\uC77C\uC744 \uCD94\uAC00\uD558\uACE0 `vhk save` \uB610\uB294 `git commit`\uC73C\uB85C \uCCAB \uCEE4\uBC0B\uC744 \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
|
|
1787
1307
|
return;
|
|
1788
1308
|
}
|
|
1789
1309
|
printSecurityWarnings();
|
|
1790
|
-
console.log(
|
|
1310
|
+
console.log(chalk4.dim(`${ko.recap.analyzing}
|
|
1791
1311
|
`));
|
|
1792
1312
|
const since = options.since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1793
1313
|
const diff2 = await getSessionDiff(since);
|
|
1794
1314
|
const commits = await getRecentCommits(10, since);
|
|
1795
1315
|
if (diff2.filesChanged === 0 && commits.length === 0) {
|
|
1796
|
-
console.log(
|
|
1316
|
+
console.log(chalk4.yellow(ko.recap.noChanges));
|
|
1797
1317
|
return;
|
|
1798
1318
|
}
|
|
1799
|
-
console.log(
|
|
1800
|
-
console.log(` \uD30C\uC77C: ${
|
|
1801
|
-
console.log(` \uCD94\uAC00: ${
|
|
1319
|
+
console.log(chalk4.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
|
|
1320
|
+
console.log(` \uD30C\uC77C: ${chalk4.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
|
|
1321
|
+
console.log(` \uCD94\uAC00: ${chalk4.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk4.red("-" + diff2.deletions)}`);
|
|
1802
1322
|
if (diff2.files.length > 0) {
|
|
1803
|
-
console.log(
|
|
1323
|
+
console.log(chalk4.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
|
|
1804
1324
|
diff2.files.slice(0, 15).forEach((f) => {
|
|
1805
|
-
const icon = f.status === "new" ?
|
|
1325
|
+
const icon = f.status === "new" ? chalk4.green("\u{1F195}") : f.status === "deleted" ? chalk4.red("\u{1F5D1}\uFE0F") : chalk4.yellow("\u270F\uFE0F");
|
|
1806
1326
|
console.log(` ${icon} ${f.file}`);
|
|
1807
1327
|
});
|
|
1808
1328
|
if (diff2.files.length > 15) {
|
|
1809
|
-
console.log(
|
|
1329
|
+
console.log(chalk4.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
|
|
1810
1330
|
}
|
|
1811
1331
|
}
|
|
1812
1332
|
if (commits.length > 0) {
|
|
1813
|
-
console.log(
|
|
1333
|
+
console.log(chalk4.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
|
|
1814
1334
|
commits.slice(0, 5).forEach((c) => {
|
|
1815
|
-
console.log(
|
|
1335
|
+
console.log(chalk4.dim(` \u2022 ${c.message}`));
|
|
1816
1336
|
});
|
|
1817
1337
|
}
|
|
1818
1338
|
console.log("");
|
|
@@ -1841,12 +1361,12 @@ ${ko.recap.title}
|
|
|
1841
1361
|
}
|
|
1842
1362
|
]);
|
|
1843
1363
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1844
|
-
const logDir =
|
|
1845
|
-
if (!
|
|
1846
|
-
const existing =
|
|
1364
|
+
const logDir = path5.join(process.cwd(), "docs", "log");
|
|
1365
|
+
if (!fs4.existsSync(logDir)) fs4.mkdirSync(logDir, { recursive: true });
|
|
1366
|
+
const existing = fs4.readdirSync(logDir).filter((f) => f.startsWith(today));
|
|
1847
1367
|
const sessionNum = existing.length + 1;
|
|
1848
1368
|
const fileName = `${today}-session-${sessionNum}.md`;
|
|
1849
|
-
const filePath =
|
|
1369
|
+
const filePath = path5.join(logDir, fileName);
|
|
1850
1370
|
const fileList = diff2.files.map((f) => `| ${f.file} | ${f.status} |`).join("\n");
|
|
1851
1371
|
const commitList = commits.slice(0, 10).map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
1852
1372
|
const content = [
|
|
@@ -1877,14 +1397,14 @@ ${ko.recap.title}
|
|
|
1877
1397
|
"---",
|
|
1878
1398
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
1879
1399
|
].join("\n");
|
|
1880
|
-
|
|
1400
|
+
fs4.writeFileSync(filePath, content, "utf-8");
|
|
1881
1401
|
const adrCandidates = detectAdrCandidates(diff2);
|
|
1882
1402
|
if (adrCandidates.length > 0) {
|
|
1883
|
-
console.log(
|
|
1403
|
+
console.log(chalk4.cyan.bold(`
|
|
1884
1404
|
${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
1885
1405
|
for (const candidate of adrCandidates) {
|
|
1886
|
-
console.log(
|
|
1887
|
-
candidate.files.forEach((f) => console.log(
|
|
1406
|
+
console.log(chalk4.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
|
|
1407
|
+
candidate.files.forEach((f) => console.log(chalk4.dim(` ${f}`)));
|
|
1888
1408
|
}
|
|
1889
1409
|
const { createAdr } = await inquirer3.prompt([{
|
|
1890
1410
|
type: "confirm",
|
|
@@ -1914,17 +1434,17 @@ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
|
1914
1434
|
adrAnswers.decision,
|
|
1915
1435
|
adrAnswers.consequences
|
|
1916
1436
|
);
|
|
1917
|
-
console.log(
|
|
1437
|
+
console.log(chalk4.green(` \u2705 ADR \uC0DD\uC131: ${path5.relative(process.cwd(), adrPath)}`));
|
|
1918
1438
|
}
|
|
1919
1439
|
}
|
|
1920
1440
|
}
|
|
1921
1441
|
const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
|
|
1922
1442
|
const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
|
|
1923
1443
|
if (troubleCommits.length > 0) {
|
|
1924
|
-
console.log(
|
|
1444
|
+
console.log(chalk4.yellow.bold(`
|
|
1925
1445
|
${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
1926
1446
|
troubleCommits.forEach((c) => {
|
|
1927
|
-
console.log(
|
|
1447
|
+
console.log(chalk4.dim(` \u2022 ${c.message}`));
|
|
1928
1448
|
});
|
|
1929
1449
|
const { createTroubleshoot } = await inquirer3.prompt([{
|
|
1930
1450
|
type: "confirm",
|
|
@@ -1933,8 +1453,8 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
|
1933
1453
|
default: true
|
|
1934
1454
|
}]);
|
|
1935
1455
|
if (createTroubleshoot) {
|
|
1936
|
-
const tsDir =
|
|
1937
|
-
if (!
|
|
1456
|
+
const tsDir = path5.join(process.cwd(), "docs", "troubleshooting");
|
|
1457
|
+
if (!fs4.existsSync(tsDir)) fs4.mkdirSync(tsDir, { recursive: true });
|
|
1938
1458
|
const tsAnswers = await inquirer3.prompt([
|
|
1939
1459
|
{
|
|
1940
1460
|
type: "input",
|
|
@@ -1953,7 +1473,7 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
|
1953
1473
|
}
|
|
1954
1474
|
]);
|
|
1955
1475
|
const tsFileName = `${today}-${tsAnswers.problem.slice(0, 30).replace(/[^a-zA-Z0-9가-힣]/g, "-")}.md`;
|
|
1956
|
-
const tsFilePath =
|
|
1476
|
+
const tsFilePath = path5.join(tsDir, tsFileName);
|
|
1957
1477
|
const tsContent = [
|
|
1958
1478
|
`# \uD2B8\uB7EC\uBE14\uC288\uD305: ${tsAnswers.problem}`,
|
|
1959
1479
|
"",
|
|
@@ -1974,15 +1494,15 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
|
1974
1494
|
"---",
|
|
1975
1495
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
1976
1496
|
].join("\n");
|
|
1977
|
-
|
|
1978
|
-
console.log(
|
|
1497
|
+
fs4.writeFileSync(tsFilePath, tsContent, "utf-8");
|
|
1498
|
+
console.log(chalk4.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path5.relative(process.cwd(), tsFilePath)}`));
|
|
1979
1499
|
}
|
|
1980
1500
|
}
|
|
1981
|
-
console.log(
|
|
1501
|
+
console.log(chalk4.green.bold(`
|
|
1982
1502
|
${ko.recap.done}`));
|
|
1983
|
-
console.log(
|
|
1984
|
-
const claudeMdPath =
|
|
1985
|
-
if (
|
|
1503
|
+
console.log(chalk4.dim(` \u{1F4C4} ${path5.relative(process.cwd(), filePath)}`));
|
|
1504
|
+
const claudeMdPath = path5.join(process.cwd(), "CLAUDE.md");
|
|
1505
|
+
if (fs4.existsSync(claudeMdPath)) {
|
|
1986
1506
|
const { updateClaude } = await inquirer3.prompt([{
|
|
1987
1507
|
type: "confirm",
|
|
1988
1508
|
name: "updateClaude",
|
|
@@ -1990,7 +1510,7 @@ ${ko.recap.done}`));
|
|
|
1990
1510
|
default: true
|
|
1991
1511
|
}]);
|
|
1992
1512
|
if (updateClaude) {
|
|
1993
|
-
let claudeContent =
|
|
1513
|
+
let claudeContent = fs4.readFileSync(claudeMdPath, "utf-8");
|
|
1994
1514
|
claudeContent = claudeContent.replace(
|
|
1995
1515
|
/- \*\*마지막 업데이트:\*\*.*/,
|
|
1996
1516
|
`- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${today}`
|
|
@@ -1999,8 +1519,8 @@ ${ko.recap.done}`));
|
|
|
1999
1519
|
/- \*\*다음 액션:\*\*.*/,
|
|
2000
1520
|
`- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
|
|
2001
1521
|
);
|
|
2002
|
-
|
|
2003
|
-
console.log(
|
|
1522
|
+
fs4.writeFileSync(claudeMdPath, claudeContent, "utf-8");
|
|
1523
|
+
console.log(chalk4.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
|
|
2004
1524
|
}
|
|
2005
1525
|
}
|
|
2006
1526
|
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"';
|
|
@@ -2012,9 +1532,9 @@ ${ko.recap.done}`));
|
|
|
2012
1532
|
}
|
|
2013
1533
|
|
|
2014
1534
|
// src/commands/sync.ts
|
|
2015
|
-
import
|
|
2016
|
-
import
|
|
2017
|
-
import
|
|
1535
|
+
import chalk5 from "chalk";
|
|
1536
|
+
import fs5 from "fs";
|
|
1537
|
+
import path6 from "path";
|
|
2018
1538
|
var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
|
|
2019
1539
|
var CLAUDE_MD_KEYS = ["\uAE30\uB85D", "\uB85C\uADF8", "ADR", "\uD2B8\uB7EC\uBE14\uC288\uD305", "TIL", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8"];
|
|
2020
1540
|
function parseRulesMd(content) {
|
|
@@ -2082,46 +1602,46 @@ function toClaudeMd(sections, existing) {
|
|
|
2082
1602
|
return lines.join("\n");
|
|
2083
1603
|
}
|
|
2084
1604
|
async function sync() {
|
|
2085
|
-
console.log(
|
|
1605
|
+
console.log(chalk5.bold(`
|
|
2086
1606
|
${ko.sync.title}
|
|
2087
1607
|
`));
|
|
2088
1608
|
const cwd = process.cwd();
|
|
2089
|
-
const rulesPath =
|
|
2090
|
-
if (!
|
|
2091
|
-
console.log(
|
|
2092
|
-
console.log(
|
|
2093
|
-
console.log(
|
|
1609
|
+
const rulesPath = path6.join(cwd, "RULES.md");
|
|
1610
|
+
if (!fs5.existsSync(rulesPath)) {
|
|
1611
|
+
console.log(chalk5.yellow(ko.sync.noRules));
|
|
1612
|
+
console.log(chalk5.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
|
|
1613
|
+
console.log(chalk5.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
|
|
2094
1614
|
console.log("");
|
|
2095
|
-
console.log(
|
|
2096
|
-
console.log(
|
|
2097
|
-
console.log(
|
|
2098
|
-
console.log(
|
|
2099
|
-
console.log(
|
|
2100
|
-
console.log(
|
|
1615
|
+
console.log(chalk5.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
|
|
1616
|
+
console.log(chalk5.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
|
|
1617
|
+
console.log(chalk5.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
|
|
1618
|
+
console.log(chalk5.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
|
|
1619
|
+
console.log(chalk5.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
|
|
1620
|
+
console.log(chalk5.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
|
|
2101
1621
|
return;
|
|
2102
1622
|
}
|
|
2103
|
-
const rulesContent =
|
|
1623
|
+
const rulesContent = fs5.readFileSync(rulesPath, "utf-8");
|
|
2104
1624
|
const sections = parseRulesMd(rulesContent);
|
|
2105
|
-
console.log(
|
|
1625
|
+
console.log(chalk5.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
|
|
2106
1626
|
const firstLine = rulesContent.split("\n")[0];
|
|
2107
1627
|
const projectName = firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
|
|
2108
|
-
const cursorrulesPath =
|
|
2109
|
-
|
|
2110
|
-
console.log(
|
|
2111
|
-
const claudePath =
|
|
2112
|
-
const existingClaude =
|
|
1628
|
+
const cursorrulesPath = path6.join(cwd, ".cursorrules");
|
|
1629
|
+
fs5.writeFileSync(cursorrulesPath, toCursorrules(sections, projectName), "utf-8");
|
|
1630
|
+
console.log(chalk5.green(` ${ko.sync.cursorrulesDone}`));
|
|
1631
|
+
const claudePath = path6.join(cwd, "CLAUDE.md");
|
|
1632
|
+
const existingClaude = fs5.existsSync(claudePath) ? fs5.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
|
|
2113
1633
|
|
|
2114
1634
|
## \uD604\uC7AC \uC0C1\uD0DC
|
|
2115
1635
|
- **Phase:** __FILL__
|
|
2116
1636
|
- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C
|
|
2117
1637
|
- **\uB2E4\uC74C \uC561\uC158:** __FILL__
|
|
2118
1638
|
- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
|
|
2119
|
-
|
|
2120
|
-
console.log(
|
|
2121
|
-
console.log(
|
|
1639
|
+
fs5.writeFileSync(claudePath, toClaudeMd(sections, existingClaude), "utf-8");
|
|
1640
|
+
console.log(chalk5.green(` ${ko.sync.claudeDone}`));
|
|
1641
|
+
console.log(chalk5.bold.green(`
|
|
2122
1642
|
${ko.sync.done}`));
|
|
2123
|
-
console.log(
|
|
2124
|
-
console.log(
|
|
1643
|
+
console.log(chalk5.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md (\uC790\uB3D9 \uC0DD\uC131)"));
|
|
1644
|
+
console.log(chalk5.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
|
|
2125
1645
|
printNextStep({
|
|
2126
1646
|
message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
|
|
2127
1647
|
command: "vhk \uC810\uAC80",
|
|
@@ -2131,15 +1651,15 @@ ${ko.sync.done}`));
|
|
|
2131
1651
|
|
|
2132
1652
|
// src/commands/check.ts
|
|
2133
1653
|
import chalk7 from "chalk";
|
|
2134
|
-
import
|
|
2135
|
-
import
|
|
1654
|
+
import path8 from "path";
|
|
1655
|
+
import fs7 from "fs";
|
|
2136
1656
|
|
|
2137
1657
|
// src/lib/rules-parser.ts
|
|
2138
|
-
import
|
|
2139
|
-
import
|
|
1658
|
+
import fs6 from "fs";
|
|
1659
|
+
import path7 from "path";
|
|
2140
1660
|
function parseRules(rulesPath) {
|
|
2141
|
-
if (!
|
|
2142
|
-
const content =
|
|
1661
|
+
if (!fs6.existsSync(rulesPath)) return [];
|
|
1662
|
+
const content = fs6.readFileSync(rulesPath, "utf-8");
|
|
2143
1663
|
const lines = content.split("\n");
|
|
2144
1664
|
const rules = [];
|
|
2145
1665
|
let currentSection = "";
|
|
@@ -2200,17 +1720,17 @@ function createNamingRule(id, section, desc, convention) {
|
|
|
2200
1720
|
description: desc,
|
|
2201
1721
|
check: (cwd) => {
|
|
2202
1722
|
const violations = [];
|
|
2203
|
-
const srcDir =
|
|
2204
|
-
if (!
|
|
1723
|
+
const srcDir = path7.join(cwd, "src");
|
|
1724
|
+
if (!fs6.existsSync(srcDir)) return violations;
|
|
2205
1725
|
walkFiles(srcDir, (filePath) => {
|
|
2206
|
-
const name =
|
|
1726
|
+
const name = path7.basename(filePath, path7.extname(filePath));
|
|
2207
1727
|
if (convention === "kebab-case" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
2208
1728
|
if (!["index", "vite.config", "tsconfig"].includes(name)) {
|
|
2209
1729
|
violations.push({
|
|
2210
1730
|
ruleId: id,
|
|
2211
1731
|
severity: "warning",
|
|
2212
1732
|
message: `\uD30C\uC77C\uBA85\uC774 kebab-case\uAC00 \uC544\uB2D8: ${name}`,
|
|
2213
|
-
file:
|
|
1733
|
+
file: path7.relative(cwd, filePath)
|
|
2214
1734
|
});
|
|
2215
1735
|
}
|
|
2216
1736
|
}
|
|
@@ -2226,8 +1746,8 @@ function createStructureRule(id, section, desc, expectedPath) {
|
|
|
2226
1746
|
type: "structure",
|
|
2227
1747
|
description: desc,
|
|
2228
1748
|
check: (cwd) => {
|
|
2229
|
-
const fullPath =
|
|
2230
|
-
if (!
|
|
1749
|
+
const fullPath = path7.join(cwd, expectedPath);
|
|
1750
|
+
if (!fs6.existsSync(fullPath)) {
|
|
2231
1751
|
return [{
|
|
2232
1752
|
ruleId: id,
|
|
2233
1753
|
severity: "error",
|
|
@@ -2247,11 +1767,11 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
2247
1767
|
pattern: new RegExp(escapeRegex(pattern), "i"),
|
|
2248
1768
|
check: (cwd) => {
|
|
2249
1769
|
const violations = [];
|
|
2250
|
-
const srcDir =
|
|
2251
|
-
if (!
|
|
1770
|
+
const srcDir = path7.join(cwd, "src");
|
|
1771
|
+
if (!fs6.existsSync(srcDir)) return violations;
|
|
2252
1772
|
const regex = new RegExp(escapeRegex(pattern), "i");
|
|
2253
1773
|
walkFiles(srcDir, (filePath) => {
|
|
2254
|
-
const fileContent =
|
|
1774
|
+
const fileContent = fs6.readFileSync(filePath, "utf-8");
|
|
2255
1775
|
const fileLines = fileContent.split("\n");
|
|
2256
1776
|
fileLines.forEach((line, idx) => {
|
|
2257
1777
|
if (regex.test(line)) {
|
|
@@ -2259,7 +1779,7 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
2259
1779
|
ruleId: id,
|
|
2260
1780
|
severity: type === "banned" ? "error" : "warning",
|
|
2261
1781
|
message: type === "banned" ? `\uAE08\uC9C0 \uD328\uD134 \uBC1C\uACAC: \`${pattern}\`` : `\uD544\uC218 \uD328\uD134 \uB204\uB77D: \`${pattern}\``,
|
|
2262
|
-
file:
|
|
1782
|
+
file: path7.relative(cwd, filePath),
|
|
2263
1783
|
line: idx + 1
|
|
2264
1784
|
});
|
|
2265
1785
|
}
|
|
@@ -2271,9 +1791,9 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
2271
1791
|
};
|
|
2272
1792
|
}
|
|
2273
1793
|
function walkFiles(dir, callback) {
|
|
2274
|
-
const entries =
|
|
1794
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
2275
1795
|
for (const entry of entries) {
|
|
2276
|
-
const fullPath =
|
|
1796
|
+
const fullPath = path7.join(dir, entry.name);
|
|
2277
1797
|
if (entry.isDirectory()) {
|
|
2278
1798
|
if (!["node_modules", ".git", "dist", ".next"].includes(entry.name)) {
|
|
2279
1799
|
walkFiles(fullPath, callback);
|
|
@@ -2287,45 +1807,401 @@ function escapeRegex(str) {
|
|
|
2287
1807
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2288
1808
|
}
|
|
2289
1809
|
|
|
2290
|
-
// src/commands/
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
1810
|
+
// src/commands/goal.ts
|
|
1811
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync, readFileSync as readFileSync2 } from "fs";
|
|
1812
|
+
import { join as join2 } from "path";
|
|
1813
|
+
import chalk6 from "chalk";
|
|
1814
|
+
|
|
1815
|
+
// src/lib/goal-frontmatter.ts
|
|
1816
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
1817
|
+
import { join } from "path";
|
|
1818
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
1819
|
+
function parseFrontmatter(content) {
|
|
1820
|
+
const m = content.match(FRONTMATTER_RE);
|
|
1821
|
+
if (!m) return { frontmatter: {}, body: content };
|
|
1822
|
+
const fm = parseSimpleYaml(m[1]);
|
|
1823
|
+
const body = (m[2] ?? "").replace(/^\r?\n+/, "");
|
|
1824
|
+
return { frontmatter: fm, body };
|
|
1825
|
+
}
|
|
1826
|
+
function parseSimpleYaml(yaml) {
|
|
1827
|
+
const out = {};
|
|
1828
|
+
const lines = yaml.split(/\r?\n/);
|
|
1829
|
+
for (const raw of lines) {
|
|
1830
|
+
const line = raw.trim();
|
|
1831
|
+
if (!line || line.startsWith("#")) continue;
|
|
1832
|
+
const idx = line.indexOf(":");
|
|
1833
|
+
if (idx <= 0) continue;
|
|
1834
|
+
const key = line.slice(0, idx).trim();
|
|
1835
|
+
let value = line.slice(idx + 1).trim();
|
|
1836
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1837
|
+
value = value.slice(1, -1);
|
|
1838
|
+
}
|
|
1839
|
+
if (key === "id" || key === "vhk_format") {
|
|
1840
|
+
const n = Number(value);
|
|
1841
|
+
out[key] = Number.isFinite(n) ? n : void 0;
|
|
1842
|
+
} else {
|
|
1843
|
+
out[key] = value;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
return out;
|
|
1847
|
+
}
|
|
1848
|
+
function parseGoalFile(filePath) {
|
|
1849
|
+
if (!existsSync(filePath)) return null;
|
|
1850
|
+
try {
|
|
1851
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1852
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
1853
|
+
return { filePath, frontmatter, body };
|
|
1854
|
+
} catch {
|
|
1855
|
+
return null;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function listGoals(goalsDir) {
|
|
1859
|
+
if (!existsSync(goalsDir)) return [];
|
|
1860
|
+
let entries;
|
|
1861
|
+
try {
|
|
1862
|
+
entries = readdirSync(goalsDir);
|
|
1863
|
+
} catch {
|
|
1864
|
+
return [];
|
|
1865
|
+
}
|
|
1866
|
+
const parsed = [];
|
|
1867
|
+
for (const name of entries) {
|
|
1868
|
+
if (!name.endsWith(".md")) continue;
|
|
1869
|
+
if (name === "_meta.md") continue;
|
|
1870
|
+
const fp = join(goalsDir, name);
|
|
1871
|
+
try {
|
|
1872
|
+
if (!statSync(fp).isFile()) continue;
|
|
1873
|
+
} catch {
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
const g = parseGoalFile(fp);
|
|
1877
|
+
if (!g) continue;
|
|
1878
|
+
if (g.frontmatter.type !== "goal") continue;
|
|
1879
|
+
if (typeof g.frontmatter.id !== "number") continue;
|
|
1880
|
+
parsed.push(g);
|
|
1881
|
+
}
|
|
1882
|
+
parsed.sort((a, b) => a.frontmatter.id - b.frontmatter.id);
|
|
1883
|
+
return parsed;
|
|
1884
|
+
}
|
|
1885
|
+
function updateFrontmatterStatus(content, newStatus, extraFields) {
|
|
1886
|
+
const m = content.match(FRONTMATTER_RE);
|
|
1887
|
+
if (!m) return content;
|
|
1888
|
+
const fmRaw = m[1];
|
|
1889
|
+
const body = m[2] ?? "";
|
|
1890
|
+
const lines = fmRaw.split(/\r?\n/);
|
|
1891
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
1892
|
+
let hadStatus = false;
|
|
1893
|
+
const updated = lines.map((raw) => {
|
|
1894
|
+
const trimmed = raw.trim();
|
|
1895
|
+
if (!trimmed || trimmed.startsWith("#")) return raw;
|
|
1896
|
+
const idx = trimmed.indexOf(":");
|
|
1897
|
+
if (idx <= 0) return raw;
|
|
1898
|
+
const key = trimmed.slice(0, idx).trim();
|
|
1899
|
+
seenKeys.add(key);
|
|
1900
|
+
if (key === "status") {
|
|
1901
|
+
hadStatus = true;
|
|
1902
|
+
return `status: ${newStatus}`;
|
|
1903
|
+
}
|
|
1904
|
+
if (extraFields && key in extraFields) {
|
|
1905
|
+
return `${key}: ${extraFields[key]}`;
|
|
1906
|
+
}
|
|
1907
|
+
return raw;
|
|
1908
|
+
});
|
|
1909
|
+
if (!hadStatus) updated.push(`status: ${newStatus}`);
|
|
1910
|
+
if (extraFields) {
|
|
1911
|
+
for (const [k, v] of Object.entries(extraFields)) {
|
|
1912
|
+
if (!seenKeys.has(k)) updated.push(`${k}: ${v}`);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return `---
|
|
1916
|
+
${updated.join("\n")}
|
|
1917
|
+
---
|
|
1918
|
+
${body}`;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// src/commands/goal.ts
|
|
1922
|
+
var GOALS_DIR = "goals";
|
|
1923
|
+
var STATE_DIR = "docs/state";
|
|
1924
|
+
var SCRIPTS_DIR = "scripts";
|
|
1925
|
+
var STATUS_ICON = {
|
|
1926
|
+
NOT_STARTED: "\u26AA",
|
|
1927
|
+
IN_PROGRESS: "\u{1F7E1}",
|
|
1928
|
+
DONE: "\u2705",
|
|
1929
|
+
BLOCKED: "\u{1F6D1}"
|
|
1930
|
+
};
|
|
1931
|
+
function selectActiveId(goals) {
|
|
1932
|
+
const ip = goals.find((g) => g.frontmatter.status === "IN_PROGRESS");
|
|
1933
|
+
if (ip && typeof ip.frontmatter.id === "number") return ip.frontmatter.id;
|
|
1934
|
+
const ns = goals.find(
|
|
1935
|
+
(g) => g.frontmatter.status === "NOT_STARTED" || g.frontmatter.status === void 0
|
|
1936
|
+
);
|
|
1937
|
+
if (ns && typeof ns.frontmatter.id === "number") return ns.frontmatter.id;
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
function resolveGoalId(optId, goals) {
|
|
1941
|
+
if (optId !== void 0) {
|
|
1942
|
+
const n = Number(optId);
|
|
1943
|
+
if (!Number.isFinite(n)) return null;
|
|
1944
|
+
return n;
|
|
1945
|
+
}
|
|
1946
|
+
return selectActiveId(goals);
|
|
1947
|
+
}
|
|
1948
|
+
async function goalList() {
|
|
1949
|
+
console.log(chalk6.bold(`
|
|
1950
|
+
${ko.goal.listTitle}
|
|
2294
1951
|
`));
|
|
2295
|
-
const
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
console.log(
|
|
2299
|
-
console.log(chalk7.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
|
|
1952
|
+
const goals = listGoals(GOALS_DIR);
|
|
1953
|
+
if (goals.length === 0) {
|
|
1954
|
+
console.log(chalk6.yellow(" \u{1F4ED} goals/ \uB514\uB809\uD1A0\uB9AC\uC5D0 goal \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
1955
|
+
console.log(chalk6.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
|
|
2300
1956
|
return;
|
|
2301
1957
|
}
|
|
2302
|
-
const
|
|
2303
|
-
|
|
1958
|
+
for (const g of goals) {
|
|
1959
|
+
const fm = g.frontmatter;
|
|
1960
|
+
const status2 = fm.status ?? "NOT_STARTED";
|
|
1961
|
+
const icon = STATUS_ICON[status2] ?? "?";
|
|
1962
|
+
const id = String(fm.id).padStart(2);
|
|
1963
|
+
const pri = String(fm.priority ?? "--").padEnd(3);
|
|
1964
|
+
const ver = String(fm.version ?? "----").padEnd(6);
|
|
1965
|
+
console.log(
|
|
1966
|
+
` [${id}] ${icon} ${status2.padEnd(11)} ${pri} ${ver} ${fm.title ?? "(untitled)"}`
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
async function goalNext() {
|
|
1971
|
+
console.log(chalk6.bold(`
|
|
1972
|
+
${ko.goal.nextTitle}
|
|
2304
1973
|
`));
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
1974
|
+
const goals = listGoals(GOALS_DIR);
|
|
1975
|
+
const activeId = selectActiveId(goals);
|
|
1976
|
+
if (activeId === null) {
|
|
1977
|
+
console.log(chalk6.green(" \u{1F389} \uBAA8\uB4E0 goal \uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!"));
|
|
2308
1978
|
return;
|
|
2309
1979
|
}
|
|
2310
|
-
const
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
1980
|
+
const active = goals.find((g) => g.frontmatter.id === activeId);
|
|
1981
|
+
if (!active) return;
|
|
1982
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1983
|
+
const text = [
|
|
1984
|
+
"# Next Task",
|
|
1985
|
+
"",
|
|
1986
|
+
`_Auto-updated ${ts} via \`vhk goal next\`._`,
|
|
1987
|
+
"",
|
|
1988
|
+
"```",
|
|
1989
|
+
`TASK: Goal ${activeId} \u2014 ${active.frontmatter.title ?? ""}`,
|
|
1990
|
+
` status: ${active.frontmatter.status ?? "NOT_STARTED"}`,
|
|
1991
|
+
` priority: ${active.frontmatter.priority ?? "--"}`,
|
|
1992
|
+
` file: ${active.filePath}`,
|
|
1993
|
+
"```",
|
|
1994
|
+
""
|
|
1995
|
+
].join("\n");
|
|
1996
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
1997
|
+
writeFileSync(join2(STATE_DIR, "next-task.md"), text, "utf-8");
|
|
1998
|
+
console.log(
|
|
1999
|
+
chalk6.green(
|
|
2000
|
+
` \u2705 next-task.md \uAC31\uC2E0 \u2014 Goal ${activeId}: ${active.frontmatter.title ?? ""}`
|
|
2001
|
+
)
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
var META_TEMPLATE = `---
|
|
2005
|
+
vhk_format: 1
|
|
2006
|
+
type: meta
|
|
2007
|
+
project: __FILL__
|
|
2008
|
+
version: v0.1
|
|
2009
|
+
---
|
|
2010
|
+
|
|
2011
|
+
# Common Gates
|
|
2012
|
+
|
|
2013
|
+
1. (\uD504\uB85C\uC81D\uD2B8\uBCC4 \uAC8C\uC774\uD2B8 \u2014 \uC608: pnpm test:run)
|
|
2014
|
+
|
|
2015
|
+
## Forbidden Actions (\uC804\uC5ED)
|
|
2016
|
+
|
|
2017
|
+
- (\uD574\uB2F9 \uC0AC\uD56D)
|
|
2018
|
+
`;
|
|
2019
|
+
var STATE_NEXT_TASK_TEMPLATE = "# Next Task\n\n```\nTASK: (vhk goal next \uB85C \uC790\uB3D9 \uAC31\uC2E0)\n```\n";
|
|
2020
|
+
var STATE_BLOCKERS_TEMPLATE = "# Blockers\n\n_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._\n";
|
|
2021
|
+
var STATE_LEARNINGS_TEMPLATE = "# Learnings\n\n_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._\n";
|
|
2022
|
+
async function goalInit() {
|
|
2023
|
+
console.log(chalk6.bold(`
|
|
2024
|
+
${ko.goal.initTitle}
|
|
2025
|
+
`));
|
|
2026
|
+
const targets = [
|
|
2027
|
+
{ path: join2(GOALS_DIR, "_meta.md"), content: META_TEMPLATE },
|
|
2028
|
+
{ path: join2(STATE_DIR, "next-task.md"), content: STATE_NEXT_TASK_TEMPLATE },
|
|
2029
|
+
{ path: join2(STATE_DIR, "blockers.md"), content: STATE_BLOCKERS_TEMPLATE },
|
|
2030
|
+
{ path: join2(STATE_DIR, "learnings.md"), content: STATE_LEARNINGS_TEMPLATE }
|
|
2031
|
+
];
|
|
2032
|
+
mkdirSync(GOALS_DIR, { recursive: true });
|
|
2033
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
2034
|
+
let created = 0;
|
|
2035
|
+
let skipped = 0;
|
|
2036
|
+
for (const t2 of targets) {
|
|
2037
|
+
if (existsSync2(t2.path)) {
|
|
2038
|
+
console.log(chalk6.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${t2.path}`));
|
|
2039
|
+
skipped++;
|
|
2317
2040
|
} else {
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
const icon = v.severity === "error" ? chalk7.red("\u2716") : v.severity === "warning" ? chalk7.yellow("\u26A0") : chalk7.blue("\u2139");
|
|
2322
|
-
console.log(` ${icon} ${v.message}${loc}`);
|
|
2323
|
-
});
|
|
2324
|
-
allViolations.push(...violations);
|
|
2041
|
+
writeFileSync(t2.path, t2.content, "utf-8");
|
|
2042
|
+
console.log(chalk6.green(` \u2713 created: ${t2.path}`));
|
|
2043
|
+
created++;
|
|
2325
2044
|
}
|
|
2326
2045
|
}
|
|
2327
|
-
console.log(
|
|
2328
|
-
|
|
2046
|
+
console.log(chalk6.bold(`
|
|
2047
|
+
\u{1F4CA} created=${created} skipped=${skipped}`));
|
|
2048
|
+
if (created > 0) {
|
|
2049
|
+
printNextStep({
|
|
2050
|
+
message: "goals/ \uAD6C\uC870 \uC2A4\uCE90\uD3F4\uB529 \uC644\uB8CC!",
|
|
2051
|
+
command: "vhk goal list",
|
|
2052
|
+
cursorHint: "goal \uBAA9\uB85D \uBCF4\uC5EC\uC918"
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
function findGateScript(id) {
|
|
2057
|
+
const mjs = join2(SCRIPTS_DIR, `check-goal-${id}.mjs`);
|
|
2058
|
+
if (existsSync2(mjs)) return mjs;
|
|
2059
|
+
const sh = join2(SCRIPTS_DIR, `check-goal-${id}.sh`);
|
|
2060
|
+
if (existsSync2(sh)) return sh;
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
2063
|
+
function runGate(scriptPath) {
|
|
2064
|
+
const isMjs = scriptPath.endsWith(".mjs");
|
|
2065
|
+
const runner = isMjs ? "node" : "bash";
|
|
2066
|
+
const r = safeExecFile(runner, [scriptPath]);
|
|
2067
|
+
return { ok: r.ok, out: r.out, err: r.ok ? "" : r.err, runner };
|
|
2068
|
+
}
|
|
2069
|
+
async function goalCheck(opts) {
|
|
2070
|
+
console.log(chalk6.bold(`
|
|
2071
|
+
${ko.goal.checkTitle}
|
|
2072
|
+
`));
|
|
2073
|
+
const goals = listGoals(GOALS_DIR);
|
|
2074
|
+
const id = resolveGoalId(opts.id, goals);
|
|
2075
|
+
if (id === null) {
|
|
2076
|
+
console.log(
|
|
2077
|
+
chalk6.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
|
|
2078
|
+
);
|
|
2079
|
+
process.exitCode = 1;
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const scriptPath = findGateScript(id);
|
|
2083
|
+
if (!scriptPath) {
|
|
2084
|
+
console.log(
|
|
2085
|
+
chalk6.red(` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C: scripts/check-goal-${id}.{mjs,sh}`)
|
|
2086
|
+
);
|
|
2087
|
+
process.exitCode = 1;
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
const gate2 = runGate(scriptPath);
|
|
2091
|
+
console.log(chalk6.dim(` \u25B6 ${gate2.runner} ${scriptPath}
|
|
2092
|
+
`));
|
|
2093
|
+
if (gate2.out) console.log(gate2.out);
|
|
2094
|
+
if (gate2.ok) {
|
|
2095
|
+
console.log(chalk6.green(`
|
|
2096
|
+
\u2705 Goal ${id} \uAC8C\uC774\uD2B8 \uD1B5\uACFC`));
|
|
2097
|
+
} else {
|
|
2098
|
+
console.log(chalk6.red(`
|
|
2099
|
+
\u274C Goal ${id} \uAC8C\uC774\uD2B8 \uC2E4\uD328`));
|
|
2100
|
+
if (gate2.err && !gate2.out) console.log(chalk6.dim(gate2.err.slice(0, 500)));
|
|
2101
|
+
process.exitCode = 1;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
async function goalDone(opts) {
|
|
2105
|
+
console.log(chalk6.bold(`
|
|
2106
|
+
${ko.goal.doneTitle}
|
|
2107
|
+
`));
|
|
2108
|
+
const goals = listGoals(GOALS_DIR);
|
|
2109
|
+
const id = resolveGoalId(opts.id, goals);
|
|
2110
|
+
if (id === null) {
|
|
2111
|
+
console.log(
|
|
2112
|
+
chalk6.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
|
|
2113
|
+
);
|
|
2114
|
+
process.exitCode = 1;
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
const target = goals.find((g) => g.frontmatter.id === id);
|
|
2118
|
+
if (!target) {
|
|
2119
|
+
console.log(chalk6.red(` \u274C goal id ${id} \uD30C\uC77C \uC5C6\uC74C.`));
|
|
2120
|
+
process.exitCode = 1;
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
const scriptPath = findGateScript(id);
|
|
2124
|
+
if (!scriptPath) {
|
|
2125
|
+
console.log(
|
|
2126
|
+
chalk6.red(
|
|
2127
|
+
` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C \u2014 done \uCC98\uB9AC \uAC70\uBD80: scripts/check-goal-${id}.{mjs,sh}`
|
|
2128
|
+
)
|
|
2129
|
+
);
|
|
2130
|
+
process.exitCode = 1;
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const gate2 = runGate(scriptPath);
|
|
2134
|
+
console.log(chalk6.dim(` \u25B6 \uAC8C\uC774\uD2B8 \uAC80\uC99D: ${gate2.runner} ${scriptPath}
|
|
2135
|
+
`));
|
|
2136
|
+
if (gate2.out) console.log(gate2.out);
|
|
2137
|
+
if (!gate2.ok) {
|
|
2138
|
+
console.log(
|
|
2139
|
+
chalk6.red(
|
|
2140
|
+
`
|
|
2141
|
+
\u274C \uAC8C\uC774\uD2B8 \uC2E4\uD328 \u2014 frontmatter \uBCC0\uACBD \uC5C6\uC774 \uC885\uB8CC. (Forbidden: \uC2E4\uD328 = \uBCF4\uC874)`
|
|
2142
|
+
)
|
|
2143
|
+
);
|
|
2144
|
+
process.exitCode = 1;
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
const content = readFileSync2(target.filePath, "utf-8");
|
|
2148
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2149
|
+
const updated = updateFrontmatterStatus(content, "DONE", { completed: today });
|
|
2150
|
+
writeFileSync(target.filePath, updated, "utf-8");
|
|
2151
|
+
console.log(chalk6.green(`
|
|
2152
|
+
\u2705 Goal ${id} \u2192 DONE (completed: ${today})`));
|
|
2153
|
+
printNextStep({
|
|
2154
|
+
message: `Goal ${id} \uC644\uB8CC! \uB2E4\uC74C goal \uB85C:`,
|
|
2155
|
+
command: "vhk goal next",
|
|
2156
|
+
cursorHint: "\uB2E4\uC74C goal \uC54C\uB824\uC918"
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// src/commands/check.ts
|
|
2161
|
+
async function check(opts = {}) {
|
|
2162
|
+
if (opts.goal !== void 0) {
|
|
2163
|
+
return goalCheck({ id: opts.goal });
|
|
2164
|
+
}
|
|
2165
|
+
return checkRules();
|
|
2166
|
+
}
|
|
2167
|
+
async function checkRules() {
|
|
2168
|
+
console.log(chalk7.bold(`
|
|
2169
|
+
${ko.check.title}
|
|
2170
|
+
`));
|
|
2171
|
+
const cwd = process.cwd();
|
|
2172
|
+
const rulesPath = path8.join(cwd, "RULES.md");
|
|
2173
|
+
if (!fs7.existsSync(rulesPath)) {
|
|
2174
|
+
console.log(chalk7.yellow(ko.check.noRules));
|
|
2175
|
+
console.log(chalk7.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
const rules = parseRules(rulesPath);
|
|
2179
|
+
console.log(chalk7.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
|
|
2180
|
+
`));
|
|
2181
|
+
if (rules.length === 0) {
|
|
2182
|
+
console.log(chalk7.yellow(ko.check.noAutoRules));
|
|
2183
|
+
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."));
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const allViolations = [];
|
|
2187
|
+
let passCount = 0;
|
|
2188
|
+
for (const rule of rules) {
|
|
2189
|
+
const violations = rule.check(cwd);
|
|
2190
|
+
if (violations.length === 0) {
|
|
2191
|
+
console.log(chalk7.green(` \u2705 ${rule.id}`) + chalk7.dim(` \u2014 ${rule.description.slice(0, 60)}`));
|
|
2192
|
+
passCount++;
|
|
2193
|
+
} else {
|
|
2194
|
+
console.log(chalk7.red(` \u274C ${rule.id}`) + chalk7.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
|
|
2195
|
+
violations.forEach((v) => {
|
|
2196
|
+
const loc = v.file ? chalk7.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
|
|
2197
|
+
const icon = v.severity === "error" ? chalk7.red("\u2716") : v.severity === "warning" ? chalk7.yellow("\u26A0") : chalk7.blue("\u2139");
|
|
2198
|
+
console.log(` ${icon} ${v.message}${loc}`);
|
|
2199
|
+
});
|
|
2200
|
+
allViolations.push(...violations);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
console.log("");
|
|
2204
|
+
const errors = allViolations.filter((v) => v.severity === "error").length;
|
|
2329
2205
|
const warnings = allViolations.filter((v) => v.severity === "warning").length;
|
|
2330
2206
|
if (allViolations.length === 0) {
|
|
2331
2207
|
console.log(chalk7.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
|
|
@@ -2352,217 +2228,20 @@ ${ko.check.title}
|
|
|
2352
2228
|
|
|
2353
2229
|
// src/commands/secure.ts
|
|
2354
2230
|
import chalk8 from "chalk";
|
|
2355
|
-
import
|
|
2356
|
-
import
|
|
2357
|
-
|
|
2358
|
-
// src/lib/scan-secrets.ts
|
|
2359
|
-
import fs10 from "fs";
|
|
2360
|
-
|
|
2361
|
-
// src/lib/secret-patterns.ts
|
|
2362
|
-
var SECRET_PATTERNS = [
|
|
2363
|
-
{
|
|
2364
|
-
id: "aws-access-key",
|
|
2365
|
-
name: "AWS Access Key",
|
|
2366
|
-
severity: "critical",
|
|
2367
|
-
pattern: /AKIA[0-9A-Z]{16}/
|
|
2368
|
-
},
|
|
2369
|
-
{
|
|
2370
|
-
id: "aws-secret-key",
|
|
2371
|
-
name: "AWS Secret Key",
|
|
2372
|
-
severity: "critical",
|
|
2373
|
-
pattern: /aws_secret_access_key\s*=\s*['"]?[A-Za-z0-9/+=]{40}['"]?/i
|
|
2374
|
-
},
|
|
2375
|
-
{
|
|
2376
|
-
id: "private-key",
|
|
2377
|
-
name: "Private Key",
|
|
2378
|
-
severity: "critical",
|
|
2379
|
-
pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/
|
|
2380
|
-
},
|
|
2381
|
-
{
|
|
2382
|
-
id: "notion-token",
|
|
2383
|
-
name: "Notion Integration Token",
|
|
2384
|
-
severity: "critical",
|
|
2385
|
-
pattern: /secret_[A-Za-z0-9]{40,50}/
|
|
2386
|
-
},
|
|
2387
|
-
{
|
|
2388
|
-
id: "github-token",
|
|
2389
|
-
name: "GitHub Token",
|
|
2390
|
-
severity: "critical",
|
|
2391
|
-
pattern: /ghp_[A-Za-z0-9]{36,}/
|
|
2392
|
-
},
|
|
2393
|
-
{
|
|
2394
|
-
id: "openai-key",
|
|
2395
|
-
name: "OpenAI API Key",
|
|
2396
|
-
severity: "critical",
|
|
2397
|
-
pattern: /\bsk-(?:proj-|ant-api03-|live-)[A-Za-z0-9_-]{16,}\b/
|
|
2398
|
-
},
|
|
2399
|
-
{
|
|
2400
|
-
id: "generic-api-key",
|
|
2401
|
-
name: "Generic API Key",
|
|
2402
|
-
severity: "high",
|
|
2403
|
-
pattern: /(?:api[_-]?key|apikey|access[_-]?token)\s*[:=]\s*['"]?[A-Za-z0-9_\-]{16,}['"]?/i
|
|
2404
|
-
},
|
|
2405
|
-
{
|
|
2406
|
-
id: "password-inline",
|
|
2407
|
-
name: "Inline Password",
|
|
2408
|
-
severity: "high",
|
|
2409
|
-
pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/i
|
|
2410
|
-
},
|
|
2411
|
-
{
|
|
2412
|
-
id: "jwt",
|
|
2413
|
-
name: "JWT Token",
|
|
2414
|
-
severity: "medium",
|
|
2415
|
-
pattern: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/
|
|
2416
|
-
}
|
|
2417
|
-
];
|
|
2418
|
-
function maskSecret(value) {
|
|
2419
|
-
if (value.length <= 8) return "****";
|
|
2420
|
-
const visible = Math.min(8, value.length - 4);
|
|
2421
|
-
return value.slice(0, visible) + "****";
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
// src/lib/scan-files.ts
|
|
2425
|
-
import fs9 from "fs";
|
|
2426
|
-
import path10 from "path";
|
|
2427
|
-
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
2428
|
-
"node_modules",
|
|
2429
|
-
".git",
|
|
2430
|
-
"dist",
|
|
2431
|
-
".next",
|
|
2432
|
-
".nuxt",
|
|
2433
|
-
"build",
|
|
2434
|
-
"coverage",
|
|
2435
|
-
"out",
|
|
2436
|
-
".turbo",
|
|
2437
|
-
".vercel",
|
|
2438
|
-
".cache",
|
|
2439
|
-
".pnpm",
|
|
2440
|
-
".idea",
|
|
2441
|
-
".claude",
|
|
2442
|
-
".cursor"
|
|
2443
|
-
]);
|
|
2444
|
-
var SKIP_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
2445
|
-
"pnpm-lock.yaml",
|
|
2446
|
-
"package-lock.json",
|
|
2447
|
-
"yarn.lock"
|
|
2448
|
-
]);
|
|
2449
|
-
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2450
|
-
".ts",
|
|
2451
|
-
".tsx",
|
|
2452
|
-
".js",
|
|
2453
|
-
".jsx",
|
|
2454
|
-
".mjs",
|
|
2455
|
-
".cjs",
|
|
2456
|
-
".json",
|
|
2457
|
-
".yaml",
|
|
2458
|
-
".yml",
|
|
2459
|
-
".toml"
|
|
2460
|
-
]);
|
|
2461
|
-
var MAX_SCAN_FILE_BYTES = 512 * 1024;
|
|
2462
|
-
function isScannableFileName(fileName) {
|
|
2463
|
-
if (SKIP_FILE_NAMES.has(fileName)) return false;
|
|
2464
|
-
if (fileName.startsWith(".env")) return true;
|
|
2465
|
-
return SCAN_EXTENSIONS.has(path10.extname(fileName).toLowerCase());
|
|
2466
|
-
}
|
|
2467
|
-
function walkProjectFiles(rootDir, onFile, ig = loadGitignore(rootDir)) {
|
|
2468
|
-
function walk(dir) {
|
|
2469
|
-
let entries;
|
|
2470
|
-
try {
|
|
2471
|
-
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
2472
|
-
} catch {
|
|
2473
|
-
return;
|
|
2474
|
-
}
|
|
2475
|
-
for (const entry of entries) {
|
|
2476
|
-
const fullPath = path10.join(dir, entry.name);
|
|
2477
|
-
const rel = path10.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
2478
|
-
if (entry.isDirectory()) {
|
|
2479
|
-
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
2480
|
-
if (isPathIgnored(ig, `${rel}/`)) continue;
|
|
2481
|
-
walk(fullPath);
|
|
2482
|
-
continue;
|
|
2483
|
-
}
|
|
2484
|
-
if (!isScannableFileName(entry.name)) continue;
|
|
2485
|
-
if (isPathIgnored(ig, rel)) continue;
|
|
2486
|
-
let size = 0;
|
|
2487
|
-
try {
|
|
2488
|
-
size = fs9.statSync(fullPath).size;
|
|
2489
|
-
} catch {
|
|
2490
|
-
continue;
|
|
2491
|
-
}
|
|
2492
|
-
if (size > MAX_SCAN_FILE_BYTES) continue;
|
|
2493
|
-
onFile(fullPath, rel);
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
walk(rootDir);
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
// src/lib/scan-secrets.ts
|
|
2500
|
-
var MAX_SECRET_FINDINGS = 200;
|
|
2501
|
-
var MAX_LINE_CHARS = 4e3;
|
|
2502
|
-
function globalPattern(pattern) {
|
|
2503
|
-
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
2504
|
-
return new RegExp(pattern.source, flags);
|
|
2505
|
-
}
|
|
2506
|
-
function findSecretsInLine(line, relPath, lineNum) {
|
|
2507
|
-
const found = [];
|
|
2508
|
-
const trimmed = line.trim();
|
|
2509
|
-
if (trimmed.startsWith("//") && trimmed.includes("example")) return found;
|
|
2510
|
-
if (trimmed.startsWith("#") && trimmed.includes("example")) return found;
|
|
2511
|
-
if (line.length > MAX_LINE_CHARS) return found;
|
|
2512
|
-
for (const pattern of SECRET_PATTERNS) {
|
|
2513
|
-
const regex = globalPattern(pattern.pattern);
|
|
2514
|
-
for (const match of line.matchAll(regex)) {
|
|
2515
|
-
found.push({
|
|
2516
|
-
patternId: pattern.id,
|
|
2517
|
-
patternName: pattern.name,
|
|
2518
|
-
severity: pattern.severity,
|
|
2519
|
-
file: relPath,
|
|
2520
|
-
line: lineNum,
|
|
2521
|
-
match: maskSecret(match[0])
|
|
2522
|
-
});
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
return found;
|
|
2526
|
-
}
|
|
2527
|
-
function scanProjectForSecrets(cwd) {
|
|
2528
|
-
const findings = [];
|
|
2529
|
-
let scannedFiles = 0;
|
|
2530
|
-
let truncated = false;
|
|
2531
|
-
walkProjectFiles(cwd, (filePath, relPath) => {
|
|
2532
|
-
scannedFiles++;
|
|
2533
|
-
const content = fs10.readFileSync(filePath, "utf-8");
|
|
2534
|
-
const lines = content.split("\n");
|
|
2535
|
-
lines.forEach((line, idx) => {
|
|
2536
|
-
if (truncated) return;
|
|
2537
|
-
const lineFindings = findSecretsInLine(line, relPath, idx + 1);
|
|
2538
|
-
for (const f of lineFindings) {
|
|
2539
|
-
findings.push(f);
|
|
2540
|
-
if (findings.length >= MAX_SECRET_FINDINGS) {
|
|
2541
|
-
truncated = true;
|
|
2542
|
-
return;
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
});
|
|
2546
|
-
});
|
|
2547
|
-
return { findings, scannedFiles, truncated };
|
|
2548
|
-
}
|
|
2549
|
-
function filterSevereFindings(findings) {
|
|
2550
|
-
return findings.filter((f) => f.severity === "critical" || f.severity === "high");
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
// src/commands/secure.ts
|
|
2231
|
+
import fs8 from "fs";
|
|
2232
|
+
import path9 from "path";
|
|
2554
2233
|
async function secure() {
|
|
2555
2234
|
console.log(chalk8.bold(`
|
|
2556
2235
|
${ko.secure.title}
|
|
2557
2236
|
`));
|
|
2558
2237
|
const cwd = process.cwd();
|
|
2559
|
-
const gitignorePath =
|
|
2560
|
-
const hasGitignore =
|
|
2238
|
+
const gitignorePath = path9.join(cwd, ".gitignore");
|
|
2239
|
+
const hasGitignore = fs8.existsSync(gitignorePath);
|
|
2561
2240
|
if (!hasGitignore) {
|
|
2562
2241
|
console.log(chalk8.yellow(` ${ko.secure.noGitignore}`));
|
|
2563
2242
|
console.log(chalk8.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
|
|
2564
2243
|
} else {
|
|
2565
|
-
const gitignoreContent =
|
|
2244
|
+
const gitignoreContent = fs8.readFileSync(gitignorePath, "utf-8");
|
|
2566
2245
|
if (!gitignoreContent.includes(".env")) {
|
|
2567
2246
|
console.log(chalk8.yellow(` ${ko.secure.noEnvInGitignore}`));
|
|
2568
2247
|
console.log(chalk8.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
|
|
@@ -2626,27 +2305,24 @@ ${ko.secure.title}
|
|
|
2626
2305
|
|
|
2627
2306
|
// src/commands/doctor.ts
|
|
2628
2307
|
import chalk9 from "chalk";
|
|
2629
|
-
import
|
|
2630
|
-
import
|
|
2631
|
-
import path12 from "path";
|
|
2308
|
+
import fs9 from "fs";
|
|
2309
|
+
import path10 from "path";
|
|
2632
2310
|
import { fileURLToPath } from "url";
|
|
2633
2311
|
function checkCommand(name, command, hint) {
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
return { name, command, ok: false, hint };
|
|
2639
|
-
}
|
|
2312
|
+
const result = safeExecFile(command, ["--version"]);
|
|
2313
|
+
if (!result.ok) return { name, command, ok: false, hint };
|
|
2314
|
+
const version = result.out.split("\n")[0];
|
|
2315
|
+
return { name, command, version, ok: true, hint };
|
|
2640
2316
|
}
|
|
2641
|
-
function
|
|
2642
|
-
const dir =
|
|
2317
|
+
function getVhkVersion2() {
|
|
2318
|
+
const dir = path10.dirname(fileURLToPath(import.meta.url));
|
|
2643
2319
|
const candidates = [
|
|
2644
|
-
|
|
2645
|
-
|
|
2320
|
+
path10.join(dir, "../package.json"),
|
|
2321
|
+
path10.join(dir, "../../package.json")
|
|
2646
2322
|
];
|
|
2647
2323
|
for (const pkgPath of candidates) {
|
|
2648
2324
|
try {
|
|
2649
|
-
if (
|
|
2325
|
+
if (fs9.existsSync(pkgPath)) {
|
|
2650
2326
|
const pkg = readJsonFile(pkgPath);
|
|
2651
2327
|
return pkg.version;
|
|
2652
2328
|
}
|
|
@@ -2657,17 +2333,11 @@ function getVhkVersion() {
|
|
|
2657
2333
|
return void 0;
|
|
2658
2334
|
}
|
|
2659
2335
|
function fetchLatestNpmVersion(packageName) {
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
}).trim();
|
|
2666
|
-
if (/^\d+\.\d+\.\d+/.test(result)) return result;
|
|
2667
|
-
return void 0;
|
|
2668
|
-
} catch {
|
|
2669
|
-
return void 0;
|
|
2670
|
-
}
|
|
2336
|
+
const result = safeExecFile("npm", ["view", packageName, "version"]);
|
|
2337
|
+
if (!result.ok) return void 0;
|
|
2338
|
+
const out = result.out;
|
|
2339
|
+
if (/^\d+\.\d+\.\d+/.test(out)) return out;
|
|
2340
|
+
return void 0;
|
|
2671
2341
|
}
|
|
2672
2342
|
function compareSemver(a, b) {
|
|
2673
2343
|
const parse = (v) => v.replace(/^v/i, "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -2698,7 +2368,7 @@ ${ko.doctor.title}
|
|
|
2698
2368
|
}
|
|
2699
2369
|
}
|
|
2700
2370
|
console.log("");
|
|
2701
|
-
const vhkVersion =
|
|
2371
|
+
const vhkVersion = getVhkVersion2();
|
|
2702
2372
|
if (vhkVersion) {
|
|
2703
2373
|
console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(` \u2014 v${vhkVersion}`));
|
|
2704
2374
|
} else {
|
|
@@ -2723,13 +2393,13 @@ ${ko.doctor.title}
|
|
|
2723
2393
|
{ name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
|
|
2724
2394
|
];
|
|
2725
2395
|
for (const file of projectFiles) {
|
|
2726
|
-
const exists =
|
|
2396
|
+
const exists = fs9.existsSync(path10.join(cwd, file.name));
|
|
2727
2397
|
if (exists) {
|
|
2728
2398
|
console.log(chalk9.green(` \u2705 ${file.name}`));
|
|
2729
2399
|
if (file.name === ".env") {
|
|
2730
|
-
const gitignorePath =
|
|
2731
|
-
if (
|
|
2732
|
-
const gitignore =
|
|
2400
|
+
const gitignorePath = path10.join(cwd, ".gitignore");
|
|
2401
|
+
if (fs9.existsSync(gitignorePath)) {
|
|
2402
|
+
const gitignore = fs9.readFileSync(gitignorePath, "utf-8");
|
|
2733
2403
|
if (!gitignore.includes(".env")) {
|
|
2734
2404
|
console.log(chalk9.yellow(` ${ko.doctor.envNotIgnored}`));
|
|
2735
2405
|
}
|
|
@@ -2761,8 +2431,8 @@ ${ko.doctor.title}
|
|
|
2761
2431
|
// src/commands/ship.ts
|
|
2762
2432
|
import chalk10 from "chalk";
|
|
2763
2433
|
import inquirer4 from "inquirer";
|
|
2764
|
-
import
|
|
2765
|
-
import
|
|
2434
|
+
import fs10 from "fs";
|
|
2435
|
+
import path11 from "path";
|
|
2766
2436
|
var CHECKLIST = [
|
|
2767
2437
|
{ id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
|
|
2768
2438
|
{ id: "test", questionKey: "checkTest", hintKey: "hintTest" },
|
|
@@ -2775,9 +2445,9 @@ function sanitizeVersion(version) {
|
|
|
2775
2445
|
return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
|
|
2776
2446
|
}
|
|
2777
2447
|
function updateChangelogUnreleased(cwd, version, date) {
|
|
2778
|
-
const changelogPath =
|
|
2779
|
-
if (!
|
|
2780
|
-
const content =
|
|
2448
|
+
const changelogPath = path11.join(cwd, "CHANGELOG.md");
|
|
2449
|
+
if (!fs10.existsSync(changelogPath)) return { status: "missing" };
|
|
2450
|
+
const content = fs10.readFileSync(changelogPath, "utf-8");
|
|
2781
2451
|
const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
|
|
2782
2452
|
if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
|
|
2783
2453
|
const blankUnreleased = [
|
|
@@ -2794,7 +2464,7 @@ function updateChangelogUnreleased(cwd, version, date) {
|
|
|
2794
2464
|
`## [${version}] \u2014 ${date}`
|
|
2795
2465
|
].join("\n");
|
|
2796
2466
|
const updated = content.replace(unreleasedHeading, blankUnreleased);
|
|
2797
|
-
|
|
2467
|
+
fs10.writeFileSync(changelogPath, updated, "utf-8");
|
|
2798
2468
|
return { status: "updated", version };
|
|
2799
2469
|
}
|
|
2800
2470
|
async function ship() {
|
|
@@ -2851,12 +2521,12 @@ ${ko.ship.title}
|
|
|
2851
2521
|
{ type: "input", name: "learned", message: ko.ship.questionLearned },
|
|
2852
2522
|
{ type: "input", name: "nextVersion", message: ko.ship.questionNext }
|
|
2853
2523
|
]);
|
|
2854
|
-
const buildLogDir =
|
|
2855
|
-
if (!
|
|
2524
|
+
const buildLogDir = path11.join(cwd, "docs", "build-log");
|
|
2525
|
+
if (!fs10.existsSync(buildLogDir)) fs10.mkdirSync(buildLogDir, { recursive: true });
|
|
2856
2526
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2857
2527
|
const versionSlug = sanitizeVersion(retro.version);
|
|
2858
2528
|
const fileName = `${today}-v${versionSlug}.md`;
|
|
2859
|
-
const filePath =
|
|
2529
|
+
const filePath = path11.join(buildLogDir, fileName);
|
|
2860
2530
|
const content = [
|
|
2861
2531
|
`# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
|
|
2862
2532
|
"",
|
|
@@ -2885,9 +2555,9 @@ ${ko.ship.title}
|
|
|
2885
2555
|
"---",
|
|
2886
2556
|
`*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
2887
2557
|
].join("\n");
|
|
2888
|
-
|
|
2558
|
+
fs10.writeFileSync(filePath, content, "utf-8");
|
|
2889
2559
|
console.log(chalk10.green(`
|
|
2890
|
-
${ko.ship.buildLogDone(
|
|
2560
|
+
${ko.ship.buildLogDone(path11.relative(cwd, filePath))}`));
|
|
2891
2561
|
const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
|
|
2892
2562
|
if (changelogResult.status === "updated") {
|
|
2893
2563
|
log.success(ko.ship.changelogUpdated(changelogResult.version));
|
|
@@ -3055,9 +2725,19 @@ async function save() {
|
|
|
3055
2725
|
if (process.exitCode !== 1) {
|
|
3056
2726
|
console.log(chalk11.green(`
|
|
3057
2727
|
\u2705 ${t("save.done", lines.length)}`));
|
|
2728
|
+
printNextStep({
|
|
2729
|
+
message: t("save.nextOkMessage"),
|
|
2730
|
+
command: "vhk recap",
|
|
2731
|
+
cursorHint: t("save.nextOkCursor")
|
|
2732
|
+
});
|
|
3058
2733
|
} else {
|
|
3059
2734
|
console.log(chalk11.green(`
|
|
3060
2735
|
\u2705 ${t("save.doneLocalOnly", lines.length)}`));
|
|
2736
|
+
printNextStep({
|
|
2737
|
+
message: t("save.nextPushFailMessage"),
|
|
2738
|
+
command: "vhk doctor",
|
|
2739
|
+
cursorHint: t("save.nextPushFailCursor")
|
|
2740
|
+
});
|
|
3061
2741
|
}
|
|
3062
2742
|
} catch (err) {
|
|
3063
2743
|
spinner.fail(t("save.failed"));
|
|
@@ -3177,6 +2857,11 @@ ${t("undo.recentHeader")}`));
|
|
|
3177
2857
|
console.log(chalk12.yellow(`
|
|
3178
2858
|
\u{1F4A1} ${t("undo.forcePushHint")}`));
|
|
3179
2859
|
}
|
|
2860
|
+
printNextStep({
|
|
2861
|
+
message: t("undo.nextMessage"),
|
|
2862
|
+
command: "vhk save",
|
|
2863
|
+
cursorHint: t("undo.nextCursor")
|
|
2864
|
+
});
|
|
3180
2865
|
} catch (err) {
|
|
3181
2866
|
console.log(chalk12.red(`\u274C ${t("undo.failed")}`));
|
|
3182
2867
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -3187,8 +2872,8 @@ ${t("undo.recentHeader")}`));
|
|
|
3187
2872
|
|
|
3188
2873
|
// src/commands/status.ts
|
|
3189
2874
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
3190
|
-
import
|
|
3191
|
-
import
|
|
2875
|
+
import fs11 from "fs";
|
|
2876
|
+
import path12 from "path";
|
|
3192
2877
|
import chalk13 from "chalk";
|
|
3193
2878
|
function countFileChanges(porcelain) {
|
|
3194
2879
|
const lines = porcelain.split("\n").filter(Boolean);
|
|
@@ -3227,8 +2912,8 @@ function parseRecentCommitLines(logOutput) {
|
|
|
3227
2912
|
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3228
2913
|
}
|
|
3229
2914
|
function readProjectPackage(cwd = process.cwd()) {
|
|
3230
|
-
const pkgPath =
|
|
3231
|
-
if (!
|
|
2915
|
+
const pkgPath = path12.join(cwd, "package.json");
|
|
2916
|
+
if (!fs11.existsSync(pkgPath)) return null;
|
|
3232
2917
|
try {
|
|
3233
2918
|
const pkg = readJsonFile(pkgPath);
|
|
3234
2919
|
if (!pkg.name && !pkg.version) return null;
|
|
@@ -3300,18 +2985,27 @@ async function status() {
|
|
|
3300
2985
|
} else {
|
|
3301
2986
|
console.log(chalk13.dim(`\u{1F4E6} ${t("status.noPackage")}`));
|
|
3302
2987
|
}
|
|
3303
|
-
|
|
2988
|
+
const hasChanges = counts.staged + counts.unstaged + counts.untracked > 0;
|
|
2989
|
+
if (hasChanges) {
|
|
2990
|
+
printNextStep({
|
|
2991
|
+
message: t("status.nextWithChangesMessage"),
|
|
2992
|
+
command: "vhk save",
|
|
2993
|
+
cursorHint: t("status.nextWithChangesCursor")
|
|
2994
|
+
});
|
|
2995
|
+
} else {
|
|
2996
|
+
printNextStep({
|
|
2997
|
+
message: t("status.nextCleanMessage"),
|
|
2998
|
+
command: "vhk goal next",
|
|
2999
|
+
cursorHint: t("status.nextCleanCursor")
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
3304
3002
|
}
|
|
3305
3003
|
|
|
3306
3004
|
// src/commands/diff.ts
|
|
3307
|
-
import { execFileSync as execFileSync5, execSync as execSync2 } from "child_process";
|
|
3308
3005
|
import chalk14 from "chalk";
|
|
3309
3006
|
function gitOut2(args) {
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
} catch {
|
|
3313
|
-
return "";
|
|
3314
|
-
}
|
|
3007
|
+
const r = safeExecFile("git", args);
|
|
3008
|
+
return r.ok ? r.out : "";
|
|
3315
3009
|
}
|
|
3316
3010
|
function parseDiffStat(stat) {
|
|
3317
3011
|
const files = [];
|
|
@@ -3354,9 +3048,7 @@ async function diff() {
|
|
|
3354
3048
|
console.log(chalk14.bold(`
|
|
3355
3049
|
\u{1F50D} ${t("diff.title")}`));
|
|
3356
3050
|
console.log(chalk14.gray("\u2500".repeat(40)));
|
|
3357
|
-
|
|
3358
|
-
execSync2("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
|
|
3359
|
-
} catch {
|
|
3051
|
+
if (!safeExecFile("git", ["rev-parse", "--is-inside-work-tree"]).ok) {
|
|
3360
3052
|
console.log(chalk14.red(`\u274C ${t("diff.notGitRepo")}`));
|
|
3361
3053
|
return;
|
|
3362
3054
|
}
|
|
@@ -3397,8 +3089,8 @@ ${t("diff.summaryHeader")}`));
|
|
|
3397
3089
|
}
|
|
3398
3090
|
|
|
3399
3091
|
// src/commands/mcp-init.ts
|
|
3400
|
-
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
3401
|
-
import { join, dirname } from "path";
|
|
3092
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3093
|
+
import { join as join3, dirname } from "path";
|
|
3402
3094
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3403
3095
|
import chalk15 from "chalk";
|
|
3404
3096
|
function resolveMcpEntryPoint() {
|
|
@@ -3406,8 +3098,8 @@ function resolveMcpEntryPoint() {
|
|
|
3406
3098
|
const here = fileURLToPath2(import.meta.url);
|
|
3407
3099
|
const dir = dirname(here);
|
|
3408
3100
|
for (const rel of [["mcp", "index.js"], ["..", "mcp", "index.js"]]) {
|
|
3409
|
-
const candidate =
|
|
3410
|
-
if (
|
|
3101
|
+
const candidate = join3(dir, ...rel);
|
|
3102
|
+
if (existsSync3(candidate)) return candidate;
|
|
3411
3103
|
}
|
|
3412
3104
|
} catch {
|
|
3413
3105
|
}
|
|
@@ -3415,17 +3107,17 @@ function resolveMcpEntryPoint() {
|
|
|
3415
3107
|
const url = import.meta.resolve?.("@byh3071/vhk/dist/mcp/index.js");
|
|
3416
3108
|
if (typeof url === "string") {
|
|
3417
3109
|
const p = fileURLToPath2(url);
|
|
3418
|
-
if (
|
|
3110
|
+
if (existsSync3(p)) return p;
|
|
3419
3111
|
}
|
|
3420
3112
|
} catch {
|
|
3421
3113
|
}
|
|
3422
3114
|
try {
|
|
3423
|
-
const pkgPath =
|
|
3424
|
-
if (
|
|
3115
|
+
const pkgPath = join3(process.cwd(), "package.json");
|
|
3116
|
+
if (existsSync3(pkgPath)) {
|
|
3425
3117
|
const pkg = readJsonFile(pkgPath);
|
|
3426
3118
|
if (pkg.name === "@byh3071/vhk") {
|
|
3427
|
-
const local =
|
|
3428
|
-
if (
|
|
3119
|
+
const local = join3(process.cwd(), "dist", "mcp", "index.js");
|
|
3120
|
+
if (existsSync3(local)) return local;
|
|
3429
3121
|
}
|
|
3430
3122
|
}
|
|
3431
3123
|
} catch {
|
|
@@ -3442,14 +3134,14 @@ function resolveVhkMcpEntry() {
|
|
|
3442
3134
|
async function mcpInit() {
|
|
3443
3135
|
console.log(chalk15.bold("\n\u{1F50C} " + t("mcp.initTitle")));
|
|
3444
3136
|
console.log(chalk15.gray("\u2500".repeat(40)));
|
|
3445
|
-
const cursorDir =
|
|
3446
|
-
if (!
|
|
3447
|
-
|
|
3137
|
+
const cursorDir = join3(process.cwd(), ".cursor");
|
|
3138
|
+
if (!existsSync3(cursorDir)) {
|
|
3139
|
+
mkdirSync2(cursorDir, { recursive: true });
|
|
3448
3140
|
}
|
|
3449
|
-
const configPath =
|
|
3141
|
+
const configPath = join3(cursorDir, "mcp.json");
|
|
3450
3142
|
const vhkEntry = resolveVhkMcpEntry();
|
|
3451
3143
|
let config;
|
|
3452
|
-
if (
|
|
3144
|
+
if (existsSync3(configPath)) {
|
|
3453
3145
|
try {
|
|
3454
3146
|
const parsed = readJsonFile(configPath);
|
|
3455
3147
|
config = {
|
|
@@ -3462,246 +3154,21 @@ async function mcpInit() {
|
|
|
3462
3154
|
} else {
|
|
3463
3155
|
config = { mcpServers: { vhk: vhkEntry } };
|
|
3464
3156
|
}
|
|
3465
|
-
|
|
3157
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
3466
3158
|
console.log(chalk15.green("\n\u2705 Cursor MCP \uC124\uC815 \uC644\uB8CC!"));
|
|
3467
3159
|
console.log(chalk15.cyan("\u{1F4C1} \uC0DD\uC131\uB41C \uD30C\uC77C:"));
|
|
3468
3160
|
console.log(` ${configPath}`);
|
|
3469
|
-
console.log(chalk15.cyan("\n\u{1F504} \uB2E4\uC74C \uB2E8\uACC4:"));
|
|
3470
|
-
console.log(" 1. Cursor\uB97C \uC7AC\uC2DC\uC791\uD558\uC138\uC694");
|
|
3471
|
-
console.log(" 2. Cursor \uCC44\uD305\uC5D0\uC11C vhk \uB3C4\uAD6C\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
|
|
3472
|
-
console.log(chalk15.gray('\n\u{1F4A1} \uC608: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uC54C\uB824\uC918" \u2192 Cursor\uAC00 vhk status \uD638\uCD9C'));
|
|
3473
|
-
}
|
|
3474
|
-
|
|
3475
|
-
// src/commands/deploy.ts
|
|
3476
|
-
import { existsSync as existsSync2 } from "fs";
|
|
3477
|
-
import chalk16 from "chalk";
|
|
3478
|
-
import inquirer7 from "inquirer";
|
|
3479
|
-
var PLATFORMS = {
|
|
3480
|
-
vercel: {
|
|
3481
|
-
name: "Vercel",
|
|
3482
|
-
detectFiles: ["vercel.json", ".vercel"],
|
|
3483
|
-
command: "vercel",
|
|
3484
|
-
commandArgs: ["--prod"],
|
|
3485
|
-
checkArgs: ["--version"],
|
|
3486
|
-
installHint: "npm i -g vercel"
|
|
3487
|
-
},
|
|
3488
|
-
netlify: {
|
|
3489
|
-
name: "Netlify",
|
|
3490
|
-
detectFiles: ["netlify.toml", ".netlify"],
|
|
3491
|
-
command: "netlify",
|
|
3492
|
-
commandArgs: ["deploy", "--prod"],
|
|
3493
|
-
checkArgs: ["--version"],
|
|
3494
|
-
installHint: "npm i -g netlify-cli"
|
|
3495
|
-
},
|
|
3496
|
-
cloudflare: {
|
|
3497
|
-
name: "Cloudflare Workers",
|
|
3498
|
-
detectFiles: ["wrangler.toml"],
|
|
3499
|
-
command: "wrangler",
|
|
3500
|
-
commandArgs: ["deploy"],
|
|
3501
|
-
checkArgs: ["--version"],
|
|
3502
|
-
installHint: "npm i -g wrangler"
|
|
3503
|
-
}
|
|
3504
|
-
};
|
|
3505
|
-
function detectPlatform() {
|
|
3506
|
-
for (const [key, config] of Object.entries(PLATFORMS)) {
|
|
3507
|
-
for (const file of config.detectFiles) {
|
|
3508
|
-
if (existsSync2(file)) return key;
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
return null;
|
|
3512
|
-
}
|
|
3513
|
-
function isCLIAvailable(cmd, checkArgs) {
|
|
3514
|
-
return safeExecFile(cmd, checkArgs).ok;
|
|
3515
|
-
}
|
|
3516
|
-
async function deploy() {
|
|
3517
|
-
console.log(chalk16.bold("\n\u{1F680} " + t("deploy.title")));
|
|
3518
|
-
console.log(chalk16.gray("\u2500".repeat(40)));
|
|
3519
|
-
let platform = detectPlatform();
|
|
3520
|
-
if (platform) {
|
|
3521
|
-
console.log(chalk16.cyan(`
|
|
3522
|
-
\u{1F50D} \uAC10\uC9C0\uB41C \uD50C\uB7AB\uD3FC: ${PLATFORMS[platform].name}`));
|
|
3523
|
-
} else {
|
|
3524
|
-
const { selected } = await inquirer7.prompt([
|
|
3525
|
-
{
|
|
3526
|
-
type: "list",
|
|
3527
|
-
name: "selected",
|
|
3528
|
-
message: t("deploy.selectPlatform"),
|
|
3529
|
-
choices: [
|
|
3530
|
-
{ name: "\u25B2 Vercel", value: "vercel" },
|
|
3531
|
-
{ name: "\u25C6 Netlify", value: "netlify" },
|
|
3532
|
-
{ name: "\u2601 Cloudflare Workers", value: "cloudflare" }
|
|
3533
|
-
]
|
|
3534
|
-
}
|
|
3535
|
-
]);
|
|
3536
|
-
platform = selected;
|
|
3537
|
-
}
|
|
3538
|
-
const config = PLATFORMS[platform];
|
|
3539
|
-
if (!isCLIAvailable(config.command, config.checkArgs)) {
|
|
3540
|
-
console.log(chalk16.red(`
|
|
3541
|
-
\u274C ${config.name} CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
|
|
3542
|
-
console.log(chalk16.yellow(` \u2192 ${config.installHint}`));
|
|
3543
|
-
return;
|
|
3544
|
-
}
|
|
3545
|
-
const { confirm } = await inquirer7.prompt([
|
|
3546
|
-
{
|
|
3547
|
-
type: "confirm",
|
|
3548
|
-
name: "confirm",
|
|
3549
|
-
message: `${config.name}\uC5D0 \uD504\uB85C\uB355\uC158 \uBC30\uD3EC\uD560\uAE4C\uC694?`,
|
|
3550
|
-
default: true
|
|
3551
|
-
}
|
|
3552
|
-
]);
|
|
3553
|
-
if (!confirm) {
|
|
3554
|
-
console.log(chalk16.gray("\uCDE8\uC18C\uB428"));
|
|
3555
|
-
return;
|
|
3556
|
-
}
|
|
3557
|
-
console.log(chalk16.cyan(`
|
|
3558
|
-
${t("deploy.deploying")}
|
|
3559
|
-
`));
|
|
3560
|
-
const result = safeExecFileStream(config.command, config.commandArgs);
|
|
3561
|
-
if (result.ok) {
|
|
3562
|
-
console.log(chalk16.green(`
|
|
3563
|
-
\u2705 ${t("deploy.success")}`));
|
|
3564
|
-
printNextStep({
|
|
3565
|
-
message: "\uBC30\uD3EC \uC644\uB8CC! \uC0AC\uC774\uD2B8\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
3566
|
-
command: "vhk status",
|
|
3567
|
-
cursorHint: "\uC0C1\uD0DC \uD655\uC778\uD574\uC918"
|
|
3568
|
-
});
|
|
3569
|
-
} else {
|
|
3570
|
-
console.log(chalk16.red(`
|
|
3571
|
-
\u274C ${t("deploy.failed")}`));
|
|
3572
|
-
console.log(chalk16.red(result.err));
|
|
3573
|
-
}
|
|
3574
|
-
}
|
|
3575
|
-
|
|
3576
|
-
// src/commands/publish.ts
|
|
3577
|
-
import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
3578
|
-
import chalk17 from "chalk";
|
|
3579
|
-
import inquirer8 from "inquirer";
|
|
3580
|
-
import ora2 from "ora";
|
|
3581
|
-
function bumpVersion(current, type) {
|
|
3582
|
-
const [major, minor, patch] = current.split(".").map((n) => parseInt(n, 10) || 0);
|
|
3583
|
-
switch (type) {
|
|
3584
|
-
case "major":
|
|
3585
|
-
return `${major + 1}.0.0`;
|
|
3586
|
-
case "minor":
|
|
3587
|
-
return `${major}.${minor + 1}.0`;
|
|
3588
|
-
case "patch":
|
|
3589
|
-
return `${major}.${minor}.${patch + 1}`;
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
async function publish() {
|
|
3593
|
-
console.log(chalk17.bold("\n\u{1F4E6} " + t("publish.title")));
|
|
3594
|
-
console.log(chalk17.gray("\u2500".repeat(40)));
|
|
3595
|
-
if (!existsSync3("package.json")) {
|
|
3596
|
-
console.log(chalk17.red("\u274C package.json\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
3597
|
-
return;
|
|
3598
|
-
}
|
|
3599
|
-
let pkg;
|
|
3600
|
-
try {
|
|
3601
|
-
pkg = readJsonFile("package.json");
|
|
3602
|
-
} catch {
|
|
3603
|
-
console.log(chalk17.red("\u274C package.json \uD30C\uC2F1 \uC2E4\uD328"));
|
|
3604
|
-
return;
|
|
3605
|
-
}
|
|
3606
|
-
const currentVersion = pkg.version || "0.0.0";
|
|
3607
|
-
console.log(chalk17.cyan(`
|
|
3608
|
-
\u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${currentVersion}`));
|
|
3609
|
-
const { bumpType } = await inquirer8.prompt([
|
|
3610
|
-
{
|
|
3611
|
-
type: "list",
|
|
3612
|
-
name: "bumpType",
|
|
3613
|
-
message: t("publish.selectBump"),
|
|
3614
|
-
choices: [
|
|
3615
|
-
{ name: `\u{1F527} patch (${bumpVersion(currentVersion, "patch")}) \u2014 \uBC84\uADF8 \uC218\uC815`, value: "patch" },
|
|
3616
|
-
{ name: `\u2728 minor (${bumpVersion(currentVersion, "minor")}) \u2014 \uC0C8 \uAE30\uB2A5`, value: "minor" },
|
|
3617
|
-
{ name: `\u{1F4A5} major (${bumpVersion(currentVersion, "major")}) \u2014 \uD638\uD658\uC131 \uBCC0\uACBD`, value: "major" }
|
|
3618
|
-
]
|
|
3619
|
-
}
|
|
3620
|
-
]);
|
|
3621
|
-
const newVersion = bumpVersion(currentVersion, bumpType);
|
|
3622
|
-
console.log(chalk17.cyan(`
|
|
3623
|
-
\u{1F195} \uC0C8 \uBC84\uC804: v${newVersion}`));
|
|
3624
|
-
pkg.version = newVersion;
|
|
3625
|
-
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
3626
|
-
console.log(chalk17.green("\u2705 package.json \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8"));
|
|
3627
|
-
const buildSpinner = ora2(t("publish.building")).start();
|
|
3628
|
-
const buildResult = safeExecFile("pnpm", ["build"]);
|
|
3629
|
-
if (!buildResult.ok) {
|
|
3630
|
-
buildSpinner.fail(t("publish.buildFailed"));
|
|
3631
|
-
console.log(chalk17.red(buildResult.err.slice(0, 500)));
|
|
3632
|
-
pkg.version = currentVersion;
|
|
3633
|
-
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
3634
|
-
return;
|
|
3635
|
-
}
|
|
3636
|
-
buildSpinner.succeed(t("publish.buildSuccess"));
|
|
3637
|
-
const testSpinner = ora2(t("publish.testing")).start();
|
|
3638
|
-
const testResult = safeExecFile("pnpm", ["test", "--run"]);
|
|
3639
|
-
if (!testResult.ok) {
|
|
3640
|
-
testSpinner.fail(t("publish.testFailed"));
|
|
3641
|
-
console.log(chalk17.red(testResult.err.slice(0, 500)));
|
|
3642
|
-
pkg.version = currentVersion;
|
|
3643
|
-
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
3644
|
-
return;
|
|
3645
|
-
}
|
|
3646
|
-
testSpinner.succeed(t("publish.testSuccess"));
|
|
3647
|
-
const { confirm } = await inquirer8.prompt([
|
|
3648
|
-
{
|
|
3649
|
-
type: "confirm",
|
|
3650
|
-
name: "confirm",
|
|
3651
|
-
message: `v${newVersion}\uC744 npm\uC5D0 \uBC30\uD3EC\uD560\uAE4C\uC694?`,
|
|
3652
|
-
default: true
|
|
3653
|
-
}
|
|
3654
|
-
]);
|
|
3655
|
-
if (!confirm) {
|
|
3656
|
-
pkg.version = currentVersion;
|
|
3657
|
-
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
3658
|
-
console.log(chalk17.gray("\uCDE8\uC18C\uB428. \uBC84\uC804\uC774 \uC6D0\uB798\uB300\uB85C \uBCF5\uAD6C\uB429\uB2C8\uB2E4."));
|
|
3659
|
-
return;
|
|
3660
|
-
}
|
|
3661
|
-
console.log(chalk17.cyan(`
|
|
3662
|
-
\u{1F4E4} ${t("publish.publishing")}`));
|
|
3663
|
-
console.log(chalk17.gray(" 2FA \uD65C\uC131\uD654 \uC2DC: OTP 6\uC790\uB9AC \uC785\uB825 \uB610\uB294 \uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D URL \uD074\uB9AD (Windows Hello / PIN \uC9C0\uC6D0)"));
|
|
3664
|
-
const pubResult = safeExecFileStream("npm", ["publish", "--access", "public"]);
|
|
3665
|
-
if (!pubResult.ok) {
|
|
3666
|
-
console.log(chalk17.red(`
|
|
3667
|
-
\u2716 ${t("publish.publishFailed")}`));
|
|
3668
|
-
console.log(chalk17.red(pubResult.err.slice(0, 500)));
|
|
3669
|
-
pkg.version = currentVersion;
|
|
3670
|
-
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
3671
|
-
console.log(chalk17.gray(`\u{1F4E6} package.json \uBC84\uC804\uC744 v${currentVersion}\uB85C \uBCF5\uAD6C\uD588\uC2B5\uB2C8\uB2E4.`));
|
|
3672
|
-
return;
|
|
3673
|
-
}
|
|
3674
|
-
console.log(chalk17.green(`
|
|
3675
|
-
\u2714 ${t("publish.publishSuccess")}`));
|
|
3676
|
-
const addResult = safeExecFile("git", ["add", "package.json"]);
|
|
3677
|
-
if (addResult.ok) {
|
|
3678
|
-
safeExecFile("git", ["commit", "-m", `chore: release v${newVersion}`]);
|
|
3679
|
-
const tagResult = safeExecFile("git", ["tag", `v${newVersion}`]);
|
|
3680
|
-
if (tagResult.ok) {
|
|
3681
|
-
const pushResult = safeExecFile("git", ["push"]);
|
|
3682
|
-
const pushTagsResult = safeExecFile("git", ["push", "--tags"]);
|
|
3683
|
-
if (pushResult.ok && pushTagsResult.ok) {
|
|
3684
|
-
console.log(chalk17.green(`
|
|
3685
|
-
\u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131 + push \uC644\uB8CC`));
|
|
3686
|
-
} else {
|
|
3687
|
-
console.log(chalk17.yellow(`
|
|
3688
|
-
\u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131\uB428 (push\uB294 \uC218\uB3D9\uC73C\uB85C)`));
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
|
-
}
|
|
3692
|
-
console.log(chalk17.green.bold(`
|
|
3693
|
-
\u{1F389} v${newVersion} \uBC30\uD3EC \uC644\uB8CC!`));
|
|
3694
3161
|
printNextStep({
|
|
3695
|
-
message: "
|
|
3696
|
-
command: "vhk
|
|
3697
|
-
cursorHint: "
|
|
3162
|
+
message: t("mcp.nextMessage"),
|
|
3163
|
+
command: "vhk mcp",
|
|
3164
|
+
cursorHint: t("mcp.nextCursor")
|
|
3698
3165
|
});
|
|
3699
3166
|
}
|
|
3700
3167
|
|
|
3701
3168
|
// src/commands/design.ts
|
|
3702
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
3703
|
-
import
|
|
3704
|
-
import
|
|
3169
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3170
|
+
import chalk16 from "chalk";
|
|
3171
|
+
import inquirer7 from "inquirer";
|
|
3705
3172
|
var PALETTES = [
|
|
3706
3173
|
{
|
|
3707
3174
|
name: "Minimal",
|
|
@@ -3774,9 +3241,9 @@ export default vhkColors
|
|
|
3774
3241
|
`;
|
|
3775
3242
|
}
|
|
3776
3243
|
async function design() {
|
|
3777
|
-
console.log(
|
|
3778
|
-
console.log(
|
|
3779
|
-
const { paletteIndex } = await
|
|
3244
|
+
console.log(chalk16.bold("\n\u{1F3A8} " + t("design.title")));
|
|
3245
|
+
console.log(chalk16.gray("\u2500".repeat(40)));
|
|
3246
|
+
const { paletteIndex } = await inquirer7.prompt([
|
|
3780
3247
|
{
|
|
3781
3248
|
type: "list",
|
|
3782
3249
|
name: "paletteIndex",
|
|
@@ -3788,32 +3255,32 @@ async function design() {
|
|
|
3788
3255
|
}
|
|
3789
3256
|
]);
|
|
3790
3257
|
const palette = PALETTES[paletteIndex];
|
|
3791
|
-
console.log(
|
|
3258
|
+
console.log(chalk16.cyan(`
|
|
3792
3259
|
\u{1F3A8} \uC120\uD0DD\uB41C \uD314\uB808\uD2B8: ${palette.name}`));
|
|
3793
3260
|
const targetPath = hasTailwind() ? "src/styles/vhk-colors.ts" : "src/styles/tokens.css";
|
|
3794
3261
|
const content = hasTailwind() ? generateTailwindExtend(palette) : generateCSSTokens(palette);
|
|
3795
3262
|
if (existsSync4(targetPath)) {
|
|
3796
|
-
const { overwrite } = await
|
|
3263
|
+
const { overwrite } = await inquirer7.prompt([{
|
|
3797
3264
|
type: "confirm",
|
|
3798
3265
|
name: "overwrite",
|
|
3799
3266
|
message: `${targetPath} \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?`,
|
|
3800
3267
|
default: false
|
|
3801
3268
|
}]);
|
|
3802
3269
|
if (!overwrite) {
|
|
3803
|
-
console.log(
|
|
3270
|
+
console.log(chalk16.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
|
|
3804
3271
|
return;
|
|
3805
3272
|
}
|
|
3806
3273
|
}
|
|
3807
|
-
|
|
3274
|
+
mkdirSync3("src/styles", { recursive: true });
|
|
3808
3275
|
writeFileSync3(targetPath, content, "utf-8");
|
|
3809
3276
|
if (hasTailwind()) {
|
|
3810
|
-
console.log(
|
|
3811
|
-
console.log(
|
|
3277
|
+
console.log(chalk16.green("\n\u2705 src/styles/vhk-colors.ts \uC0DD\uC131"));
|
|
3278
|
+
console.log(chalk16.gray(" tailwind.config\uC758 extend.colors\uC5D0 import \uD574\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."));
|
|
3812
3279
|
} else {
|
|
3813
|
-
console.log(
|
|
3814
|
-
console.log(
|
|
3280
|
+
console.log(chalk16.green("\n\u2705 src/styles/tokens.css \uC0DD\uC131"));
|
|
3281
|
+
console.log(chalk16.gray(" HTML\uC5D0 <link>\uB85C \uCD94\uAC00\uD558\uAC70\uB098 CSS\uC5D0\uC11C @import \uD558\uC138\uC694."));
|
|
3815
3282
|
}
|
|
3816
|
-
console.log(
|
|
3283
|
+
console.log(chalk16.bold("\n\u{1F308} \uCEEC\uB7EC \uBBF8\uB9AC\uBCF4\uAE30:"));
|
|
3817
3284
|
for (const [key, value] of Object.entries(palette.colors)) {
|
|
3818
3285
|
console.log(` ${key.padEnd(12)} ${value}`);
|
|
3819
3286
|
}
|
|
@@ -3828,9 +3295,9 @@ async function designPalette() {
|
|
|
3828
3295
|
}
|
|
3829
3296
|
|
|
3830
3297
|
// src/commands/theme.ts
|
|
3831
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
3832
|
-
import
|
|
3833
|
-
import
|
|
3298
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
3299
|
+
import chalk17 from "chalk";
|
|
3300
|
+
import inquirer8 from "inquirer";
|
|
3834
3301
|
function generateDarkCSS() {
|
|
3835
3302
|
return `/* vhk theme \u2014 \uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC CSS \uBCC0\uC218 */
|
|
3836
3303
|
|
|
@@ -3886,13 +3353,13 @@ export function initTheme(): void {
|
|
|
3886
3353
|
`;
|
|
3887
3354
|
}
|
|
3888
3355
|
async function theme() {
|
|
3889
|
-
console.log(
|
|
3890
|
-
console.log(
|
|
3356
|
+
console.log(chalk17.bold("\n\u{1F319} " + t("theme.title")));
|
|
3357
|
+
console.log(chalk17.gray("\u2500".repeat(40)));
|
|
3891
3358
|
const cssPath = "src/styles/theme.css";
|
|
3892
3359
|
const togglePath = "src/lib/theme-toggle.ts";
|
|
3893
3360
|
const conflicts = [cssPath, togglePath].filter((p) => existsSync5(p));
|
|
3894
3361
|
if (conflicts.length > 0) {
|
|
3895
|
-
const { overwrite } = await
|
|
3362
|
+
const { overwrite } = await inquirer8.prompt([{
|
|
3896
3363
|
type: "confirm",
|
|
3897
3364
|
name: "overwrite",
|
|
3898
3365
|
message: `\uB2E4\uC74C \uD30C\uC77C\uC774 \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?
|
|
@@ -3900,21 +3367,21 @@ async function theme() {
|
|
|
3900
3367
|
default: false
|
|
3901
3368
|
}]);
|
|
3902
3369
|
if (!overwrite) {
|
|
3903
|
-
console.log(
|
|
3370
|
+
console.log(chalk17.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
|
|
3904
3371
|
return;
|
|
3905
3372
|
}
|
|
3906
3373
|
}
|
|
3907
|
-
|
|
3908
|
-
|
|
3374
|
+
mkdirSync4("src/styles", { recursive: true });
|
|
3375
|
+
mkdirSync4("src/lib", { recursive: true });
|
|
3909
3376
|
writeFileSync4(cssPath, generateDarkCSS(), "utf-8");
|
|
3910
|
-
console.log(
|
|
3377
|
+
console.log(chalk17.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (\uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC)"));
|
|
3911
3378
|
writeFileSync4(togglePath, generateToggleUtil(), "utf-8");
|
|
3912
|
-
console.log(
|
|
3913
|
-
console.log(
|
|
3914
|
-
console.log(
|
|
3915
|
-
console.log(
|
|
3916
|
-
console.log(
|
|
3917
|
-
console.log(
|
|
3379
|
+
console.log(chalk17.green("\u2705 src/lib/theme-toggle.ts \uC0DD\uC131 (\uD1A0\uAE00 \uC720\uD2F8\uB9AC\uD2F0)"));
|
|
3380
|
+
console.log(chalk17.bold("\n\u{1F4D6} \uC0AC\uC6A9\uBC95:"));
|
|
3381
|
+
console.log(chalk17.gray(" 1. theme.css\uB97C \uAE00\uB85C\uBC8C \uC2A4\uD0C0\uC77C\uC5D0 \uCD94\uAC00"));
|
|
3382
|
+
console.log(chalk17.gray(' 2. import { initTheme, toggleTheme } from "./lib/theme-toggle"'));
|
|
3383
|
+
console.log(chalk17.gray(" 3. \uC571 \uC9C4\uC785\uC810\uC5D0\uC11C initTheme() \uD638\uCD9C"));
|
|
3384
|
+
console.log(chalk17.gray(" 4. \uD1A0\uAE00 \uBC84\uD2BC\uC5D0\uC11C toggleTheme() \uD638\uCD9C"));
|
|
3918
3385
|
printNextStep({
|
|
3919
3386
|
message: "\uD14C\uB9C8 \uC124\uC815 \uC644\uB8CC!",
|
|
3920
3387
|
command: "vhk ref list",
|
|
@@ -3923,8 +3390,8 @@ async function theme() {
|
|
|
3923
3390
|
}
|
|
3924
3391
|
|
|
3925
3392
|
// src/commands/ref.ts
|
|
3926
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
3927
|
-
import
|
|
3393
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
3394
|
+
import chalk18 from "chalk";
|
|
3928
3395
|
var REFS_PATH = ".vhk/refs.json";
|
|
3929
3396
|
function loadRefs() {
|
|
3930
3397
|
if (!existsSync6(REFS_PATH)) return [];
|
|
@@ -3936,28 +3403,28 @@ function loadRefs() {
|
|
|
3936
3403
|
}
|
|
3937
3404
|
}
|
|
3938
3405
|
function saveRefs(refs) {
|
|
3939
|
-
|
|
3406
|
+
mkdirSync5(".vhk", { recursive: true });
|
|
3940
3407
|
writeFileSync5(REFS_PATH, JSON.stringify(refs, null, 2) + "\n", "utf-8");
|
|
3941
3408
|
}
|
|
3942
3409
|
async function refAdd(url, memo = "") {
|
|
3943
|
-
console.log(
|
|
3944
|
-
console.log(
|
|
3410
|
+
console.log(chalk18.bold("\n\u{1F517} " + t("ref.addTitle")));
|
|
3411
|
+
console.log(chalk18.gray("\u2500".repeat(40)));
|
|
3945
3412
|
if (!url) {
|
|
3946
|
-
console.log(
|
|
3947
|
-
console.log(
|
|
3413
|
+
console.log(chalk18.red("\u274C URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
|
|
3414
|
+
console.log(chalk18.gray(' \uC608: vhk ref add https://example.com --memo "\uCC38\uACE0 \uC0AC\uC774\uD2B8"'));
|
|
3948
3415
|
return;
|
|
3949
3416
|
}
|
|
3950
3417
|
const refs = loadRefs();
|
|
3951
3418
|
if (refs.some((r) => r.url === url)) {
|
|
3952
|
-
console.log(
|
|
3419
|
+
console.log(chalk18.yellow("\u26A0\uFE0F \uC774\uBBF8 \uC800\uC7A5\uB41C URL\uC785\uB2C8\uB2E4."));
|
|
3953
3420
|
return;
|
|
3954
3421
|
}
|
|
3955
3422
|
refs.push({ url, memo, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3956
3423
|
saveRefs(refs);
|
|
3957
|
-
console.log(
|
|
3424
|
+
console.log(chalk18.green(`
|
|
3958
3425
|
\u2705 \uB808\uD37C\uB7F0\uC2A4 \uCD94\uAC00\uB428 (#${refs.length})`));
|
|
3959
|
-
console.log(
|
|
3960
|
-
if (memo) console.log(
|
|
3426
|
+
console.log(chalk18.cyan(` ${url}`));
|
|
3427
|
+
if (memo) console.log(chalk18.gray(` \u{1F4DD} ${memo}`));
|
|
3961
3428
|
printNextStep({
|
|
3962
3429
|
message: "\uB808\uD37C\uB7F0\uC2A4 \uC800\uC7A5 \uC644\uB8CC!",
|
|
3963
3430
|
command: "vhk ref list",
|
|
@@ -3965,22 +3432,22 @@ async function refAdd(url, memo = "") {
|
|
|
3965
3432
|
});
|
|
3966
3433
|
}
|
|
3967
3434
|
async function refList() {
|
|
3968
|
-
console.log(
|
|
3969
|
-
console.log(
|
|
3435
|
+
console.log(chalk18.bold("\n\u{1F4DA} " + t("ref.listTitle")));
|
|
3436
|
+
console.log(chalk18.gray("\u2500".repeat(40)));
|
|
3970
3437
|
const refs = loadRefs();
|
|
3971
3438
|
if (refs.length === 0) {
|
|
3972
|
-
console.log(
|
|
3973
|
-
console.log(
|
|
3439
|
+
console.log(chalk18.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uB808\uD37C\uB7F0\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
3440
|
+
console.log(chalk18.gray(' vhk ref add <url> --memo "\uBA54\uBAA8"\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
|
|
3974
3441
|
return;
|
|
3975
3442
|
}
|
|
3976
|
-
console.log(
|
|
3443
|
+
console.log(chalk18.cyan(`
|
|
3977
3444
|
\uCD1D ${refs.length}\uAC1C\uC758 \uB808\uD37C\uB7F0\uC2A4:
|
|
3978
3445
|
`));
|
|
3979
3446
|
refs.forEach((ref, index) => {
|
|
3980
3447
|
const date = new Date(ref.addedAt).toLocaleDateString("ko-KR");
|
|
3981
|
-
console.log(
|
|
3982
|
-
if (ref.memo) console.log(
|
|
3983
|
-
console.log(
|
|
3448
|
+
console.log(chalk18.white(` [${index + 1}] ${ref.url}`));
|
|
3449
|
+
if (ref.memo) console.log(chalk18.gray(` \u{1F4DD} ${ref.memo}`));
|
|
3450
|
+
console.log(chalk18.gray(` \u{1F4C5} ${date}`));
|
|
3984
3451
|
console.log("");
|
|
3985
3452
|
});
|
|
3986
3453
|
}
|
|
@@ -3988,7 +3455,7 @@ async function refOpen(indexStr) {
|
|
|
3988
3455
|
const refs = loadRefs();
|
|
3989
3456
|
const idx = parseInt(indexStr, 10) - 1;
|
|
3990
3457
|
if (Number.isNaN(idx) || idx < 0 || idx >= refs.length) {
|
|
3991
|
-
console.log(
|
|
3458
|
+
console.log(chalk18.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${refs.length || 0})`));
|
|
3992
3459
|
return;
|
|
3993
3460
|
}
|
|
3994
3461
|
const ref = refs[idx];
|
|
@@ -3996,34 +3463,34 @@ async function refOpen(indexStr) {
|
|
|
3996
3463
|
try {
|
|
3997
3464
|
parsed = new URL(ref.url);
|
|
3998
3465
|
} catch {
|
|
3999
|
-
console.log(
|
|
3466
|
+
console.log(chalk18.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 URL: ${ref.url}`));
|
|
4000
3467
|
return;
|
|
4001
3468
|
}
|
|
4002
3469
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
4003
|
-
console.log(
|
|
3470
|
+
console.log(chalk18.red(`\u274C http(s) URL\uB9CC \uC5F4 \uC218 \uC788\uC2B5\uB2C8\uB2E4 (${parsed.protocol})`));
|
|
4004
3471
|
return;
|
|
4005
3472
|
}
|
|
4006
|
-
console.log(
|
|
3473
|
+
console.log(chalk18.cyan(`
|
|
4007
3474
|
\u{1F310} \uC5F4\uAE30: ${ref.url}`));
|
|
4008
3475
|
let result;
|
|
4009
3476
|
if (process.platform === "darwin") {
|
|
4010
3477
|
result = safeExecFile("open", [ref.url]);
|
|
4011
3478
|
} else if (process.platform === "win32") {
|
|
4012
|
-
result = safeExecFile("
|
|
3479
|
+
result = safeExecFile("rundll32.exe", ["url.dll,FileProtocolHandler", ref.url]);
|
|
4013
3480
|
} else {
|
|
4014
3481
|
result = safeExecFile("xdg-open", [ref.url]);
|
|
4015
3482
|
}
|
|
4016
3483
|
if (result.ok) {
|
|
4017
|
-
console.log(
|
|
3484
|
+
console.log(chalk18.green("\u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
4018
3485
|
} else {
|
|
4019
|
-
console.log(
|
|
3486
|
+
console.log(chalk18.yellow("\u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. URL\uC744 \uC9C1\uC811 \uBC29\uBB38\uD574\uC8FC\uC138\uC694."));
|
|
4020
3487
|
}
|
|
4021
3488
|
}
|
|
4022
3489
|
|
|
4023
3490
|
// src/commands/harness.ts
|
|
4024
3491
|
import { existsSync as existsSync7 } from "fs";
|
|
4025
|
-
import
|
|
4026
|
-
import
|
|
3492
|
+
import chalk19 from "chalk";
|
|
3493
|
+
import ora2 from "ora";
|
|
4027
3494
|
function detectPM() {
|
|
4028
3495
|
if (existsSync7("pnpm-lock.yaml")) return "pnpm";
|
|
4029
3496
|
if (existsSync7("yarn.lock")) return "yarn";
|
|
@@ -4063,30 +3530,30 @@ function detectChecks() {
|
|
|
4063
3530
|
return checks;
|
|
4064
3531
|
}
|
|
4065
3532
|
async function harness() {
|
|
4066
|
-
console.log(
|
|
4067
|
-
console.log(
|
|
3533
|
+
console.log(chalk19.bold("\n\u{1F527} " + t("harness.title")));
|
|
3534
|
+
console.log(chalk19.gray("\u2500".repeat(40)));
|
|
4068
3535
|
const checks = detectChecks();
|
|
4069
3536
|
if (checks.length === 0) {
|
|
4070
|
-
console.log(
|
|
4071
|
-
console.log(
|
|
3537
|
+
console.log(chalk19.yellow("\n\u26A0\uFE0F \uC2E4\uD589\uD560 \uC218 \uC788\uB294 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
3538
|
+
console.log(chalk19.gray(" package.json\uC5D0 lint, test, build \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD94\uAC00\uD574\uC8FC\uC138\uC694."));
|
|
4072
3539
|
return;
|
|
4073
3540
|
}
|
|
4074
|
-
console.log(
|
|
3541
|
+
console.log(chalk19.cyan(`
|
|
4075
3542
|
\u{1F3C3} ${checks.length}\uAC1C \uC810\uAC80 \uC2DC\uC791:
|
|
4076
3543
|
`));
|
|
4077
3544
|
const results = [];
|
|
4078
3545
|
for (const check2 of checks) {
|
|
4079
3546
|
const display = `${check2.bin} ${check2.args.join(" ")}`;
|
|
4080
|
-
const spinner =
|
|
4081
|
-
const
|
|
3547
|
+
const spinner = ora2(`${check2.name} \uC2E4\uD589 \uC911...`).start();
|
|
3548
|
+
const start2 = Date.now();
|
|
4082
3549
|
const result = safeExecFile(check2.bin, check2.args);
|
|
4083
|
-
const duration = Date.now() -
|
|
3550
|
+
const duration = Date.now() - start2;
|
|
4084
3551
|
const sec = (duration / 1e3).toFixed(1);
|
|
4085
3552
|
if (result.ok) {
|
|
4086
|
-
spinner.succeed(`${check2.name} ${
|
|
3553
|
+
spinner.succeed(`${check2.name} ${chalk19.gray(`(${sec}s)`)}`);
|
|
4087
3554
|
results.push({ name: check2.name, command: display, passed: true, duration });
|
|
4088
3555
|
} else {
|
|
4089
|
-
spinner.fail(`${check2.name} ${
|
|
3556
|
+
spinner.fail(`${check2.name} ${chalk19.gray(`(${sec}s)`)}`);
|
|
4090
3557
|
results.push({
|
|
4091
3558
|
name: check2.name,
|
|
4092
3559
|
command: display,
|
|
@@ -4096,22 +3563,22 @@ async function harness() {
|
|
|
4096
3563
|
});
|
|
4097
3564
|
}
|
|
4098
3565
|
}
|
|
4099
|
-
console.log(
|
|
4100
|
-
console.log(
|
|
3566
|
+
console.log(chalk19.bold("\n\u{1F4CA} \uD1B5\uD569 \uB9AC\uD3EC\uD2B8:"));
|
|
3567
|
+
console.log(chalk19.gray("\u2500".repeat(40)));
|
|
4101
3568
|
for (const r of results) {
|
|
4102
|
-
const icon = r.passed ?
|
|
3569
|
+
const icon = r.passed ? chalk19.green("\u2705") : chalk19.red("\u274C");
|
|
4103
3570
|
const sec = (r.duration / 1e3).toFixed(1);
|
|
4104
|
-
console.log(` ${icon} ${r.name.padEnd(15)} ${
|
|
3571
|
+
console.log(` ${icon} ${r.name.padEnd(15)} ${chalk19.gray(`${sec}s`)}`);
|
|
4105
3572
|
}
|
|
4106
3573
|
const passed = results.filter((r) => r.passed).length;
|
|
4107
3574
|
const all = passed === results.length;
|
|
4108
|
-
console.log(
|
|
3575
|
+
console.log(chalk19.gray("\u2500".repeat(40)));
|
|
4109
3576
|
if (all) {
|
|
4110
|
-
console.log(
|
|
3577
|
+
console.log(chalk19.green.bold(`
|
|
4111
3578
|
\u{1F389} \uC804\uCCB4 \uD1B5\uACFC! (${passed}/${results.length})`));
|
|
4112
3579
|
} else {
|
|
4113
3580
|
console.log(
|
|
4114
|
-
|
|
3581
|
+
chalk19.red.bold(`
|
|
4115
3582
|
\u26A0\uFE0F ${results.length - passed}\uAC1C \uC2E4\uD328 (${passed}/${results.length} \uD1B5\uACFC)`)
|
|
4116
3583
|
);
|
|
4117
3584
|
}
|
|
@@ -4122,127 +3589,37 @@ async function harness() {
|
|
|
4122
3589
|
});
|
|
4123
3590
|
}
|
|
4124
3591
|
|
|
4125
|
-
// src/commands/audit.ts
|
|
4126
|
-
import { existsSync as existsSync8 } from "fs";
|
|
4127
|
-
import chalk22 from "chalk";
|
|
4128
|
-
import inquirer11 from "inquirer";
|
|
4129
|
-
import ora4 from "ora";
|
|
4130
|
-
function detectCurrentPM() {
|
|
4131
|
-
if (existsSync8("pnpm-lock.yaml")) return "pnpm";
|
|
4132
|
-
if (existsSync8("yarn.lock")) return "yarn";
|
|
4133
|
-
return "npm";
|
|
4134
|
-
}
|
|
4135
|
-
function parseAuditOutput(output, pm) {
|
|
4136
|
-
const empty = { critical: 0, high: 0, moderate: 0, low: 0, total: 0 };
|
|
4137
|
-
if (!output) return empty;
|
|
4138
|
-
try {
|
|
4139
|
-
const json = JSON.parse(output);
|
|
4140
|
-
const meta = json.metadata?.vulnerabilities;
|
|
4141
|
-
if (meta) {
|
|
4142
|
-
const summary = {
|
|
4143
|
-
critical: meta.critical ?? 0,
|
|
4144
|
-
high: meta.high ?? 0,
|
|
4145
|
-
moderate: meta.moderate ?? 0,
|
|
4146
|
-
low: meta.low ?? 0,
|
|
4147
|
-
total: meta.total ?? 0
|
|
4148
|
-
};
|
|
4149
|
-
if (!summary.total) {
|
|
4150
|
-
summary.total = summary.critical + summary.high + summary.moderate + summary.low;
|
|
4151
|
-
}
|
|
4152
|
-
return summary;
|
|
4153
|
-
}
|
|
4154
|
-
void pm;
|
|
4155
|
-
return empty;
|
|
4156
|
-
} catch {
|
|
4157
|
-
return empty;
|
|
4158
|
-
}
|
|
4159
|
-
}
|
|
4160
|
-
function runAuditJson(pm) {
|
|
4161
|
-
const result = safeExecFile(pm, ["audit", "--json"]);
|
|
4162
|
-
return result.out;
|
|
4163
|
-
}
|
|
4164
|
-
function runAuditFix(pm) {
|
|
4165
|
-
if (pm !== "npm") {
|
|
4166
|
-
return { ok: false, err: `${pm}\uC740 \uC790\uB3D9 fix\uB97C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. npm \uD658\uACBD\uC5D0\uC11C\uB9CC \uB3D9\uC791\uD569\uB2C8\uB2E4.` };
|
|
4167
|
-
}
|
|
4168
|
-
const result = safeExecFile("npm", ["audit", "fix"]);
|
|
4169
|
-
return result.ok ? { ok: true } : { ok: false, err: result.err };
|
|
4170
|
-
}
|
|
4171
|
-
async function audit(autoFix = false) {
|
|
4172
|
-
console.log(chalk22.bold("\n\u{1F6E1}\uFE0F " + t("audit.title")));
|
|
4173
|
-
console.log(chalk22.gray("\u2500".repeat(40)));
|
|
4174
|
-
const pm = detectCurrentPM();
|
|
4175
|
-
console.log(chalk22.cyan(`\u{1F4E6} \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${pm}`));
|
|
4176
|
-
const spinner = ora4("\uBCF4\uC548 \uAC10\uC0AC \uC2E4\uD589 \uC911...").start();
|
|
4177
|
-
const output = runAuditJson(pm);
|
|
4178
|
-
spinner.stop();
|
|
4179
|
-
const summary = parseAuditOutput(output, pm);
|
|
4180
|
-
if (summary.total === 0) {
|
|
4181
|
-
console.log(chalk22.green.bold("\n\u{1F389} \uCDE8\uC57D\uC810\uC774 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4!"));
|
|
4182
|
-
return;
|
|
4183
|
-
}
|
|
4184
|
-
console.log(chalk22.bold("\n\u{1F4CA} \uCDE8\uC57D\uC810 \uC694\uC57D:"));
|
|
4185
|
-
if (summary.critical > 0) console.log(chalk22.red(` \u{1F534} Critical: ${summary.critical}`));
|
|
4186
|
-
if (summary.high > 0) console.log(chalk22.red(` \u{1F7E0} High: ${summary.high}`));
|
|
4187
|
-
if (summary.moderate > 0) console.log(chalk22.yellow(` \u{1F7E1} Moderate: ${summary.moderate}`));
|
|
4188
|
-
if (summary.low > 0) console.log(chalk22.gray(` \u26AA Low: ${summary.low}`));
|
|
4189
|
-
console.log(chalk22.bold(`
|
|
4190
|
-
\uCD1D ${summary.total}\uAC1C\uC758 \uCDE8\uC57D\uC810`));
|
|
4191
|
-
const shouldRunFix = autoFix ? true : summary.critical > 0 || summary.high > 0 ? (await inquirer11.prompt([
|
|
4192
|
-
{
|
|
4193
|
-
type: "confirm",
|
|
4194
|
-
name: "shouldFix",
|
|
4195
|
-
message: "\uC790\uB3D9 \uC218\uC815\uC744 \uC2DC\uB3C4\uD560\uAE4C\uC694? (npm audit fix)",
|
|
4196
|
-
default: true
|
|
4197
|
-
}
|
|
4198
|
-
])).shouldFix : false;
|
|
4199
|
-
if (shouldRunFix) {
|
|
4200
|
-
const fixSpinner = ora4("\uC790\uB3D9 \uC218\uC815 \uC911...").start();
|
|
4201
|
-
const result = runAuditFix(pm);
|
|
4202
|
-
if (result.ok) {
|
|
4203
|
-
fixSpinner.succeed("\uC790\uB3D9 \uC218\uC815 \uC644\uB8CC!");
|
|
4204
|
-
} else {
|
|
4205
|
-
fixSpinner.warn(result.err ?? "\uC77C\uBD80 \uCDE8\uC57D\uC810\uC740 \uC218\uB3D9 \uC218\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
4206
|
-
}
|
|
4207
|
-
}
|
|
4208
|
-
printNextStep({
|
|
4209
|
-
message: "\uBCF4\uC548 \uAC10\uC0AC \uC644\uB8CC.",
|
|
4210
|
-
command: "vhk harness",
|
|
4211
|
-
cursorHint: "\uD488\uC9C8 \uC810\uAC80\uD574\uC918"
|
|
4212
|
-
});
|
|
4213
|
-
}
|
|
4214
|
-
|
|
4215
3592
|
// src/commands/migrate.ts
|
|
4216
|
-
import { existsSync as
|
|
4217
|
-
import
|
|
4218
|
-
import
|
|
4219
|
-
import
|
|
3593
|
+
import { existsSync as existsSync8, unlinkSync, rmSync } from "fs";
|
|
3594
|
+
import chalk20 from "chalk";
|
|
3595
|
+
import inquirer9 from "inquirer";
|
|
3596
|
+
import ora3 from "ora";
|
|
4220
3597
|
var LOCK_FILES = {
|
|
4221
3598
|
npm: "package-lock.json",
|
|
4222
3599
|
yarn: "yarn.lock",
|
|
4223
3600
|
pnpm: "pnpm-lock.yaml"
|
|
4224
3601
|
};
|
|
4225
|
-
function
|
|
4226
|
-
if (
|
|
4227
|
-
if (
|
|
4228
|
-
if (
|
|
3602
|
+
function detectCurrentPM() {
|
|
3603
|
+
if (existsSync8("pnpm-lock.yaml")) return "pnpm";
|
|
3604
|
+
if (existsSync8("yarn.lock")) return "yarn";
|
|
3605
|
+
if (existsSync8("package-lock.json")) return "npm";
|
|
4229
3606
|
return null;
|
|
4230
3607
|
}
|
|
4231
|
-
function
|
|
3608
|
+
function isCLIAvailable(pm) {
|
|
4232
3609
|
return safeExecFile(pm, ["--version"]).ok;
|
|
4233
3610
|
}
|
|
4234
3611
|
async function migrate(target) {
|
|
4235
|
-
console.log(
|
|
4236
|
-
console.log(
|
|
4237
|
-
const current =
|
|
4238
|
-
console.log(
|
|
3612
|
+
console.log(chalk20.bold("\n\u{1F504} " + t("migrate.title")));
|
|
3613
|
+
console.log(chalk20.gray("\u2500".repeat(40)));
|
|
3614
|
+
const current = detectCurrentPM();
|
|
3615
|
+
console.log(chalk20.cyan(`
|
|
4239
3616
|
\uD604\uC7AC \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00"}`));
|
|
4240
3617
|
let targetPM;
|
|
4241
3618
|
if (target && ["npm", "yarn", "pnpm"].includes(target)) {
|
|
4242
3619
|
targetPM = target;
|
|
4243
3620
|
} else {
|
|
4244
3621
|
const choices = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current).map((pm) => ({ name: pm, value: pm }));
|
|
4245
|
-
const { selected } = await
|
|
3622
|
+
const { selected } = await inquirer9.prompt([
|
|
4246
3623
|
{
|
|
4247
3624
|
type: "list",
|
|
4248
3625
|
name: "selected",
|
|
@@ -4253,17 +3630,17 @@ async function migrate(target) {
|
|
|
4253
3630
|
targetPM = selected;
|
|
4254
3631
|
}
|
|
4255
3632
|
if (targetPM === current) {
|
|
4256
|
-
console.log(
|
|
3633
|
+
console.log(chalk20.yellow(`
|
|
4257
3634
|
\u26A0\uFE0F \uC774\uBBF8 ${targetPM}\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.`));
|
|
4258
3635
|
return;
|
|
4259
3636
|
}
|
|
4260
|
-
if (!
|
|
4261
|
-
console.log(
|
|
3637
|
+
if (!isCLIAvailable(targetPM)) {
|
|
3638
|
+
console.log(chalk20.red(`
|
|
4262
3639
|
\u274C ${targetPM}\uC774 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
|
|
4263
|
-
console.log(
|
|
3640
|
+
console.log(chalk20.yellow(` npm i -g ${targetPM}`));
|
|
4264
3641
|
return;
|
|
4265
3642
|
}
|
|
4266
|
-
const { confirm } = await
|
|
3643
|
+
const { confirm } = await inquirer9.prompt([
|
|
4267
3644
|
{
|
|
4268
3645
|
type: "confirm",
|
|
4269
3646
|
name: "confirm",
|
|
@@ -4272,30 +3649,30 @@ async function migrate(target) {
|
|
|
4272
3649
|
}
|
|
4273
3650
|
]);
|
|
4274
3651
|
if (!confirm) {
|
|
4275
|
-
console.log(
|
|
3652
|
+
console.log(chalk20.gray("\uCDE8\uC18C\uB428"));
|
|
4276
3653
|
return;
|
|
4277
3654
|
}
|
|
4278
|
-
const cleanup =
|
|
3655
|
+
const cleanup = ora3("\uAE30\uC874 lock \uD30C\uC77C \uC815\uB9AC \uC911...").start();
|
|
4279
3656
|
for (const lockFile of Object.values(LOCK_FILES)) {
|
|
4280
|
-
if (
|
|
3657
|
+
if (existsSync8(lockFile)) {
|
|
4281
3658
|
unlinkSync(lockFile);
|
|
4282
3659
|
}
|
|
4283
3660
|
}
|
|
4284
|
-
if (
|
|
3661
|
+
if (existsSync8("node_modules")) {
|
|
4285
3662
|
cleanup.text = "node_modules \uC0AD\uC81C \uC911...";
|
|
4286
3663
|
rmSync("node_modules", { recursive: true, force: true });
|
|
4287
3664
|
}
|
|
4288
3665
|
cleanup.succeed("\uAE30\uC874 \uD30C\uC77C \uC815\uB9AC \uC644\uB8CC");
|
|
4289
|
-
const install =
|
|
3666
|
+
const install = ora3(`${targetPM} install \uC2E4\uD589 \uC911...`).start();
|
|
4290
3667
|
const installResult = safeExecFile(targetPM, ["install"]);
|
|
4291
3668
|
if (installResult.ok) {
|
|
4292
3669
|
install.succeed(`${targetPM} install \uC644\uB8CC!`);
|
|
4293
3670
|
} else {
|
|
4294
3671
|
install.fail(`${targetPM} install \uC2E4\uD328`);
|
|
4295
|
-
console.log(
|
|
3672
|
+
console.log(chalk20.red(installResult.err.slice(0, 300)));
|
|
4296
3673
|
return;
|
|
4297
3674
|
}
|
|
4298
|
-
console.log(
|
|
3675
|
+
console.log(chalk20.green.bold(`
|
|
4299
3676
|
\u{1F389} ${current ?? "\uC774\uC804"} \u2192 ${targetPM} \uC804\uD658 \uC644\uB8CC!`));
|
|
4300
3677
|
printNextStep({
|
|
4301
3678
|
message: "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 \uC644\uB8CC!",
|
|
@@ -4305,18 +3682,17 @@ async function migrate(target) {
|
|
|
4305
3682
|
}
|
|
4306
3683
|
|
|
4307
3684
|
// src/commands/update.ts
|
|
4308
|
-
import {
|
|
4309
|
-
import {
|
|
4310
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
3685
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3686
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
4311
3687
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4312
|
-
import
|
|
4313
|
-
import
|
|
3688
|
+
import chalk21 from "chalk";
|
|
3689
|
+
import ora4 from "ora";
|
|
4314
3690
|
var PACKAGE = "@byh3071/vhk";
|
|
4315
3691
|
function getCurrentVersion() {
|
|
4316
3692
|
const dir = dirname2(fileURLToPath3(import.meta.url));
|
|
4317
|
-
for (const pkgPath of [
|
|
3693
|
+
for (const pkgPath of [join4(dir, "../package.json"), join4(dir, "../../package.json")]) {
|
|
4318
3694
|
try {
|
|
4319
|
-
if (
|
|
3695
|
+
if (existsSync9(pkgPath)) {
|
|
4320
3696
|
const pkg = readJsonFile(pkgPath);
|
|
4321
3697
|
if (pkg.version) return pkg.version;
|
|
4322
3698
|
}
|
|
@@ -4327,15 +3703,8 @@ function getCurrentVersion() {
|
|
|
4327
3703
|
return "0.0.0";
|
|
4328
3704
|
}
|
|
4329
3705
|
function getLatestVersion() {
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
encoding: "utf-8",
|
|
4333
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4334
|
-
}).toString();
|
|
4335
|
-
return out.trim();
|
|
4336
|
-
} catch {
|
|
4337
|
-
return null;
|
|
4338
|
-
}
|
|
3706
|
+
const r = safeExecFile("npm", ["view", PACKAGE, "version"]);
|
|
3707
|
+
return r.ok ? r.out : null;
|
|
4339
3708
|
}
|
|
4340
3709
|
function isUpToDate(current, latest) {
|
|
4341
3710
|
const parse = (v) => v.split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -4346,54 +3715,169 @@ function isUpToDate(current, latest) {
|
|
|
4346
3715
|
return cc >= lc;
|
|
4347
3716
|
}
|
|
4348
3717
|
async function update() {
|
|
4349
|
-
console.log(
|
|
4350
|
-
console.log(
|
|
3718
|
+
console.log(chalk21.bold("\n\u2B06\uFE0F " + t("update.title")));
|
|
3719
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
4351
3720
|
const current = getCurrentVersion();
|
|
4352
|
-
console.log(
|
|
3721
|
+
console.log(chalk21.cyan(`
|
|
4353
3722
|
\u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${current}`));
|
|
4354
|
-
const spinner =
|
|
3723
|
+
const spinner = ora4("\uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778 \uC911...").start();
|
|
4355
3724
|
const latest = getLatestVersion();
|
|
4356
3725
|
if (!latest) {
|
|
4357
3726
|
spinner.fail("\uCD5C\uC2E0 \uBC84\uC804\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
4358
|
-
console.log(
|
|
4359
|
-
console.log(
|
|
3727
|
+
console.log(chalk21.yellow(" \uB124\uD2B8\uC6CC\uD06C\uB97C \uD655\uC778\uD558\uAC70\uB098 \uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
|
|
3728
|
+
console.log(chalk21.gray(` npm update -g ${PACKAGE}`));
|
|
4360
3729
|
return;
|
|
4361
3730
|
}
|
|
4362
3731
|
spinner.stop();
|
|
4363
|
-
console.log(
|
|
3732
|
+
console.log(chalk21.cyan(`\u{1F195} \uCD5C\uC2E0 \uBC84\uC804: v${latest}`));
|
|
4364
3733
|
if (isUpToDate(current, latest)) {
|
|
4365
|
-
console.log(
|
|
3734
|
+
console.log(chalk21.green("\n\u2705 \uC774\uBBF8 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4!"));
|
|
4366
3735
|
return;
|
|
4367
3736
|
}
|
|
4368
|
-
const updateSpinner =
|
|
4369
|
-
|
|
4370
|
-
|
|
3737
|
+
const updateSpinner = ora4(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC911...`).start();
|
|
3738
|
+
const upd = safeExecFile("npm", ["update", "-g", PACKAGE]);
|
|
3739
|
+
if (upd.ok) {
|
|
4371
3740
|
updateSpinner.succeed(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`);
|
|
4372
|
-
console.log(
|
|
3741
|
+
console.log(chalk21.green.bold(`
|
|
4373
3742
|
\u{1F389} VHK CLI v${latest} \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
|
|
4374
|
-
console.log(
|
|
4375
|
-
|
|
3743
|
+
console.log(chalk21.gray(" \uBCC0\uACBD \uC0AC\uD56D\uC740 GitHub Releases\uB97C \uD655\uC778\uD558\uC138\uC694."));
|
|
3744
|
+
printNextStep({
|
|
3745
|
+
message: t("update.nextOkMessage"),
|
|
3746
|
+
command: "vhk --version",
|
|
3747
|
+
cursorHint: t("update.nextOkCursor")
|
|
3748
|
+
});
|
|
3749
|
+
} else {
|
|
4376
3750
|
updateSpinner.fail("\uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328");
|
|
4377
|
-
|
|
4378
|
-
console.log(
|
|
4379
|
-
console.log(
|
|
4380
|
-
|
|
3751
|
+
console.log(chalk21.red(upd.err.slice(0, 300)));
|
|
3752
|
+
console.log(chalk21.yellow("\n\uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
|
|
3753
|
+
console.log(chalk21.gray(` npm update -g ${PACKAGE}`));
|
|
3754
|
+
printNextStep({
|
|
3755
|
+
message: t("update.nextFailMessage"),
|
|
3756
|
+
command: "vhk doctor",
|
|
3757
|
+
cursorHint: t("update.nextFailCursor")
|
|
3758
|
+
});
|
|
4381
3759
|
}
|
|
4382
3760
|
}
|
|
4383
3761
|
|
|
4384
3762
|
// src/commands/context.ts
|
|
4385
3763
|
import {
|
|
4386
3764
|
existsSync as existsSync11,
|
|
4387
|
-
mkdirSync as
|
|
4388
|
-
readFileSync as
|
|
4389
|
-
readdirSync,
|
|
4390
|
-
statSync,
|
|
4391
|
-
writeFileSync as
|
|
3765
|
+
mkdirSync as mkdirSync7,
|
|
3766
|
+
readFileSync as readFileSync4,
|
|
3767
|
+
readdirSync as readdirSync2,
|
|
3768
|
+
statSync as statSync2,
|
|
3769
|
+
writeFileSync as writeFileSync7
|
|
4392
3770
|
} from "fs";
|
|
4393
|
-
import { join as
|
|
4394
|
-
import
|
|
3771
|
+
import { join as join6 } from "path";
|
|
3772
|
+
import chalk22 from "chalk";
|
|
3773
|
+
|
|
3774
|
+
// src/lib/state-files.ts
|
|
3775
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6, appendFileSync, rmSync as rmSync2 } from "fs";
|
|
3776
|
+
import { join as join5 } from "path";
|
|
3777
|
+
var STATE_DIR2 = "docs/state";
|
|
3778
|
+
var BLOCKERS_PATH = join5(STATE_DIR2, "blockers.md");
|
|
3779
|
+
var LEARNINGS_PATH = join5(STATE_DIR2, "learnings.md");
|
|
3780
|
+
var VHK_DIR = ".vhk";
|
|
3781
|
+
var HARD_STOP_PATH = join5(VHK_DIR, "HARD_STOP");
|
|
3782
|
+
var HARD_STOP_BLOCKER_THRESHOLD = 3;
|
|
3783
|
+
function ensureStateDir() {
|
|
3784
|
+
mkdirSync6(STATE_DIR2, { recursive: true });
|
|
3785
|
+
}
|
|
3786
|
+
function ensureVhkDir() {
|
|
3787
|
+
mkdirSync6(VHK_DIR, { recursive: true });
|
|
3788
|
+
}
|
|
3789
|
+
function isoDate() {
|
|
3790
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3791
|
+
}
|
|
3792
|
+
var ACTIVE_BLOCKER_RE = /^- (?!~~)\[/;
|
|
3793
|
+
function countActiveBlockers(content) {
|
|
3794
|
+
let count = 0;
|
|
3795
|
+
for (const line of content.split(/\r?\n/)) {
|
|
3796
|
+
if (ACTIVE_BLOCKER_RE.test(line)) count++;
|
|
3797
|
+
}
|
|
3798
|
+
return count;
|
|
3799
|
+
}
|
|
3800
|
+
function appendBlocker(description, goalId) {
|
|
3801
|
+
ensureStateDir();
|
|
3802
|
+
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
3803
|
+
const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
|
|
3804
|
+
if (!existsSync10(BLOCKERS_PATH)) {
|
|
3805
|
+
writeFileSync6(
|
|
3806
|
+
BLOCKERS_PATH,
|
|
3807
|
+
`# Blockers
|
|
3808
|
+
|
|
3809
|
+
_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._
|
|
3810
|
+
|
|
3811
|
+
${line}
|
|
3812
|
+
`,
|
|
3813
|
+
"utf-8"
|
|
3814
|
+
);
|
|
3815
|
+
} else {
|
|
3816
|
+
appendFileSync(BLOCKERS_PATH, `${line}
|
|
3817
|
+
`, "utf-8");
|
|
3818
|
+
}
|
|
3819
|
+
const current = readFileSync3(BLOCKERS_PATH, "utf-8");
|
|
3820
|
+
const count = countActiveBlockers(current);
|
|
3821
|
+
let hardStopTripped = false;
|
|
3822
|
+
if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync10(HARD_STOP_PATH)) {
|
|
3823
|
+
writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
|
|
3824
|
+
hardStopTripped = true;
|
|
3825
|
+
}
|
|
3826
|
+
return { count, hardStopTripped };
|
|
3827
|
+
}
|
|
3828
|
+
function appendLearning(lesson, goalId) {
|
|
3829
|
+
ensureStateDir();
|
|
3830
|
+
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
3831
|
+
const line = `- [${isoDate()} ${tag}] ${lesson.trim()}`;
|
|
3832
|
+
if (!existsSync10(LEARNINGS_PATH)) {
|
|
3833
|
+
writeFileSync6(
|
|
3834
|
+
LEARNINGS_PATH,
|
|
3835
|
+
`# Learnings
|
|
3836
|
+
|
|
3837
|
+
_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._
|
|
3838
|
+
|
|
3839
|
+
${line}
|
|
3840
|
+
`,
|
|
3841
|
+
"utf-8"
|
|
3842
|
+
);
|
|
3843
|
+
} else {
|
|
3844
|
+
appendFileSync(LEARNINGS_PATH, `${line}
|
|
3845
|
+
`, "utf-8");
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
function getRecentLearnings(limit = 3) {
|
|
3849
|
+
if (!existsSync10(LEARNINGS_PATH)) return [];
|
|
3850
|
+
const lines = readFileSync3(LEARNINGS_PATH, "utf-8").split(/\r?\n/);
|
|
3851
|
+
const entries = lines.filter((l) => l.startsWith("- ["));
|
|
3852
|
+
return entries.slice(-limit);
|
|
3853
|
+
}
|
|
3854
|
+
function writeHardStop(reason) {
|
|
3855
|
+
ensureVhkDir();
|
|
3856
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3857
|
+
writeFileSync6(HARD_STOP_PATH, `${ts}
|
|
3858
|
+
${reason}
|
|
3859
|
+
`, "utf-8");
|
|
3860
|
+
}
|
|
3861
|
+
function isHardStopActive() {
|
|
3862
|
+
return existsSync10(HARD_STOP_PATH);
|
|
3863
|
+
}
|
|
3864
|
+
function readHardStopReason() {
|
|
3865
|
+
if (!existsSync10(HARD_STOP_PATH)) return null;
|
|
3866
|
+
try {
|
|
3867
|
+
return readFileSync3(HARD_STOP_PATH, "utf-8").trim();
|
|
3868
|
+
} catch {
|
|
3869
|
+
return null;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
function clearHardStop() {
|
|
3873
|
+
if (!existsSync10(HARD_STOP_PATH)) return false;
|
|
3874
|
+
rmSync2(HARD_STOP_PATH, { force: true });
|
|
3875
|
+
return true;
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
// src/commands/context.ts
|
|
4395
3879
|
var CONTEXT_PATH = ".vhk/context.md";
|
|
4396
|
-
var
|
|
3880
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
4397
3881
|
"node_modules",
|
|
4398
3882
|
".git",
|
|
4399
3883
|
"dist",
|
|
@@ -4409,15 +3893,15 @@ function buildTree(dir, prefix = "", maxDepth = 3, depth = 0) {
|
|
|
4409
3893
|
if (depth >= maxDepth) return [];
|
|
4410
3894
|
const lines = [];
|
|
4411
3895
|
try {
|
|
4412
|
-
const entries =
|
|
3896
|
+
const entries = readdirSync2(dir);
|
|
4413
3897
|
const filtered = entries.filter(
|
|
4414
|
-
(e) => (!e.startsWith(".") || e === ".env.example") && !
|
|
3898
|
+
(e) => (!e.startsWith(".") || e === ".env.example") && !IGNORE_DIRS.has(e)
|
|
4415
3899
|
);
|
|
4416
3900
|
filtered.forEach((entry, index) => {
|
|
4417
3901
|
const isLast = index === filtered.length - 1;
|
|
4418
3902
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
4419
|
-
const fullPath =
|
|
4420
|
-
const stat =
|
|
3903
|
+
const fullPath = join6(dir, entry);
|
|
3904
|
+
const stat = statSync2(fullPath);
|
|
4421
3905
|
const isDir = stat.isDirectory();
|
|
4422
3906
|
lines.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
|
|
4423
3907
|
if (isDir) {
|
|
@@ -4491,8 +3975,8 @@ function getVhkCommands() {
|
|
|
4491
3975
|
];
|
|
4492
3976
|
}
|
|
4493
3977
|
async function context() {
|
|
4494
|
-
console.log(
|
|
4495
|
-
console.log(
|
|
3978
|
+
console.log(chalk22.bold("\n\u{1F9E0} " + t("context.title")));
|
|
3979
|
+
console.log(chalk22.gray("\u2500".repeat(40)));
|
|
4496
3980
|
const stack = extractTechStack();
|
|
4497
3981
|
const tree = buildTree(".").join("\n");
|
|
4498
3982
|
const commands = getVhkCommands();
|
|
@@ -4537,16 +4021,45 @@ async function context() {
|
|
|
4537
4021
|
} catch {
|
|
4538
4022
|
}
|
|
4539
4023
|
}
|
|
4024
|
+
const goals = listGoals("goals");
|
|
4025
|
+
const activeId = selectActiveId(goals);
|
|
4026
|
+
if (activeId !== null) {
|
|
4027
|
+
const active = goals.find((g) => g.frontmatter.id === activeId);
|
|
4028
|
+
if (active) {
|
|
4029
|
+
lines.push("## Active Goal");
|
|
4030
|
+
lines.push("");
|
|
4031
|
+
lines.push(`- **id**: ${activeId}`);
|
|
4032
|
+
lines.push(`- **title**: ${active.frontmatter.title ?? "(untitled)"}`);
|
|
4033
|
+
lines.push(`- **status**: ${active.frontmatter.status ?? "NOT_STARTED"}`);
|
|
4034
|
+
lines.push(`- **priority**: ${active.frontmatter.priority ?? "--"}`);
|
|
4035
|
+
lines.push(`- **file**: ${active.filePath}`);
|
|
4036
|
+
lines.push("");
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
const recent = getRecentLearnings(3);
|
|
4040
|
+
if (recent.length > 0) {
|
|
4041
|
+
lines.push("## Recent Learnings");
|
|
4042
|
+
lines.push("");
|
|
4043
|
+
for (const r of recent) lines.push(r);
|
|
4044
|
+
lines.push("");
|
|
4045
|
+
}
|
|
4046
|
+
if (isHardStopActive()) {
|
|
4047
|
+
lines.push("## \u26A0\uFE0F HARD_STOP \uD65C\uC131");
|
|
4048
|
+
lines.push("");
|
|
4049
|
+
lines.push("`.vhk/HARD_STOP` \uD30C\uC77C \uC874\uC7AC \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8 \uC0C1\uD0DC.");
|
|
4050
|
+
lines.push("\uD574\uC81C: `vhk resume --confirm` (\uC0AC\uB78C \uD655\uC778 \uD6C4\uB9CC)");
|
|
4051
|
+
lines.push("");
|
|
4052
|
+
}
|
|
4540
4053
|
lines.push("---");
|
|
4541
4054
|
lines.push("");
|
|
4542
4055
|
lines.push(`_\uC0DD\uC131: ${(/* @__PURE__ */ new Date()).toLocaleString("ko-KR")}_`);
|
|
4543
4056
|
lines.push("");
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
console.log(
|
|
4057
|
+
mkdirSync7(".vhk", { recursive: true });
|
|
4058
|
+
writeFileSync7(CONTEXT_PATH, lines.join("\n"), "utf-8");
|
|
4059
|
+
console.log(chalk22.green(`
|
|
4547
4060
|
\u2705 ${CONTEXT_PATH} \uC0DD\uC131 \uC644\uB8CC!`));
|
|
4548
|
-
console.log(
|
|
4549
|
-
console.log(
|
|
4061
|
+
console.log(chalk22.gray(` \uAE30\uC220 \uC2A4\uD0DD ${Object.keys(stack).length}\uAC1C \uAC10\uC9C0`));
|
|
4062
|
+
console.log(chalk22.gray(" AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uC5D0\uAC8C \uC774 \uD30C\uC77C\uC744 \uCC38\uC870\uD558\uAC8C \uD558\uC138\uC694."));
|
|
4550
4063
|
printNextStep({
|
|
4551
4064
|
message: "\uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC644\uB8CC!",
|
|
4552
4065
|
command: "vhk context-show",
|
|
@@ -4554,20 +4067,20 @@ async function context() {
|
|
|
4554
4067
|
});
|
|
4555
4068
|
}
|
|
4556
4069
|
async function contextShow() {
|
|
4557
|
-
console.log(
|
|
4558
|
-
console.log(
|
|
4070
|
+
console.log(chalk22.bold("\n\u{1F4C4} " + t("context.showTitle")));
|
|
4071
|
+
console.log(chalk22.gray("\u2500".repeat(40)));
|
|
4559
4072
|
if (!existsSync11(CONTEXT_PATH)) {
|
|
4560
|
-
console.log(
|
|
4561
|
-
console.log(
|
|
4073
|
+
console.log(chalk22.yellow("\n\u26A0\uFE0F \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4074
|
+
console.log(chalk22.gray(" vhk context\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."));
|
|
4562
4075
|
return;
|
|
4563
4076
|
}
|
|
4564
|
-
const content =
|
|
4077
|
+
const content = readFileSync4(CONTEXT_PATH, "utf-8");
|
|
4565
4078
|
console.log("\n" + content);
|
|
4566
4079
|
}
|
|
4567
4080
|
|
|
4568
4081
|
// src/commands/memory.ts
|
|
4569
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
4570
|
-
import
|
|
4082
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
4083
|
+
import chalk23 from "chalk";
|
|
4571
4084
|
var MEMORY_PATH = ".vhk/memory.json";
|
|
4572
4085
|
function loadMemories() {
|
|
4573
4086
|
if (!existsSync12(MEMORY_PATH)) return [];
|
|
@@ -4579,15 +4092,15 @@ function loadMemories() {
|
|
|
4579
4092
|
}
|
|
4580
4093
|
}
|
|
4581
4094
|
function saveMemories(memories) {
|
|
4582
|
-
|
|
4583
|
-
|
|
4095
|
+
mkdirSync8(".vhk", { recursive: true });
|
|
4096
|
+
writeFileSync8(MEMORY_PATH, JSON.stringify(memories, null, 2) + "\n", "utf-8");
|
|
4584
4097
|
}
|
|
4585
4098
|
async function memoryAdd(content, tags) {
|
|
4586
|
-
console.log(
|
|
4587
|
-
console.log(
|
|
4099
|
+
console.log(chalk23.bold("\n\u{1F9E0} " + t("memory.addTitle")));
|
|
4100
|
+
console.log(chalk23.gray("\u2500".repeat(40)));
|
|
4588
4101
|
if (!content) {
|
|
4589
|
-
console.log(
|
|
4590
|
-
console.log(
|
|
4102
|
+
console.log(chalk23.red("\u274C \uAE30\uC5B5\uD560 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
|
|
4103
|
+
console.log(chalk23.gray(' \uC608: vhk memory add "API\uB294 tRPC \uC0AC\uC6A9\uD558\uAE30\uB85C \uACB0\uC815"'));
|
|
4591
4104
|
return;
|
|
4592
4105
|
}
|
|
4593
4106
|
const memories = loadMemories();
|
|
@@ -4597,9 +4110,9 @@ async function memoryAdd(content, tags) {
|
|
|
4597
4110
|
tags: tags && tags.length > 0 ? tags : []
|
|
4598
4111
|
});
|
|
4599
4112
|
saveMemories(memories);
|
|
4600
|
-
console.log(
|
|
4113
|
+
console.log(chalk23.green(`
|
|
4601
4114
|
\u2705 \uAE30\uC5B5 \uC800\uC7A5\uB428 (#${memories.length})`));
|
|
4602
|
-
console.log(
|
|
4115
|
+
console.log(chalk23.cyan(` \u{1F4DD} ${content}`));
|
|
4603
4116
|
printNextStep({
|
|
4604
4117
|
message: "\uAE30\uC5B5 \uC800\uC7A5 \uC644\uB8CC!",
|
|
4605
4118
|
command: "vhk memory list",
|
|
@@ -4607,24 +4120,24 @@ async function memoryAdd(content, tags) {
|
|
|
4607
4120
|
});
|
|
4608
4121
|
}
|
|
4609
4122
|
async function memoryList() {
|
|
4610
|
-
console.log(
|
|
4611
|
-
console.log(
|
|
4123
|
+
console.log(chalk23.bold("\n\u{1F9E0} " + t("memory.listTitle")));
|
|
4124
|
+
console.log(chalk23.gray("\u2500".repeat(40)));
|
|
4612
4125
|
const memories = loadMemories();
|
|
4613
4126
|
if (memories.length === 0) {
|
|
4614
|
-
console.log(
|
|
4615
|
-
console.log(
|
|
4127
|
+
console.log(chalk23.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uAE30\uC5B5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4128
|
+
console.log(chalk23.gray(' vhk memory add "\uB0B4\uC6A9"\uC73C\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
|
|
4616
4129
|
return;
|
|
4617
4130
|
}
|
|
4618
|
-
console.log(
|
|
4131
|
+
console.log(chalk23.cyan(`
|
|
4619
4132
|
\uCD1D ${memories.length}\uAC1C\uC758 \uAE30\uC5B5:
|
|
4620
4133
|
`));
|
|
4621
4134
|
memories.forEach((m, index) => {
|
|
4622
4135
|
const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
|
|
4623
|
-
console.log(
|
|
4136
|
+
console.log(chalk23.white(` [${index + 1}] ${m.content}`));
|
|
4624
4137
|
if (m.tags && m.tags.length > 0) {
|
|
4625
|
-
console.log(
|
|
4138
|
+
console.log(chalk23.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
|
|
4626
4139
|
}
|
|
4627
|
-
console.log(
|
|
4140
|
+
console.log(chalk23.gray(` \u{1F4C5} ${date}`));
|
|
4628
4141
|
console.log("");
|
|
4629
4142
|
});
|
|
4630
4143
|
}
|
|
@@ -4632,26 +4145,26 @@ async function memoryRemove(indexStr) {
|
|
|
4632
4145
|
const memories = loadMemories();
|
|
4633
4146
|
const idx = parseInt(indexStr, 10) - 1;
|
|
4634
4147
|
if (Number.isNaN(idx) || idx < 0 || idx >= memories.length) {
|
|
4635
|
-
console.log(
|
|
4148
|
+
console.log(chalk23.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${memories.length || 0})`));
|
|
4636
4149
|
return;
|
|
4637
4150
|
}
|
|
4638
4151
|
const removed = memories.splice(idx, 1)[0];
|
|
4639
4152
|
saveMemories(memories);
|
|
4640
|
-
console.log(
|
|
4641
|
-
console.log(
|
|
4153
|
+
console.log(chalk23.green("\n\u2705 \uAE30\uC5B5 \uC0AD\uC81C\uB428:"));
|
|
4154
|
+
console.log(chalk23.gray(` ${removed.content}`));
|
|
4642
4155
|
}
|
|
4643
4156
|
|
|
4644
4157
|
// src/commands/brief.ts
|
|
4645
|
-
import { existsSync as existsSync13, mkdirSync as
|
|
4646
|
-
import
|
|
4158
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
4159
|
+
import chalk24 from "chalk";
|
|
4647
4160
|
var BRIEF_PATH = ".vhk/brief.md";
|
|
4648
4161
|
function git2(args) {
|
|
4649
4162
|
const result = safeExecFile("git", args);
|
|
4650
4163
|
return result.ok ? result.out : "";
|
|
4651
4164
|
}
|
|
4652
4165
|
async function brief() {
|
|
4653
|
-
console.log(
|
|
4654
|
-
console.log(
|
|
4166
|
+
console.log(chalk24.bold("\n\u{1F4CB} " + t("brief.title")));
|
|
4167
|
+
console.log(chalk24.gray("\u2500".repeat(40)));
|
|
4655
4168
|
const lines = [];
|
|
4656
4169
|
lines.push("# \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB9AC\uD551");
|
|
4657
4170
|
lines.push("");
|
|
@@ -4732,10 +4245,10 @@ async function brief() {
|
|
|
4732
4245
|
lines.push("");
|
|
4733
4246
|
lines.push("_VHK CLI \uBE0C\uB9AC\uD551_");
|
|
4734
4247
|
lines.push("");
|
|
4735
|
-
|
|
4736
|
-
|
|
4248
|
+
mkdirSync9(".vhk", { recursive: true });
|
|
4249
|
+
writeFileSync9(BRIEF_PATH, lines.join("\n"), "utf-8");
|
|
4737
4250
|
console.log("\n" + lines.join("\n"));
|
|
4738
|
-
console.log(
|
|
4251
|
+
console.log(chalk24.green(`
|
|
4739
4252
|
\u2705 ${BRIEF_PATH} \uC800\uC7A5 \uC644\uB8CC`));
|
|
4740
4253
|
printNextStep({
|
|
4741
4254
|
message: "\uBE0C\uB9AC\uD551 \uC0DD\uC131 \uC644\uB8CC!",
|
|
@@ -4744,11 +4257,119 @@ async function brief() {
|
|
|
4744
4257
|
});
|
|
4745
4258
|
}
|
|
4746
4259
|
|
|
4260
|
+
// src/commands/start.ts
|
|
4261
|
+
import chalk25 from "chalk";
|
|
4262
|
+
import inquirer10 from "inquirer";
|
|
4263
|
+
import { simpleGit as simpleGit2 } from "simple-git";
|
|
4264
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4265
|
+
import { join as join7 } from "path";
|
|
4266
|
+
var VHK_FOOTPRINT_FILES = [
|
|
4267
|
+
"CLAUDE.md",
|
|
4268
|
+
".cursorrules",
|
|
4269
|
+
".cursor/mcp.json",
|
|
4270
|
+
".vhk/context.md",
|
|
4271
|
+
"docs/PRD.md"
|
|
4272
|
+
];
|
|
4273
|
+
function detectExistingFootprint(cwd) {
|
|
4274
|
+
return VHK_FOOTPRINT_FILES.filter((rel) => existsSync14(join7(cwd, rel)));
|
|
4275
|
+
}
|
|
4276
|
+
async function runGitInit(cwd) {
|
|
4277
|
+
try {
|
|
4278
|
+
const git3 = simpleGit2(cwd);
|
|
4279
|
+
const isRepo = await git3.checkIsRepo().catch(() => false);
|
|
4280
|
+
if (isRepo) {
|
|
4281
|
+
log.info(ko.start.gitAlreadyInit);
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
4284
|
+
await git3.init();
|
|
4285
|
+
log.success(ko.start.gitInitDone);
|
|
4286
|
+
} catch (err) {
|
|
4287
|
+
log.error(`git init \uC2E4\uD328: ${err instanceof Error ? err.message : String(err)}`);
|
|
4288
|
+
throw new Error("step1-git-init");
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
async function runStep(label, fn) {
|
|
4292
|
+
try {
|
|
4293
|
+
return await fn();
|
|
4294
|
+
} catch (err) {
|
|
4295
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4296
|
+
log.error(`${label} \uC2E4\uD328: ${msg}`);
|
|
4297
|
+
log.warn("\uB9C8\uBC95\uC0AC\uB97C \uC911\uB2E8\uD569\uB2C8\uB2E4. \uC774\uBBF8 \uC0DD\uC131\uB41C \uD30C\uC77C\uC740 \uADF8\uB300\uB85C \uB0A8\uC544\uC788\uC5B4\uC694.");
|
|
4298
|
+
log.warn("\uBB38\uC81C\uB97C \uACE0\uCE5C \uB4A4 `vhk start`\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uAC70\uB098, \uAC1C\uBCC4 \uBA85\uB839(`vhk init`, `vhk mcp-init`, `vhk context`)\uC73C\uB85C \uC774\uC5B4 \uC9C4\uD589\uD558\uC138\uC694.");
|
|
4299
|
+
throw err;
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
async function start(options = {}) {
|
|
4303
|
+
console.log(chalk25.bold(`
|
|
4304
|
+
${ko.start.title}
|
|
4305
|
+
`));
|
|
4306
|
+
console.log(chalk25.dim(ko.start.intro));
|
|
4307
|
+
console.log(chalk25.dim(` ${ko.start.step1}`));
|
|
4308
|
+
console.log(chalk25.dim(` ${ko.start.step2}`));
|
|
4309
|
+
console.log(chalk25.dim(` ${ko.start.step3}`));
|
|
4310
|
+
console.log(chalk25.dim(` ${ko.start.step4}`));
|
|
4311
|
+
console.log();
|
|
4312
|
+
const cwd = process.cwd();
|
|
4313
|
+
const footprint = detectExistingFootprint(cwd);
|
|
4314
|
+
if (footprint.length > 0 && !options.yes) {
|
|
4315
|
+
console.log(chalk25.yellow("\u26A0\uFE0F \uC774\uBBF8 VHK \uC124\uCE58 \uD754\uC801\uC774 \uAC10\uC9C0\uB410\uC5B4\uC694:"));
|
|
4316
|
+
for (const f of footprint) console.log(chalk25.dim(` - ${f}`));
|
|
4317
|
+
console.log(chalk25.dim(" \uACC4\uC18D \uC9C4\uD589\uD558\uBA74 \uC77C\uBD80 \uD30C\uC77C(`.cursor/mcp.json`, `.vhk/context.md`)\uC740 \uAC31\uC2E0\xB7\uB36E\uC5B4\uC4F0\uAE30\uB429\uB2C8\uB2E4."));
|
|
4318
|
+
const { proceedExisting } = await inquirer10.prompt([{
|
|
4319
|
+
type: "confirm",
|
|
4320
|
+
name: "proceedExisting",
|
|
4321
|
+
message: "\uADF8\uB798\uB3C4 \uB2E4\uC2DC \uB9C8\uBC95\uC0AC\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
|
|
4322
|
+
default: false
|
|
4323
|
+
}]);
|
|
4324
|
+
if (!proceedExisting) {
|
|
4325
|
+
log.warn(ko.start.cancelled);
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
} else if (!options.yes) {
|
|
4329
|
+
const { proceed } = await inquirer10.prompt([{
|
|
4330
|
+
type: "confirm",
|
|
4331
|
+
name: "proceed",
|
|
4332
|
+
message: ko.start.confirmStart,
|
|
4333
|
+
default: true
|
|
4334
|
+
}]);
|
|
4335
|
+
if (!proceed) {
|
|
4336
|
+
log.warn(ko.start.cancelled);
|
|
4337
|
+
return;
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
log.step(ko.start.step1Header);
|
|
4341
|
+
await runStep("[1/4] git init", () => runGitInit(cwd));
|
|
4342
|
+
log.step(ko.start.step2Header);
|
|
4343
|
+
await runStep("[2/4] vhk init", () => init({
|
|
4344
|
+
skipGate: true,
|
|
4345
|
+
fromNotion: options.fromNotion,
|
|
4346
|
+
name: options.name,
|
|
4347
|
+
description: options.description,
|
|
4348
|
+
type: options.type,
|
|
4349
|
+
yes: options.yes
|
|
4350
|
+
}));
|
|
4351
|
+
log.step(ko.start.step3Header);
|
|
4352
|
+
await runStep("[3/4] vhk mcp-init", () => mcpInit());
|
|
4353
|
+
log.step(ko.start.step4Header);
|
|
4354
|
+
await runStep("[4/4] vhk context", () => context());
|
|
4355
|
+
console.log(chalk25.bold.green(`
|
|
4356
|
+
${ko.start.allDone}
|
|
4357
|
+
`));
|
|
4358
|
+
printNextStep({
|
|
4359
|
+
message: ko.start.nextHintMessage,
|
|
4360
|
+
cursorHint: ko.start.nextHintCursor
|
|
4361
|
+
});
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4747
4364
|
// src/lib/nlp-run.ts
|
|
4748
4365
|
async function dispatchNlpRoute(route, input) {
|
|
4749
4366
|
switch (route.command) {
|
|
4750
4367
|
case "gate":
|
|
4751
4368
|
return gate();
|
|
4369
|
+
case "start":
|
|
4370
|
+
return start({
|
|
4371
|
+
fromNotion: route.args?.includes("--from-notion") ? extractNotionUrl(input) : void 0
|
|
4372
|
+
});
|
|
4752
4373
|
case "init":
|
|
4753
4374
|
return init({
|
|
4754
4375
|
skipGate: route.args?.includes("--skip-gate"),
|
|
@@ -4808,28 +4429,35 @@ async function dispatchNlpRoute(route, input) {
|
|
|
4808
4429
|
return memoryList();
|
|
4809
4430
|
case "brief":
|
|
4810
4431
|
return brief();
|
|
4432
|
+
case "goal": {
|
|
4433
|
+
const sub = route.args?.[0];
|
|
4434
|
+
if (sub === "next") return goalNext();
|
|
4435
|
+
if (sub === "check") return goalCheck({});
|
|
4436
|
+
if (sub === "done") return goalDone({});
|
|
4437
|
+
return goalList();
|
|
4438
|
+
}
|
|
4811
4439
|
}
|
|
4812
4440
|
}
|
|
4813
4441
|
async function runNaturalLanguageRoute(input) {
|
|
4814
4442
|
const route = routeNaturalLanguage(input);
|
|
4815
4443
|
if (!route) {
|
|
4816
|
-
console.log(
|
|
4444
|
+
console.log(chalk26.yellow(`
|
|
4817
4445
|
\u2753 "${input}" \u2014 ${ko.nlp.notMatched}
|
|
4818
4446
|
`));
|
|
4819
4447
|
return;
|
|
4820
4448
|
}
|
|
4821
4449
|
console.log("");
|
|
4822
|
-
console.log(
|
|
4823
|
-
console.log(
|
|
4450
|
+
console.log(chalk26.cyan(` \u{1F4AC} "${input}"`));
|
|
4451
|
+
console.log(chalk26.cyan(` \u2192 ${route.explanation}`));
|
|
4824
4452
|
if (route.confidence === "low") {
|
|
4825
|
-
const { confirm } = await
|
|
4453
|
+
const { confirm } = await inquirer11.prompt([{
|
|
4826
4454
|
type: "confirm",
|
|
4827
4455
|
name: "confirm",
|
|
4828
4456
|
message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
|
|
4829
4457
|
default: true
|
|
4830
4458
|
}]);
|
|
4831
4459
|
if (!confirm) {
|
|
4832
|
-
console.log(
|
|
4460
|
+
console.log(chalk26.dim(` ${ko.nlp.menuHint}`));
|
|
4833
4461
|
return;
|
|
4834
4462
|
}
|
|
4835
4463
|
}
|
|
@@ -4837,26 +4465,79 @@ async function runNaturalLanguageRoute(input) {
|
|
|
4837
4465
|
await dispatchNlpRoute(route, input);
|
|
4838
4466
|
}
|
|
4839
4467
|
|
|
4840
|
-
// src/
|
|
4841
|
-
import
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4468
|
+
// src/commands/agent.ts
|
|
4469
|
+
import chalk27 from "chalk";
|
|
4470
|
+
function activeGoalId() {
|
|
4471
|
+
const goals = listGoals("goals");
|
|
4472
|
+
const id = selectActiveId(goals);
|
|
4473
|
+
return id ?? void 0;
|
|
4474
|
+
}
|
|
4475
|
+
async function blocker(description) {
|
|
4476
|
+
console.log(chalk27.bold(`
|
|
4477
|
+
${ko.agent.blockerTitle}
|
|
4478
|
+
`));
|
|
4479
|
+
if (!description || !description.trim()) {
|
|
4480
|
+
console.log(chalk27.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
|
|
4481
|
+
console.log(chalk27.dim(' \uC608: vhk blocker "tsc \uC5D0\uB7EC \u2014 simple-git \uD0C0\uC785 \uD638\uD658"'));
|
|
4482
|
+
process.exitCode = 1;
|
|
4483
|
+
return;
|
|
4484
|
+
}
|
|
4485
|
+
const goalId = activeGoalId();
|
|
4486
|
+
const r = appendBlocker(description, goalId);
|
|
4487
|
+
console.log(chalk27.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
|
|
4488
|
+
if (r.hardStopTripped) {
|
|
4489
|
+
console.log(chalk27.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
|
|
4490
|
+
console.log(chalk27.yellow(" \uC0AC\uB78C \uAC80\uD1A0 \uD6C4 `vhk resume --confirm` \uC73C\uB85C\uB9CC \uD574\uC81C."));
|
|
4491
|
+
process.exitCode = 2;
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
async function learn(lesson) {
|
|
4495
|
+
console.log(chalk27.bold(`
|
|
4496
|
+
${ko.agent.learnTitle}
|
|
4497
|
+
`));
|
|
4498
|
+
if (!lesson || !lesson.trim()) {
|
|
4499
|
+
console.log(chalk27.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
|
|
4500
|
+
console.log(chalk27.dim(' \uC608: vhk learn "PowerShell \uC5D0\uC11C\uB294 ; \uC0AC\uC6A9 (&& \uBBF8\uC9C0\uC6D0)"'));
|
|
4501
|
+
process.exitCode = 1;
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4504
|
+
const goalId = activeGoalId();
|
|
4505
|
+
appendLearning(lesson, goalId);
|
|
4506
|
+
console.log(chalk27.green(" \u2705 learnings.md append."));
|
|
4507
|
+
console.log(
|
|
4508
|
+
chalk27.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
|
|
4509
|
+
);
|
|
4510
|
+
}
|
|
4511
|
+
async function resume(opts = {}) {
|
|
4512
|
+
console.log(chalk27.bold(`
|
|
4513
|
+
${ko.agent.resumeTitle}
|
|
4514
|
+
`));
|
|
4515
|
+
if (!isHardStopActive()) {
|
|
4516
|
+
console.log(chalk27.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
const reason = readHardStopReason();
|
|
4520
|
+
if (reason) {
|
|
4521
|
+
console.log(chalk27.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
|
|
4522
|
+
console.log(chalk27.dim(` ${reason.split("\n").join("\n ")}`));
|
|
4523
|
+
console.log("");
|
|
4524
|
+
}
|
|
4525
|
+
if (!opts.confirm) {
|
|
4526
|
+
console.log(
|
|
4527
|
+
chalk27.red(
|
|
4528
|
+
" \u274C --confirm \uD50C\uB798\uADF8 \uC5C6\uC774\uB294 \uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0)."
|
|
4529
|
+
)
|
|
4530
|
+
);
|
|
4531
|
+
console.log(chalk27.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
|
|
4532
|
+
process.exitCode = 1;
|
|
4533
|
+
return;
|
|
4534
|
+
}
|
|
4535
|
+
const removed = clearHardStop();
|
|
4536
|
+
if (removed) {
|
|
4537
|
+
console.log(chalk27.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
|
|
4538
|
+
} else {
|
|
4539
|
+
console.log(chalk27.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
|
|
4858
4540
|
}
|
|
4859
|
-
return "0.0.0";
|
|
4860
4541
|
}
|
|
4861
4542
|
|
|
4862
4543
|
// src/index.ts
|
|
@@ -4864,7 +4545,8 @@ var program = new Command();
|
|
|
4864
4545
|
var defaultHelp = new Help();
|
|
4865
4546
|
var KO_ALIASES = {
|
|
4866
4547
|
gate: "\uAC80\uC99D",
|
|
4867
|
-
|
|
4548
|
+
start: "\uC2DC\uC791",
|
|
4549
|
+
init: "\uCD08\uAE30\uD654",
|
|
4868
4550
|
recap: "\uC815\uB9AC",
|
|
4869
4551
|
sync: "\uADDC\uCE59",
|
|
4870
4552
|
check: "\uC810\uAC80",
|
|
@@ -4890,9 +4572,13 @@ var KO_ALIASES = {
|
|
|
4890
4572
|
context: "\uB9E5\uB77D",
|
|
4891
4573
|
"context-show": "\uB9E5\uB77D\uBCF4\uAE30",
|
|
4892
4574
|
memory: "\uAE30\uC5B5",
|
|
4893
|
-
brief: "\uBE0C\uB9AC\uD551"
|
|
4575
|
+
brief: "\uBE0C\uB9AC\uD551",
|
|
4576
|
+
goal: "\uBAA9\uD45C",
|
|
4577
|
+
blocker: "\uBE14\uB85C\uCEE4",
|
|
4578
|
+
learn: "\uAD50\uD6C8",
|
|
4579
|
+
resume: "\uC7AC\uAC1C"
|
|
4894
4580
|
};
|
|
4895
|
-
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(
|
|
4581
|
+
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(getVhkVersion());
|
|
4896
4582
|
program.configureHelp({
|
|
4897
4583
|
formatHelp(cmd, helper) {
|
|
4898
4584
|
if (cmd.parent) {
|
|
@@ -4917,10 +4603,13 @@ program.configureHelp({
|
|
|
4917
4603
|
}
|
|
4918
4604
|
});
|
|
4919
4605
|
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);
|
|
4920
|
-
program.command("
|
|
4606
|
+
program.command("start").alias("\uC2DC\uC791").alias("\uC0C8\uD504\uB85C\uC81D\uD2B8").description("\uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 \uB9C8\uBC95\uC0AC \u2014 git init + \uBB38\uC11C + MCP + \uCEE8\uD14D\uC2A4\uD2B8 \uD55C \uBC88\uC5D0").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", "\uBAA8\uB4E0 \uD655\uC778 \uC2A4\uD0B5 (\uC790\uB3D9 yes)").action(start);
|
|
4607
|
+
program.command("init").alias("\uCD08\uAE30\uD654").alias("\uB9CC\uB4E4\uAE30").description("\uD558\uB124\uC2A4 \uD30C\uC77C\uB9CC \uC0DD\uC131 (git/MCP/context\uB294 \uC81C\uC678) \u2014 \uBCF4\uD1B5 vhk start \uAD8C\uC7A5").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);
|
|
4921
4608
|
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);
|
|
4922
4609
|
program.command("sync").alias("\uB9DE\uCD94\uAE30").alias("\uADDC\uCE59").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654").action(sync);
|
|
4923
|
-
program.command("check").alias("\uC810\uAC80").alias("\uB9B0\uD2B8").description("RULES.md \uADDC\uCE59 \uC810\uAC80 \u2014 \uCF54\uB4DC \uC704\uBC18 \uAC80\uC0AC").action(
|
|
4610
|
+
program.command("check").alias("\uC810\uAC80").alias("\uB9B0\uD2B8").option("--goal <id>", "goal id \uC9C0\uC815 \uC2DC scripts/check-goal-<id>.sh \uAC8C\uC774\uD2B8 \uC2E4\uD589").description("RULES.md \uADDC\uCE59 \uC810\uAC80 \u2014 \uCF54\uB4DC \uC704\uBC18 \uAC80\uC0AC (\uB610\uB294 --goal <id> \uB85C goal \uAC8C\uC774\uD2B8)").action(async (opts) => {
|
|
4611
|
+
await check(opts);
|
|
4612
|
+
});
|
|
4924
4613
|
var secureCmd = program.command("secure").alias("\uBCF4\uC548").description("\uBCF4\uC548 \uB3C4\uAD6C \uBAA8\uC74C \u2014 scan: \uC2DC\uD06C\uB9BF\xB7\uD0A4 \uC720\uCD9C \uAC80\uC0AC").action(secure);
|
|
4925
4614
|
secureCmd.command("scan").alias("\uC2A4\uCE94").description("\uC2DC\uD06C\uB9BF/\uD0A4 \uC720\uCD9C \uC2A4\uCE94").action(secure);
|
|
4926
4615
|
program.command("ship").alias("\uCD9C\uD558").description("\uBC30\uD3EC \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 + \uD68C\uACE0 + \uBE4C\uB4DC \uB85C\uADF8 \uC0DD\uC131").action(ship);
|
|
@@ -4935,10 +4624,10 @@ program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\
|
|
|
4935
4624
|
await status();
|
|
4936
4625
|
});
|
|
4937
4626
|
program.command("diff").alias("\uBCC0\uACBD").alias("\uCC28\uC774").description("Git \uBCC0\uACBD\uC0AC\uD56D \uD55C\uAD6D\uC5B4 \uC694\uC57D (staged / unstaged / \uC0C8 \uD30C\uC77C)").action(diff);
|
|
4938
|
-
program.command("mcp").description("MCP \uC11C\uBC84 \uC2DC\uC791 (
|
|
4627
|
+
program.command("mcp").description("MCP \uC11C\uBC84 \uC2DC\uC791 (24 tool stdio \u2014 Cursor\xB7Claude Desktop \uB4F1)").action(async () => {
|
|
4939
4628
|
await startMcpServer();
|
|
4940
4629
|
});
|
|
4941
|
-
program.command("mcp-init").alias("mcp\uC124\uC815").description("Cursor MCP \uC5F0\uB3D9 \uC124\uC815 \uC790\uB3D9 \uC0DD\uC131 (.cursor/mcp.json)").action(async () => {
|
|
4630
|
+
program.command("mcp-init").alias("mcp\uC124\uC815").description("Cursor\xB7Claude Desktop MCP \uC5F0\uB3D9 \uC124\uC815 \uC790\uB3D9 \uC0DD\uC131 (.cursor/mcp.json)").action(async () => {
|
|
4942
4631
|
await mcpInit();
|
|
4943
4632
|
});
|
|
4944
4633
|
program.command("deploy").alias("\uBC30\uD3EC").description("\uD504\uB85C\uB355\uC158 \uBC30\uD3EC (Vercel/Netlify/Cloudflare \uC790\uB3D9 \uAC10\uC9C0)").action(async () => {
|
|
@@ -5008,6 +4697,33 @@ memoryCmd.command("remove <index>").alias("\uC0AD\uC81C").description("\uAE30\uC
|
|
|
5008
4697
|
program.command("brief").alias("\uBE0C\uB9AC\uD551").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uC694\uC57D \uBCF4\uACE0\uC11C \uC0DD\uC131 (.vhk/brief.md)").action(async () => {
|
|
5009
4698
|
await brief();
|
|
5010
4699
|
});
|
|
4700
|
+
var goalCmd = program.command("goal").alias("\uBAA9\uD45C").description("Goal \uB2E8\uACC4\uBCC4 \uBBF8\uC158 \uAD00\uB9AC (init / list / next / check / done)").action(async () => {
|
|
4701
|
+
await goalList();
|
|
4702
|
+
});
|
|
4703
|
+
goalCmd.command("list").alias("\uBAA9\uB85D").description("goals/*.md \uBAA9\uB85D (id, status, priority, title)").action(async () => {
|
|
4704
|
+
await goalList();
|
|
4705
|
+
});
|
|
4706
|
+
goalCmd.command("next").alias("\uB2E4\uC74C").description("active goal \uC790\uB3D9 \uC120\uD0DD \u2192 docs/state/next-task.md \uAC31\uC2E0").action(async () => {
|
|
4707
|
+
await goalNext();
|
|
4708
|
+
});
|
|
4709
|
+
goalCmd.command("init").alias("\uCD08\uAE30\uD654").description("\uD604\uC7AC \uD504\uB85C\uC81D\uD2B8\uC5D0 goals/ + docs/state/ \uC2A4\uCE90\uD3F4\uB529 (\uAE30\uC874 \uD30C\uC77C \uBCF4\uC874)").action(async () => {
|
|
4710
|
+
await goalInit();
|
|
4711
|
+
});
|
|
4712
|
+
goalCmd.command("check").alias("\uAC80\uC99D").option("--id <id>", "goal id \uC9C0\uC815 (\uC0DD\uB7B5 \uC2DC active goal)").description("scripts/check-goal-<id>.sh \uC2E4\uD589 + exit code \uC804\uB2EC").action(async (opts) => {
|
|
4713
|
+
await goalCheck(opts);
|
|
4714
|
+
});
|
|
4715
|
+
goalCmd.command("done").alias("\uC644\uB8CC").option("--id <id>", "goal id \uC9C0\uC815 (\uC0DD\uB7B5 \uC2DC active goal)").description("\uAC8C\uC774\uD2B8 \uC7AC\uAC80\uC99D \u2192 \uD1B5\uACFC \uC2DC frontmatter status=DONE \uC73C\uB85C \uC804\uC774").action(async (opts) => {
|
|
4716
|
+
await goalDone(opts);
|
|
4717
|
+
});
|
|
4718
|
+
program.command("blocker <description>").alias("\uBE14\uB85C\uCEE4").description("\uBE14\uB85C\uCEE4 \uAE30\uB85D \u2192 docs/state/blockers.md append (3\uAC74 \uB204\uC801 \uC2DC HARD_STOP \uC790\uB3D9 \uC0DD\uC131)").action(async (description) => {
|
|
4719
|
+
await blocker(description);
|
|
4720
|
+
});
|
|
4721
|
+
program.command("learn <lesson>").alias("\uAD50\uD6C8").description("\uAD50\uD6C8 \uAE30\uB85D \u2192 docs/state/learnings.md append (memory.json \uACFC \uBCC4\uB3C4 SoT)").action(async (lesson) => {
|
|
4722
|
+
await learn(lesson);
|
|
4723
|
+
});
|
|
4724
|
+
program.command("resume").alias("\uC7AC\uAC1C").option("--confirm", "\uC0AC\uB78C \uD655\uC778 \u2014 \uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0 (Forbidden \uC704\uBC18)").description(".vhk/HARD_STOP \uD574\uC81C (\uC0AC\uC6A9\uC790\uAC00 \uC0AC\uC720 \uD655\uC778 \uD6C4 --confirm \uD544\uC694)").action(async (opts) => {
|
|
4725
|
+
await resume(opts);
|
|
4726
|
+
});
|
|
5011
4727
|
program.on("command:*", async (operands) => {
|
|
5012
4728
|
const unknown = operands[0] ?? "";
|
|
5013
4729
|
const rest = operands.slice(1);
|
|
@@ -5016,13 +4732,13 @@ program.on("command:*", async (operands) => {
|
|
|
5016
4732
|
});
|
|
5017
4733
|
program.action(async () => {
|
|
5018
4734
|
console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
|
|
5019
|
-
const { choice } = await
|
|
4735
|
+
const { choice } = await inquirer12.prompt([{
|
|
5020
4736
|
type: "list",
|
|
5021
4737
|
name: "choice",
|
|
5022
4738
|
message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
|
|
5023
4739
|
choices: [
|
|
5024
4740
|
{ name: "\u{1F4A1} \uC0C8 \uC544\uC774\uB514\uC5B4 \uAC80\uC99D\uD558\uAE30", value: "gate" },
|
|
5025
|
-
{ name: "\u{
|
|
4741
|
+
{ name: "\u{1F680} \uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 \uB9C8\uBC95\uC0AC (start)", value: "start" },
|
|
5026
4742
|
{ name: "\u{1F4DD} \uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD558\uAE30", value: "recap" },
|
|
5027
4743
|
{ name: "\u{1F50D} \uADDC\uCE59 \uD30C\uC77C \uC810\uAC80\uD558\uAE30", value: "check" },
|
|
5028
4744
|
{ name: "\u{1F512} \uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB9AC\uAE30", value: "secure" },
|
|
@@ -5038,8 +4754,8 @@ program.action(async () => {
|
|
|
5038
4754
|
switch (choice) {
|
|
5039
4755
|
case "gate":
|
|
5040
4756
|
return gate();
|
|
5041
|
-
case "
|
|
5042
|
-
return
|
|
4757
|
+
case "start":
|
|
4758
|
+
return start();
|
|
5043
4759
|
case "recap":
|
|
5044
4760
|
return recap({});
|
|
5045
4761
|
case "check":
|