@nclamvn/vibecode-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/bin/vibecode.js +65 -0
- package/package.json +45 -0
- package/src/commands/doctor.js +130 -0
- package/src/commands/init.js +62 -0
- package/src/commands/lock.js +105 -0
- package/src/commands/start.js +222 -0
- package/src/commands/status.js +80 -0
- package/src/config/constants.js +89 -0
- package/src/config/templates.js +194 -0
- package/src/core/contract.js +103 -0
- package/src/core/session.js +115 -0
- package/src/core/state-machine.js +95 -0
- package/src/core/workspace.js +152 -0
- package/src/index.js +12 -0
- package/src/ui/branding.js +36 -0
- package/src/ui/output.js +96 -0
- package/src/ui/prompts.js +120 -0
- package/src/utils/files.js +84 -0
- package/src/utils/hash.js +34 -0
- package/templates/blueprint.md +61 -0
- package/templates/contract.md +61 -0
- package/templates/intake.md +37 -0
- package/templates/vibecode.yaml +15 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Session Management
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import {
|
|
7
|
+
getSessionsPath,
|
|
8
|
+
getSessionPath,
|
|
9
|
+
loadState,
|
|
10
|
+
saveState
|
|
11
|
+
} from './workspace.js';
|
|
12
|
+
import { ensureDir, pathExists, writeMarkdown, readMarkdown } from '../utils/files.js';
|
|
13
|
+
import { generateSessionId } from '../utils/hash.js';
|
|
14
|
+
import { getIntakeTemplate, getBlueprintTemplate, getContractTemplate } from '../config/templates.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create new session
|
|
18
|
+
*/
|
|
19
|
+
export async function createSession(projectName) {
|
|
20
|
+
const sessionId = generateSessionId();
|
|
21
|
+
const sessionPath = getSessionPath(sessionId);
|
|
22
|
+
|
|
23
|
+
// Create session directory
|
|
24
|
+
await ensureDir(sessionPath);
|
|
25
|
+
await ensureDir(path.join(sessionPath, 'evidence'));
|
|
26
|
+
|
|
27
|
+
// Update state with current session
|
|
28
|
+
const stateData = await loadState();
|
|
29
|
+
stateData.current_session = sessionId;
|
|
30
|
+
await saveState(stateData);
|
|
31
|
+
|
|
32
|
+
return sessionId;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get current session ID
|
|
37
|
+
*/
|
|
38
|
+
export async function getCurrentSessionId() {
|
|
39
|
+
const stateData = await loadState();
|
|
40
|
+
return stateData.current_session;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get current session path
|
|
45
|
+
*/
|
|
46
|
+
export async function getCurrentSessionPath() {
|
|
47
|
+
const sessionId = await getCurrentSessionId();
|
|
48
|
+
if (!sessionId) return null;
|
|
49
|
+
return getSessionPath(sessionId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if file exists in current session
|
|
54
|
+
*/
|
|
55
|
+
export async function sessionFileExists(filename) {
|
|
56
|
+
const sessionPath = await getCurrentSessionPath();
|
|
57
|
+
if (!sessionPath) return false;
|
|
58
|
+
return await pathExists(path.join(sessionPath, filename));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Write file to current session
|
|
63
|
+
*/
|
|
64
|
+
export async function writeSessionFile(filename, content) {
|
|
65
|
+
const sessionPath = await getCurrentSessionPath();
|
|
66
|
+
if (!sessionPath) throw new Error('No active session');
|
|
67
|
+
await writeMarkdown(path.join(sessionPath, filename), content);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read file from current session
|
|
72
|
+
*/
|
|
73
|
+
export async function readSessionFile(filename) {
|
|
74
|
+
const sessionPath = await getCurrentSessionPath();
|
|
75
|
+
if (!sessionPath) throw new Error('No active session');
|
|
76
|
+
return await readMarkdown(path.join(sessionPath, filename));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get session files status
|
|
81
|
+
*/
|
|
82
|
+
export async function getSessionFilesStatus() {
|
|
83
|
+
const sessionPath = await getCurrentSessionPath();
|
|
84
|
+
if (!sessionPath) return null;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
intake: await pathExists(path.join(sessionPath, 'intake.md')),
|
|
88
|
+
blueprint: await pathExists(path.join(sessionPath, 'blueprint.md')),
|
|
89
|
+
contract: await pathExists(path.join(sessionPath, 'contract.md'))
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create intake file from description
|
|
95
|
+
*/
|
|
96
|
+
export async function createIntake(projectName, description, sessionId) {
|
|
97
|
+
const template = getIntakeTemplate(projectName, description, sessionId);
|
|
98
|
+
await writeSessionFile('intake.md', template);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create blueprint file
|
|
103
|
+
*/
|
|
104
|
+
export async function createBlueprint(projectName, sessionId) {
|
|
105
|
+
const template = getBlueprintTemplate(projectName, sessionId);
|
|
106
|
+
await writeSessionFile('blueprint.md', template);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create contract file
|
|
111
|
+
*/
|
|
112
|
+
export async function createContract(projectName, sessionId) {
|
|
113
|
+
const template = getContractTemplate(projectName, sessionId);
|
|
114
|
+
await writeSessionFile('contract.md', template);
|
|
115
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - State Machine
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import { STATES, TRANSITIONS } from '../config/constants.js';
|
|
6
|
+
import { loadState, saveState, getLogsPath } from './workspace.js';
|
|
7
|
+
import { appendToFile } from '../utils/files.js';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get current state
|
|
12
|
+
*/
|
|
13
|
+
export async function getCurrentState() {
|
|
14
|
+
const stateData = await loadState();
|
|
15
|
+
return stateData.current_state;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if transition is valid
|
|
20
|
+
*/
|
|
21
|
+
export function isValidTransition(fromState, toState) {
|
|
22
|
+
const allowedTransitions = TRANSITIONS[fromState] || [];
|
|
23
|
+
return allowedTransitions.includes(toState);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get allowed transitions from current state
|
|
28
|
+
*/
|
|
29
|
+
export function getAllowedTransitions(currentState) {
|
|
30
|
+
return TRANSITIONS[currentState] || [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Transition to new state
|
|
35
|
+
*/
|
|
36
|
+
export async function transitionTo(newState, action = 'manual_transition') {
|
|
37
|
+
const stateData = await loadState();
|
|
38
|
+
const currentState = stateData.current_state;
|
|
39
|
+
|
|
40
|
+
if (!isValidTransition(currentState, newState)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Invalid transition: ${currentState} → ${newState}. ` +
|
|
43
|
+
`Allowed: ${getAllowedTransitions(currentState).join(', ') || 'none'}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Update state
|
|
48
|
+
stateData.current_state = newState;
|
|
49
|
+
stateData.history.push({
|
|
50
|
+
state: newState,
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
action: action,
|
|
53
|
+
from: currentState
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await saveState(stateData);
|
|
57
|
+
|
|
58
|
+
// Log to audit trail
|
|
59
|
+
await logAudit(`State transition: ${currentState} → ${newState} (${action})`);
|
|
60
|
+
|
|
61
|
+
return stateData;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Log to audit file
|
|
66
|
+
*/
|
|
67
|
+
export async function logAudit(message) {
|
|
68
|
+
const timestamp = new Date().toISOString();
|
|
69
|
+
const logLine = `[${timestamp}] ${message}`;
|
|
70
|
+
const logPath = path.join(getLogsPath(), 'audit.log');
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await appendToFile(logPath, logLine);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Silently fail if logs dir doesn't exist yet
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get state history
|
|
81
|
+
*/
|
|
82
|
+
export async function getStateHistory() {
|
|
83
|
+
const stateData = await loadState();
|
|
84
|
+
return stateData.history;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if state is at or past a certain point
|
|
89
|
+
*/
|
|
90
|
+
export function isStateAtLeast(currentState, targetState) {
|
|
91
|
+
const stateOrder = Object.values(STATES);
|
|
92
|
+
const currentIndex = stateOrder.indexOf(currentState);
|
|
93
|
+
const targetIndex = stateOrder.indexOf(targetState);
|
|
94
|
+
return currentIndex >= targetIndex;
|
|
95
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Workspace Management
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import {
|
|
7
|
+
WORKSPACE_DIR,
|
|
8
|
+
CONFIG_FILE,
|
|
9
|
+
STATE_FILE,
|
|
10
|
+
SESSIONS_DIR,
|
|
11
|
+
LIBRARY_DIR,
|
|
12
|
+
LOGS_DIR,
|
|
13
|
+
STATES
|
|
14
|
+
} from '../config/constants.js';
|
|
15
|
+
import {
|
|
16
|
+
pathExists,
|
|
17
|
+
ensureDir,
|
|
18
|
+
readJson,
|
|
19
|
+
writeJson,
|
|
20
|
+
readYaml,
|
|
21
|
+
writeYaml,
|
|
22
|
+
getCurrentDirName
|
|
23
|
+
} from '../utils/files.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get workspace root path
|
|
27
|
+
*/
|
|
28
|
+
export function getWorkspacePath() {
|
|
29
|
+
return path.join(process.cwd(), WORKSPACE_DIR);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if workspace exists
|
|
34
|
+
*/
|
|
35
|
+
export async function workspaceExists() {
|
|
36
|
+
return await pathExists(getWorkspacePath());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get config file path
|
|
41
|
+
*/
|
|
42
|
+
export function getConfigPath() {
|
|
43
|
+
return path.join(getWorkspacePath(), CONFIG_FILE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get state file path
|
|
48
|
+
*/
|
|
49
|
+
export function getStatePath() {
|
|
50
|
+
return path.join(getWorkspacePath(), STATE_FILE);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get sessions directory path
|
|
55
|
+
*/
|
|
56
|
+
export function getSessionsPath() {
|
|
57
|
+
return path.join(getWorkspacePath(), SESSIONS_DIR);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get specific session path
|
|
62
|
+
*/
|
|
63
|
+
export function getSessionPath(sessionId) {
|
|
64
|
+
return path.join(getSessionsPath(), sessionId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get logs directory path
|
|
69
|
+
*/
|
|
70
|
+
export function getLogsPath() {
|
|
71
|
+
return path.join(getWorkspacePath(), LOGS_DIR);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create workspace structure
|
|
76
|
+
*/
|
|
77
|
+
export async function createWorkspace() {
|
|
78
|
+
const workspacePath = getWorkspacePath();
|
|
79
|
+
|
|
80
|
+
// Create directories
|
|
81
|
+
await ensureDir(workspacePath);
|
|
82
|
+
await ensureDir(path.join(workspacePath, SESSIONS_DIR));
|
|
83
|
+
await ensureDir(path.join(workspacePath, LIBRARY_DIR));
|
|
84
|
+
await ensureDir(path.join(workspacePath, LIBRARY_DIR, 'prompts'));
|
|
85
|
+
await ensureDir(path.join(workspacePath, LOGS_DIR));
|
|
86
|
+
|
|
87
|
+
// Create config file
|
|
88
|
+
const config = {
|
|
89
|
+
version: '1.0',
|
|
90
|
+
created: new Date().toISOString(),
|
|
91
|
+
project: {
|
|
92
|
+
name: getCurrentDirName(),
|
|
93
|
+
type: 'auto'
|
|
94
|
+
},
|
|
95
|
+
settings: {
|
|
96
|
+
enforcement: 'strict',
|
|
97
|
+
auto_backup: true,
|
|
98
|
+
audit_log: true
|
|
99
|
+
},
|
|
100
|
+
provider: {
|
|
101
|
+
default: 'claude-code'
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
await writeYaml(getConfigPath(), config);
|
|
105
|
+
|
|
106
|
+
// Create state file
|
|
107
|
+
const state = {
|
|
108
|
+
version: '1.0',
|
|
109
|
+
current_state: STATES.INIT,
|
|
110
|
+
current_session: null,
|
|
111
|
+
spec_hash: null,
|
|
112
|
+
history: [
|
|
113
|
+
{
|
|
114
|
+
state: STATES.INIT,
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
action: 'workspace_initialized'
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
};
|
|
120
|
+
await writeJson(getStatePath(), state);
|
|
121
|
+
|
|
122
|
+
return { config, state };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Load config
|
|
127
|
+
*/
|
|
128
|
+
export async function loadConfig() {
|
|
129
|
+
return await readYaml(getConfigPath());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Load state
|
|
134
|
+
*/
|
|
135
|
+
export async function loadState() {
|
|
136
|
+
return await readJson(getStatePath());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Save state
|
|
141
|
+
*/
|
|
142
|
+
export async function saveState(state) {
|
|
143
|
+
await writeJson(getStatePath(), state);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get project name from config
|
|
148
|
+
*/
|
|
149
|
+
export async function getProjectName() {
|
|
150
|
+
const config = await loadConfig();
|
|
151
|
+
return config.project?.name || getCurrentDirName();
|
|
152
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Main Exports
|
|
3
|
+
// Spec Hash: 0fe43335f5a325e3279a079ce616c052
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
export { initCommand } from './commands/init.js';
|
|
7
|
+
export { startCommand } from './commands/start.js';
|
|
8
|
+
export { statusCommand } from './commands/status.js';
|
|
9
|
+
export { lockCommand } from './commands/lock.js';
|
|
10
|
+
export { doctorCommand } from './commands/doctor.js';
|
|
11
|
+
|
|
12
|
+
export { VERSION, SPEC_HASH, STATES } from './config/constants.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Branding & ASCII Art
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { VERSION } from '../config/constants.js';
|
|
7
|
+
|
|
8
|
+
export const LOGO = `
|
|
9
|
+
██╗ ██╗██╗██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
10
|
+
██║ ██║██║██╔══██╗██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔════╝
|
|
11
|
+
██║ ██║██║██████╔╝█████╗ ██║ ██║ ██║██║ ██║█████╗
|
|
12
|
+
╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██║ ██║██║ ██║██╔══╝
|
|
13
|
+
╚████╔╝ ██║██████╔╝███████╗╚██████╗╚██████╔╝██████╔╝███████╗
|
|
14
|
+
╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export function printLogo() {
|
|
18
|
+
console.log(chalk.cyan(LOGO));
|
|
19
|
+
console.log(chalk.gray.italic(' "Build with Discipline"'));
|
|
20
|
+
console.log(chalk.gray(` Version ${VERSION}`));
|
|
21
|
+
console.log();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function printWelcome() {
|
|
25
|
+
printLogo();
|
|
26
|
+
console.log(chalk.cyan('🏗️ Chào mừng! Tôi là Kiến trúc sư của bạn.'));
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(chalk.white(' Tôi đã thiết kế hàng triệu sản phẩm số.'));
|
|
29
|
+
console.log(chalk.white(' Tôi sẽ dẫn dắt bạn qua từng bước.'));
|
|
30
|
+
console.log(chalk.white(' Bạn chỉ cần cho tôi biết MỤC TIÊU.'));
|
|
31
|
+
console.log();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function printDivider() {
|
|
35
|
+
console.log(chalk.gray('─'.repeat(70)));
|
|
36
|
+
}
|
package/src/ui/output.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Output Formatting
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import boxen from 'boxen';
|
|
7
|
+
import { PROGRESS_MAP, COLORS } from '../config/constants.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Print boxed message
|
|
11
|
+
*/
|
|
12
|
+
export function printBox(content, options = {}) {
|
|
13
|
+
const defaultOptions = {
|
|
14
|
+
padding: 1,
|
|
15
|
+
borderStyle: 'round',
|
|
16
|
+
borderColor: 'cyan'
|
|
17
|
+
};
|
|
18
|
+
console.log(boxen(content, { ...defaultOptions, ...options }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Print success message
|
|
23
|
+
*/
|
|
24
|
+
export function printSuccess(message) {
|
|
25
|
+
console.log(chalk.green('✅ ' + message));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Print error message
|
|
30
|
+
*/
|
|
31
|
+
export function printError(message) {
|
|
32
|
+
console.log(chalk.red('❌ ' + message));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Print warning message
|
|
37
|
+
*/
|
|
38
|
+
export function printWarning(message) {
|
|
39
|
+
console.log(chalk.yellow('⚠️ ' + message));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Print info message
|
|
44
|
+
*/
|
|
45
|
+
export function printInfo(message) {
|
|
46
|
+
console.log(chalk.blue('ℹ️ ' + message));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Print progress bar based on state
|
|
51
|
+
*/
|
|
52
|
+
export function printProgress(state) {
|
|
53
|
+
const progress = PROGRESS_MAP[state] || PROGRESS_MAP.INIT;
|
|
54
|
+
|
|
55
|
+
const bar = ` ${progress.intake} INTAKE ${progress.blueprint} BLUEPRINT ${progress.contract} CONTRACT ${progress.build} BUILD ${progress.ship} SHIP`;
|
|
56
|
+
|
|
57
|
+
console.log(chalk.gray('┌─ Progress ─────────────────────────────────────────────────────────┐'));
|
|
58
|
+
console.log(chalk.white(bar));
|
|
59
|
+
console.log(chalk.gray('└────────────────────────────────────────────────────────────────────┘'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Print status info
|
|
64
|
+
*/
|
|
65
|
+
export function printStatus(projectName, state, sessionId, specHash = null) {
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(chalk.cyan(`📍 Project: ${chalk.white(projectName)}`));
|
|
68
|
+
console.log(chalk.cyan(`📊 State: ${chalk.white(state)}`));
|
|
69
|
+
console.log(chalk.cyan(`📋 Session: ${chalk.gray(sessionId || 'none')}`));
|
|
70
|
+
|
|
71
|
+
if (specHash) {
|
|
72
|
+
console.log(chalk.cyan(`🔒 Spec Hash: ${chalk.green(specHash)}`));
|
|
73
|
+
} else {
|
|
74
|
+
console.log(chalk.cyan(`📄 Spec Hash: ${chalk.gray('(not locked)')}`));
|
|
75
|
+
}
|
|
76
|
+
console.log();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Print file tree
|
|
81
|
+
*/
|
|
82
|
+
export function printFileTree(files) {
|
|
83
|
+
console.log(chalk.cyan('📁 Files:'));
|
|
84
|
+
files.forEach((file, index) => {
|
|
85
|
+
const prefix = index === files.length - 1 ? '└── ' : '├── ';
|
|
86
|
+
console.log(chalk.gray(` ${prefix}${file}`));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Print next step hint
|
|
92
|
+
*/
|
|
93
|
+
export function printNextStep(message) {
|
|
94
|
+
console.log();
|
|
95
|
+
console.log(chalk.yellow(`💡 Next: ${message}`));
|
|
96
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Interactive Prompts
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Ask for project description
|
|
10
|
+
*/
|
|
11
|
+
export async function askProjectDescription() {
|
|
12
|
+
const { description } = await inquirer.prompt([
|
|
13
|
+
{
|
|
14
|
+
type: 'editor',
|
|
15
|
+
name: 'description',
|
|
16
|
+
message: 'Mô tả ý tưởng dự án của bạn (mở editor):',
|
|
17
|
+
waitForUseInput: false
|
|
18
|
+
}
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// If editor fails, fall back to input
|
|
22
|
+
if (!description || description.trim() === '') {
|
|
23
|
+
const { fallback } = await inquirer.prompt([
|
|
24
|
+
{
|
|
25
|
+
type: 'input',
|
|
26
|
+
name: 'fallback',
|
|
27
|
+
message: chalk.cyan('📝 Bạn muốn build gì hôm nay?\n') + chalk.gray(' (Mô tả bằng ngôn ngữ tự nhiên)\n\n >'),
|
|
28
|
+
}
|
|
29
|
+
]);
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return description;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Simple text input for description
|
|
38
|
+
*/
|
|
39
|
+
export async function askSimpleDescription() {
|
|
40
|
+
console.log();
|
|
41
|
+
const { description } = await inquirer.prompt([
|
|
42
|
+
{
|
|
43
|
+
type: 'input',
|
|
44
|
+
name: 'description',
|
|
45
|
+
message: chalk.cyan('📝 Bạn muốn build gì hôm nay?'),
|
|
46
|
+
validate: (input) => {
|
|
47
|
+
if (input.trim().length < 10) {
|
|
48
|
+
return 'Vui lòng mô tả chi tiết hơn (ít nhất 10 ký tự)';
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]);
|
|
54
|
+
return description;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Confirm action
|
|
59
|
+
*/
|
|
60
|
+
export async function confirmAction(message) {
|
|
61
|
+
const { confirm } = await inquirer.prompt([
|
|
62
|
+
{
|
|
63
|
+
type: 'confirm',
|
|
64
|
+
name: 'confirm',
|
|
65
|
+
message: message,
|
|
66
|
+
default: true
|
|
67
|
+
}
|
|
68
|
+
]);
|
|
69
|
+
return confirm;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Select from options
|
|
74
|
+
*/
|
|
75
|
+
export async function selectOption(message, choices) {
|
|
76
|
+
const { selection } = await inquirer.prompt([
|
|
77
|
+
{
|
|
78
|
+
type: 'list',
|
|
79
|
+
name: 'selection',
|
|
80
|
+
message: message,
|
|
81
|
+
choices: choices
|
|
82
|
+
}
|
|
83
|
+
]);
|
|
84
|
+
return selection;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Ask what to do next based on state
|
|
89
|
+
*/
|
|
90
|
+
export async function askNextAction(state) {
|
|
91
|
+
const actions = {
|
|
92
|
+
'INIT': [
|
|
93
|
+
{ name: 'Capture intake (mô tả dự án)', value: 'intake' },
|
|
94
|
+
{ name: 'Exit', value: 'exit' }
|
|
95
|
+
],
|
|
96
|
+
'INTAKE_CAPTURED': [
|
|
97
|
+
{ name: 'Create blueprint', value: 'blueprint' },
|
|
98
|
+
{ name: 'View intake', value: 'view_intake' },
|
|
99
|
+
{ name: 'Exit', value: 'exit' }
|
|
100
|
+
],
|
|
101
|
+
'BLUEPRINT_DRAFTED': [
|
|
102
|
+
{ name: 'Create contract', value: 'contract' },
|
|
103
|
+
{ name: 'Edit blueprint', value: 'edit_blueprint' },
|
|
104
|
+
{ name: 'Exit', value: 'exit' }
|
|
105
|
+
],
|
|
106
|
+
'CONTRACT_DRAFTED': [
|
|
107
|
+
{ name: 'Lock contract (vibecode lock)', value: 'lock' },
|
|
108
|
+
{ name: 'Edit contract', value: 'edit_contract' },
|
|
109
|
+
{ name: 'Exit', value: 'exit' }
|
|
110
|
+
],
|
|
111
|
+
'CONTRACT_LOCKED': [
|
|
112
|
+
{ name: 'Ready for build! (Phase B)', value: 'ready' },
|
|
113
|
+
{ name: 'View contract', value: 'view_contract' },
|
|
114
|
+
{ name: 'Exit', value: 'exit' }
|
|
115
|
+
]
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const choices = actions[state] || actions['INIT'];
|
|
119
|
+
return await selectOption('What would you like to do?', choices);
|
|
120
|
+
}
|