dave-code 1.0.3 โ†’ 1.1.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/bin/install.js CHANGED
@@ -2,8 +2,8 @@ import ora from 'ora';
2
2
 
3
3
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4
4
 
5
- async function runInstallationAnimation() {
6
- console.log('\n\x1b[1;38;2;250;100;30m=== Dave Code Installer ===\x1b[0m\n');
5
+ export async function runWelcomeAnimation() {
6
+ console.log('\n\x1b[1;38;2;250;100;30m=== Welcome to Dave Code ===\x1b[0m\n');
7
7
 
8
8
  // Stage 1: Environment Check
9
9
  const spinner1 = ora({
@@ -40,11 +40,9 @@ async function runInstallationAnimation() {
40
40
  / /_/ / /_/ /| |/ / __// /___/ /_/ / /_/ / __/
41
41
  /_____/\\__,_/ |___/\\___/ \\____/\\____/\\__,_/\\___/ ` + '\x1b[0m\n');
42
42
 
43
- console.log('\x1b[1;32mโœ” Installation completed successfully! ๐ŸŽ‰\x1b[0m');
43
+ console.log('\x1b[1;32mโœ” Initialization completed successfully! ๐ŸŽ‰\x1b[0m');
44
44
  console.log('\x1b[90m--------------------------------------------------\x1b[0m');
45
45
  console.log('๐Ÿš€ \x1b[1;36mDave Code\x1b[0m is now ready for use!');
46
46
  console.log('๐Ÿ’ก Type \x1b[33mcalldave\x1b[0m in your terminal to start pair programming.');
47
47
  console.log('\x1b[90m--------------------------------------------------\x1b[0m\n');
48
48
  }
49
-
50
- runInstallationAnimation().catch(console.error);
@@ -0,0 +1,239 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ const STORE_VERSION = 1;
7
+ const VALID_STATUSES = new Set(['ready', 'running', 'completed', 'blocked']);
8
+ let plansBaseDir = path.join(os.homedir(), '.dave-code-plans');
9
+
10
+ export function setPlansBaseDirForTesting(directory) {
11
+ plansBaseDir = directory;
12
+ }
13
+
14
+ export function canonicalWorkspaceRoot(workspaceRoot) {
15
+ const resolved = path.resolve(workspaceRoot || process.cwd());
16
+ let canonical = resolved;
17
+ try {
18
+ canonical = fs.realpathSync.native(resolved);
19
+ } catch {
20
+ // A workspace is validated by the caller. Keep the resolved value for diagnostics.
21
+ }
22
+ return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
23
+ }
24
+
25
+ function workspaceKey(workspaceRoot) {
26
+ return crypto.createHash('sha256').update(canonicalWorkspaceRoot(workspaceRoot)).digest('hex');
27
+ }
28
+
29
+ export function getPlansFile(workspaceRoot) {
30
+ return path.join(plansBaseDir, `${workspaceKey(workspaceRoot)}.json`);
31
+ }
32
+
33
+ function ensurePlansDir() {
34
+ fs.mkdirSync(plansBaseDir, { recursive: true, mode: 0o700 });
35
+ try {
36
+ fs.chmodSync(plansBaseDir, 0o700);
37
+ } catch {
38
+ // Windows does not implement POSIX modes in the same way.
39
+ }
40
+ }
41
+
42
+ function normalizeName(name) {
43
+ return String(name || '').trim().toLocaleLowerCase();
44
+ }
45
+
46
+ export function validatePlanName(name) {
47
+ const value = String(name || '').trim();
48
+ if (!value) throw new Error('Plan name is required.');
49
+ if ([...value].length > 60) throw new Error('Plan name must be 60 characters or fewer.');
50
+ if (value.includes(':') || /[\u0000-\u001f\u007f]/.test(value)) {
51
+ throw new Error('Plan name cannot contain a colon or control characters.');
52
+ }
53
+ return value;
54
+ }
55
+
56
+ export function validatePlanContent(content) {
57
+ const text = String(content || '');
58
+ const checks = [
59
+ { label: 'goal and acceptance criteria', pattern: /็›ฎๆ ‡|้ชŒๆ”ถ|goal|acceptance/i },
60
+ { label: 'verified current-state facts', pattern: /ๅฝ“ๅ‰|็Žฐ็Šถ|ไบ‹ๅฎž|current state|verified facts/i },
61
+ { label: 'ordered implementation steps', pattern: /ๆญฅ้ชค|step\s*\d|implementation steps/i },
62
+ { label: 'what and how details', pattern: /ๅšไป€ไนˆ|ๆ€Žไนˆๅš|ๅ…ทไฝ“ๅฎž็Žฐ|what|how|implementation/i },
63
+ { label: 'validation', pattern: /้ชŒ่ฏ|ๆต‹่ฏ•|validation|test plan/i },
64
+ { label: 'risks and rollback', pattern: /้ฃŽ้™ฉ|ๅ›ž้€€|ๅ›žๆปš|risk|rollback/i }
65
+ ];
66
+ return checks.filter(check => !check.pattern.test(text)).map(check => check.label);
67
+ }
68
+
69
+ function validateStoredPlan(plan) {
70
+ if (!plan || typeof plan !== 'object') return null;
71
+ try {
72
+ const name = validatePlanName(plan.name);
73
+ const status = VALID_STATUSES.has(plan.status) ? plan.status : 'ready';
74
+ return {
75
+ id: String(plan.id || crypto.randomUUID()),
76
+ name,
77
+ status,
78
+ request: String(plan.request || ''),
79
+ content: String(plan.content || ''),
80
+ createdAt: Number(plan.createdAt) || Date.now(),
81
+ updatedAt: Number(plan.updatedAt) || Date.now(),
82
+ lastRun: plan.lastRun && typeof plan.lastRun === 'object' ? plan.lastRun : null
83
+ };
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function emptyStore(workspaceRoot) {
90
+ return {
91
+ version: STORE_VERSION,
92
+ workspaceRoot: canonicalWorkspaceRoot(workspaceRoot),
93
+ plans: []
94
+ };
95
+ }
96
+
97
+ export function loadPlanStore(workspaceRoot) {
98
+ const filePath = getPlansFile(workspaceRoot);
99
+ try {
100
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
101
+ if (!parsed || parsed.version !== STORE_VERSION || !Array.isArray(parsed.plans)) {
102
+ return emptyStore(workspaceRoot);
103
+ }
104
+ return {
105
+ version: STORE_VERSION,
106
+ workspaceRoot: canonicalWorkspaceRoot(workspaceRoot),
107
+ plans: parsed.plans.map(validateStoredPlan).filter(Boolean)
108
+ };
109
+ } catch {
110
+ return emptyStore(workspaceRoot);
111
+ }
112
+ }
113
+
114
+ function atomicWriteJson(filePath, value) {
115
+ ensurePlansDir();
116
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
117
+ fs.writeFileSync(tempPath, JSON.stringify(value, null, 2), { encoding: 'utf8', mode: 0o600 });
118
+ try {
119
+ fs.chmodSync(tempPath, 0o600);
120
+ } catch {
121
+ // Best effort on platforms without POSIX permissions.
122
+ }
123
+ try {
124
+ fs.renameSync(tempPath, filePath);
125
+ } catch (error) {
126
+ if (process.platform !== 'win32' || !fs.existsSync(filePath)) throw error;
127
+ fs.unlinkSync(filePath);
128
+ fs.renameSync(tempPath, filePath);
129
+ } finally {
130
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
131
+ }
132
+ }
133
+
134
+ export function savePlanStore(workspaceRoot, store) {
135
+ const normalized = {
136
+ version: STORE_VERSION,
137
+ workspaceRoot: canonicalWorkspaceRoot(workspaceRoot),
138
+ plans: (store.plans || []).map(validateStoredPlan).filter(Boolean)
139
+ };
140
+ atomicWriteJson(getPlansFile(workspaceRoot), normalized);
141
+ return normalized;
142
+ }
143
+
144
+ export function listPlans(workspaceRoot) {
145
+ return loadPlanStore(workspaceRoot).plans.sort((a, b) => {
146
+ const rank = status => status === 'running' ? 0 : status === 'ready' ? 1 : status === 'blocked' ? 2 : 3;
147
+ return rank(a.status) - rank(b.status) || b.updatedAt - a.updatedAt;
148
+ });
149
+ }
150
+
151
+ export function findPlan(workspaceRoot, name) {
152
+ const key = normalizeName(name);
153
+ return listPlans(workspaceRoot).find(plan => normalizeName(plan.name) === key) || null;
154
+ }
155
+
156
+ export function upsertPlan(workspaceRoot, { name, request, content }, { replace = false } = {}) {
157
+ const validName = validatePlanName(name);
158
+ const validContent = String(content || '').trim();
159
+ const missingSections = validatePlanContent(validContent);
160
+ if (missingSections.length > 0) throw new Error(`Plan content is incomplete: missing ${missingSections.join(', ')}.`);
161
+ const store = loadPlanStore(workspaceRoot);
162
+ const index = store.plans.findIndex(plan => normalizeName(plan.name) === normalizeName(validName));
163
+ const now = Date.now();
164
+ if (index !== -1 && !replace) throw new Error(`Plan already exists: ${store.plans[index].name}`);
165
+
166
+ const next = index === -1
167
+ ? {
168
+ id: crypto.randomUUID(),
169
+ name: validName,
170
+ status: 'ready',
171
+ request: String(request || '').trim(),
172
+ content: validContent,
173
+ createdAt: now,
174
+ updatedAt: now,
175
+ lastRun: null
176
+ }
177
+ : {
178
+ ...store.plans[index],
179
+ name: validName,
180
+ status: 'ready',
181
+ request: String(request || '').trim(),
182
+ content: validContent,
183
+ updatedAt: now,
184
+ lastRun: null
185
+ };
186
+
187
+ if (index === -1) store.plans.push(next);
188
+ else store.plans[index] = next;
189
+ savePlanStore(workspaceRoot, store);
190
+ return next;
191
+ }
192
+
193
+ export function updatePlan(workspaceRoot, planId, updates = {}) {
194
+ const store = loadPlanStore(workspaceRoot);
195
+ const index = store.plans.findIndex(plan => plan.id === planId);
196
+ if (index === -1) return null;
197
+ const nextStatus = updates.status === undefined ? store.plans[index].status : updates.status;
198
+ if (!VALID_STATUSES.has(nextStatus)) throw new Error(`Invalid plan status: ${nextStatus}`);
199
+ store.plans[index] = {
200
+ ...store.plans[index],
201
+ ...updates,
202
+ id: store.plans[index].id,
203
+ name: updates.name === undefined ? store.plans[index].name : validatePlanName(updates.name),
204
+ status: nextStatus,
205
+ updatedAt: Date.now()
206
+ };
207
+ savePlanStore(workspaceRoot, store);
208
+ return store.plans[index];
209
+ }
210
+
211
+ export function renamePlan(workspaceRoot, planId, nextName) {
212
+ const validName = validatePlanName(nextName);
213
+ const store = loadPlanStore(workspaceRoot);
214
+ if (store.plans.some(plan => plan.id !== planId && normalizeName(plan.name) === normalizeName(validName))) {
215
+ throw new Error(`Plan already exists: ${validName}`);
216
+ }
217
+ const index = store.plans.findIndex(plan => plan.id === planId);
218
+ if (index === -1) return null;
219
+ store.plans[index] = { ...store.plans[index], name: validName, updatedAt: Date.now() };
220
+ savePlanStore(workspaceRoot, store);
221
+ return store.plans[index];
222
+ }
223
+
224
+ export function deletePlan(workspaceRoot, planId) {
225
+ const store = loadPlanStore(workspaceRoot);
226
+ const next = store.plans.filter(plan => plan.id !== planId);
227
+ if (next.length === store.plans.length) return false;
228
+ store.plans = next;
229
+ savePlanStore(workspaceRoot, store);
230
+ return true;
231
+ }
232
+
233
+ export function planStatusLines(workspaceRoot, limit = 4) {
234
+ const plans = listPlans(workspaceRoot);
235
+ const symbols = { ready: 'โ—‹', running: 'โ–ถ', completed: 'โœ“', blocked: '!' };
236
+ const lines = plans.slice(0, limit).map(plan => `${symbols[plan.status] || 'โ—‹'} ${plan.name}`);
237
+ if (plans.length > limit) lines.push(`+${plans.length - limit} more`);
238
+ return lines;
239
+ }
@@ -0,0 +1,62 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ export const RUNTIME_EVENT_TYPES = new Set([
4
+ 'turn.started',
5
+ 'turn.completed',
6
+ 'turn.failed',
7
+ 'turn.cancelled',
8
+ 'model.started',
9
+ 'model.delta',
10
+ 'model.tool_call',
11
+ 'model.completed',
12
+ 'model.retry',
13
+ 'tool.requested',
14
+ 'tool.started',
15
+ 'tool.progress',
16
+ 'tool.completed',
17
+ 'tool.failed',
18
+ 'permission.requested',
19
+ 'permission.resolved',
20
+ 'context.compacted'
21
+ ]);
22
+
23
+ export function createRuntimeEvents({ turnId, now = Date.now } = {}) {
24
+ const emitter = new EventEmitter();
25
+ const resolvedTurnId = turnId || `turn-${now()}-${Math.random().toString(36).slice(2, 8)}`;
26
+ let seq = 0;
27
+ let closed = false;
28
+
29
+ function emit(type, data = {}) {
30
+ if (closed) return null;
31
+ if (!RUNTIME_EVENT_TYPES.has(type)) {
32
+ throw new Error(`Unknown runtime event type: ${type}`);
33
+ }
34
+ const event = {
35
+ version: 1,
36
+ seq: ++seq,
37
+ type,
38
+ turnId: resolvedTurnId,
39
+ timestamp: now(),
40
+ data: data && typeof data === 'object' ? data : { value: data }
41
+ };
42
+ emitter.emit('event', event);
43
+ return event;
44
+ }
45
+
46
+ function subscribe(listener) {
47
+ emitter.on('event', listener);
48
+ return () => emitter.off('event', listener);
49
+ }
50
+
51
+ function close() {
52
+ closed = true;
53
+ emitter.removeAllListeners();
54
+ }
55
+
56
+ return {
57
+ turnId: resolvedTurnId,
58
+ emit,
59
+ subscribe,
60
+ close
61
+ };
62
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * โ”€โ”€ DESIGN PHILOSOPHY / ่ง†่ง‰่ฎพ่ฎกไธŽไบคไบ’่ง„่Œƒ โ”€โ”€
3
+ * ไธบไบ†ๅ‘็”จๆˆทๆไพ›ๆž่‡ดใ€้กบๆป‘็š„ไบคไบ’ไฝ“้ชŒ๏ผŒDave Code ่ง„ๅฎš๏ผš
4
+ * 1. ๆ‰€ๆœ‰่€—ๆ—ถ็š„ API ่ฏทๆฑ‚ใ€ๅคงๆ–‡ไปถๅค„็†ๅ’Œๆœฌๅœฐๅทฅๅ…ท่ฐƒ็”จ๏ผŒ้ƒฝๅฟ…้กปๅ…ทๆœ‰ๅŠจ็”ปๅŒ–๏ผˆAnimated๏ผ‰ๆˆ–ๅŠจๆ€๏ผˆDynamic๏ผ‰็š„็ปˆ็ซฏ UIใ€‚
5
+ * 2. ไธฅ็ฆไฝฟ็”จ้™ๆ€ใ€ๅˆทๅฑๅผ็š„่ฟž็ปญ Log ่พ“ๅ‡บใ€‚ๅฏนไบŽๅคšๆญฅ้ชคๆ“ไฝœ๏ผˆๅฆ‚ๅˆ†็‰‡ๆถˆๅŒ–๏ผ‰๏ผŒๅบ”ไฝฟ็”จๅ•่กŒๅŽŸๅœฐๆ›ดๆ–ฐ๏ผˆIn-place update๏ผ‰็š„่ฟ›ๅบฆๆกๅ’ŒๅŠ ่ฝฝๅŠจ็”ปใ€‚
6
+ * 3. ไฝฟ็”จๅฏŒๆ–‡ๆœฌ็ปˆ็ซฏ่‰ฒๅฝฉ๏ผˆๅฆ‚็ฒพๅฟƒ่ฐƒ้…็š„ HSL/ANSI ้ขœ่‰ฒ๏ผ‰ๅ’Œๅพฎๅž‹ๅญ—็ฌฆๅŠจ็”ป๏ผˆๅฆ‚ Spinner๏ผ‰ๆฅๆไพ›ๅฎžๆ—ถ็š„่ง†่ง‰ๅ้ฆˆใ€‚
7
+ * ๅŽ็ปญ็ปดๆŠคไธŽๅŠŸ่ƒฝ็ปญๅ†™ๆ—ถ๏ผŒๅฟ…้กปไธฅๆ ผ้ตๅฎˆๆญค่ฎพ่ฎก็†ๅฟตใ€‚
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import os from 'os';
13
+ import crypto from 'crypto';
14
+
15
+ export let SESSIONS_DIR = path.join(os.homedir(), '.dave-code-sessions');
16
+
17
+ export function setSessionsDirForTesting(directory) {
18
+ SESSIONS_DIR = directory;
19
+ }
20
+
21
+ function normalizeMessage(message) {
22
+ if (!message || typeof message !== 'object') return null;
23
+ if (!['user', 'assistant', 'tool'].includes(message.role)) return null;
24
+ if (typeof message.content !== 'string') return null;
25
+ return { ...message, role: message.role, content: message.content };
26
+ }
27
+
28
+ function normalizeSession(session) {
29
+ if (!session || typeof session !== 'object' || !session.id || !Array.isArray(session.messages)) return null;
30
+ if (!/^[A-Za-z0-9_-]+$/.test(String(session.id))) return null;
31
+ const messages = session.messages.map(normalizeMessage).filter(Boolean);
32
+ return {
33
+ ...session,
34
+ id: String(session.id),
35
+ title: String(session.title || 'New Chat'),
36
+ timestamp: Number(session.timestamp) || Date.now(),
37
+ messages,
38
+ workspaceRoot: typeof session.workspaceRoot === 'string' ? session.workspaceRoot : null,
39
+ activeOpenFile: typeof session.activeOpenFile === 'string' ? session.activeOpenFile : null
40
+ };
41
+ }
42
+
43
+ function atomicWriteSession(filePath, sessionData) {
44
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(5).toString('hex')}.tmp`;
45
+ fs.writeFileSync(tempPath, JSON.stringify(sessionData, null, 2), { encoding: 'utf8', mode: 0o600 });
46
+ try {
47
+ fs.chmodSync(tempPath, 0o600);
48
+ } catch {
49
+ // Best effort on Windows.
50
+ }
51
+ try {
52
+ fs.renameSync(tempPath, filePath);
53
+ } catch (error) {
54
+ if (process.platform !== 'win32' || !fs.existsSync(filePath)) throw error;
55
+ fs.unlinkSync(filePath);
56
+ fs.renameSync(tempPath, filePath);
57
+ } finally {
58
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
59
+ }
60
+ }
61
+
62
+ export function ensureSessionsDir() {
63
+ try {
64
+ if (!fs.existsSync(SESSIONS_DIR)) {
65
+ fs.mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o700 });
66
+ }
67
+ try {
68
+ fs.chmodSync(SESSIONS_DIR, 0o700);
69
+ } catch {
70
+ // Best effort on Windows.
71
+ }
72
+ } catch (e) {
73
+ // Ignore folder creation errors (handled gracefully during saves)
74
+ }
75
+ }
76
+
77
+ /**
78
+ * List all saved chat sessions sorted by timestamp descending (newest first).
79
+ * @returns {Array} Array of session objects
80
+ */
81
+ export function listSessions() {
82
+ ensureSessionsDir();
83
+ try {
84
+ if (!fs.existsSync(SESSIONS_DIR)) return [];
85
+
86
+ const files = fs.readdirSync(SESSIONS_DIR);
87
+ const sessions = [];
88
+ for (const file of files) {
89
+ if (file.endsWith('.json')) {
90
+ try {
91
+ const filePath = path.join(SESSIONS_DIR, file);
92
+ const content = fs.readFileSync(filePath, 'utf8');
93
+ const session = normalizeSession(JSON.parse(content));
94
+ if (session && session.id) {
95
+ sessions.push(session);
96
+ }
97
+ } catch (e) {
98
+ // Skip invalid JSON files
99
+ }
100
+ }
101
+ }
102
+ return sessions.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
103
+ } catch (e) {
104
+ return [];
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Save or update a session file.
110
+ * @param {string} sessionId Unique session identifier
111
+ * @param {object} sessionData Session object containing messages, title, timestamp, and workspace metadata
112
+ */
113
+ export function saveSession(sessionId, sessionData) {
114
+ ensureSessionsDir();
115
+ try {
116
+ const safeId = String(sessionId || '');
117
+ if (!/^[A-Za-z0-9_-]+$/.test(safeId)) return false;
118
+ const normalized = normalizeSession({ ...sessionData, id: safeId });
119
+ if (!normalized) return false;
120
+ const filePath = path.join(SESSIONS_DIR, `${safeId}.json`);
121
+ atomicWriteSession(filePath, normalized);
122
+ return true;
123
+ } catch (e) {
124
+ return false;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Load a session from its identifier.
130
+ * @param {string} sessionId Unique session identifier
131
+ * @returns {object|null} Loaded session or null if not found
132
+ */
133
+ export function loadSession(sessionId) {
134
+ ensureSessionsDir();
135
+ try {
136
+ if (!/^[A-Za-z0-9_-]+$/.test(String(sessionId || ''))) return null;
137
+ const filePath = path.join(SESSIONS_DIR, `${sessionId}.json`);
138
+ if (fs.existsSync(filePath)) {
139
+ const content = fs.readFileSync(filePath, 'utf8');
140
+ return normalizeSession(JSON.parse(content));
141
+ }
142
+ } catch (e) {
143
+ // Return null on parsing or read failures
144
+ }
145
+ return null;
146
+ }
147
+
148
+ /**
149
+ * Delete a session from storage.
150
+ * @param {string} sessionId Unique session identifier
151
+ * @returns {boolean} True if successfully deleted
152
+ */
153
+ export function deleteSession(sessionId) {
154
+ ensureSessionsDir();
155
+ try {
156
+ if (!/^[A-Za-z0-9_-]+$/.test(String(sessionId || ''))) return false;
157
+ const filePath = path.join(SESSIONS_DIR, `${sessionId}.json`);
158
+ if (fs.existsSync(filePath)) {
159
+ fs.unlinkSync(filePath);
160
+ return true;
161
+ }
162
+ } catch (e) {
163
+ // Ignore unlink failures
164
+ }
165
+ return false;
166
+ }