@howlil/ez-agents 3.1.0 → 3.4.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 +295 -714
- package/bin/install.js +387 -62
- package/commands/ez/auth.md +87 -0
- package/commands/ez/join-discord.md +1 -1
- package/ez-agents/bin/ez-tools.cjs +120 -2
- package/ez-agents/bin/lib/assistant-adapter.cjs +62 -3
- package/ez-agents/bin/lib/audit-exec.cjs +20 -8
- package/ez-agents/bin/lib/auth.cjs +2 -1
- package/ez-agents/bin/lib/circuit-breaker.cjs +1 -1
- package/ez-agents/bin/lib/commands.cjs +42 -23
- package/ez-agents/bin/lib/config.cjs +18 -11
- package/ez-agents/bin/lib/core.cjs +42 -25
- package/ez-agents/bin/lib/file-lock.cjs +3 -3
- package/ez-agents/bin/lib/fs-utils.cjs +1 -1
- package/ez-agents/bin/lib/git-utils.cjs +1 -1
- package/ez-agents/bin/lib/health-check.cjs +2 -3
- package/ez-agents/bin/lib/index.cjs +1 -1
- package/ez-agents/bin/lib/init.cjs +70 -23
- package/ez-agents/bin/lib/logger.cjs +11 -4
- package/ez-agents/bin/lib/model-provider.cjs +124 -29
- package/ez-agents/bin/lib/phase.cjs +39 -22
- package/ez-agents/bin/lib/planning-write.cjs +107 -0
- package/ez-agents/bin/lib/retry.cjs +1 -1
- package/ez-agents/bin/lib/roadmap.cjs +3 -2
- package/ez-agents/bin/lib/safe-exec.cjs +1 -1
- package/ez-agents/bin/lib/safe-path.cjs +1 -1
- package/ez-agents/bin/lib/state.cjs +24 -9
- package/ez-agents/bin/lib/temp-file.cjs +1 -1
- package/ez-agents/bin/lib/template.cjs +2 -1
- package/ez-agents/bin/lib/test-file-lock.cjs +1 -1
- package/ez-agents/bin/lib/test-graceful.cjs +2 -2
- package/ez-agents/bin/lib/test-logger.cjs +2 -2
- package/ez-agents/bin/lib/test-temp-file.cjs +1 -1
- package/ez-agents/bin/lib/timeout-exec.cjs +4 -3
- package/ez-agents/bin/lib/verify.cjs +54 -25
- package/ez-agents/references/continuation-format.md +1 -1
- package/ez-agents/workflows/add-tests.md +2 -2
- package/ez-agents/workflows/add-todo.md +1 -1
- package/ez-agents/workflows/autonomous.md +15 -15
- package/ez-agents/workflows/diagnose-issues.md +1 -1
- package/ez-agents/workflows/discuss-phase.md +3 -3
- package/ez-agents/workflows/execute-phase.md +2 -2
- package/ez-agents/workflows/health.md +1 -1
- package/ez-agents/workflows/help.md +2 -2
- package/ez-agents/workflows/map-codebase.md +1 -1
- package/ez-agents/workflows/new-milestone.md +5 -5
- package/ez-agents/workflows/new-project.md +12 -10
- package/ez-agents/workflows/plan-phase.md +8 -8
- package/ez-agents/workflows/progress.md +1 -1
- package/ez-agents/workflows/set-profile.md +1 -1
- package/ez-agents/workflows/settings.md +9 -9
- package/ez-agents/workflows/stats.md +1 -1
- package/ez-agents/workflows/ui-phase.md +3 -3
- package/ez-agents/workflows/ui-review.md +2 -2
- package/ez-agents/workflows/update.md +1 -1
- package/ez-agents/workflows/validate-phase.md +3 -3
- package/ez-agents/workflows/verify-work.md +3 -3
- package/package.json +1 -1
- package/scripts/build-hooks.js +1 -1
- package/scripts/fix-qwen-installation.js +144 -0
- package/README.zh-CN.md +0 -702
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ez:auth
|
|
3
|
+
description: Manage API credentials securely (save, list, delete)
|
|
4
|
+
argument-hint: "<save|list|delete|test> [provider] [secret]"
|
|
5
|
+
agent: ez-helper
|
|
6
|
+
allowed-tools:
|
|
7
|
+
- Read
|
|
8
|
+
- Bash
|
|
9
|
+
---
|
|
10
|
+
<objective>
|
|
11
|
+
Manage API credentials securely using system keychain or fallback file storage
|
|
12
|
+
|
|
13
|
+
**Subcommands:**
|
|
14
|
+
- `ez:auth save <provider> <secret>` — Save credential for provider
|
|
15
|
+
- `ez:auth list` — List all stored providers
|
|
16
|
+
- `ez:auth delete <provider>` — Delete credential for provider
|
|
17
|
+
- `ez:auth test` — Test credential system and show keychain status
|
|
18
|
+
|
|
19
|
+
**Providers:** anthropic, moonshot, alibaba, qwen, openai
|
|
20
|
+
</objective>
|
|
21
|
+
|
|
22
|
+
<execution_context>
|
|
23
|
+
@~/.claude/ez-agents/workflows/ez-helper.md
|
|
24
|
+
</execution_context>
|
|
25
|
+
|
|
26
|
+
<context>
|
|
27
|
+
@.planning/PROJECT.md
|
|
28
|
+
@.planning/STATE.md
|
|
29
|
+
|
|
30
|
+
# Credential storage module:
|
|
31
|
+
@ez-agents/bin/lib/auth.cjs
|
|
32
|
+
|
|
33
|
+
# EZ tools CLI:
|
|
34
|
+
@ez-agents/bin/ez-tools.cjs
|
|
35
|
+
</context>
|
|
36
|
+
|
|
37
|
+
<process>
|
|
38
|
+
## Subcommand Routing
|
|
39
|
+
|
|
40
|
+
1. **Parse arguments** — First arg is subcommand (save/list/delete/test), remaining are parameters
|
|
41
|
+
2. **Route to handler** — Call appropriate auth function from ez-tools.cjs
|
|
42
|
+
3. **Output result** — Show success/failure with user-friendly messages
|
|
43
|
+
|
|
44
|
+
## Handlers
|
|
45
|
+
|
|
46
|
+
### save <provider> <secret>
|
|
47
|
+
1. Validate provider name against known providers
|
|
48
|
+
2. Call saveCredential(provider, secret)
|
|
49
|
+
3. Output: "✓ Credential saved for {provider}" or error message
|
|
50
|
+
|
|
51
|
+
### list
|
|
52
|
+
1. Call listProviders()
|
|
53
|
+
2. Check isKeychainAvailable()
|
|
54
|
+
3. Output table:
|
|
55
|
+
```
|
|
56
|
+
Provider Storage
|
|
57
|
+
─────────────────────────
|
|
58
|
+
anthropic Keychain ✓
|
|
59
|
+
qwen File (fallback)
|
|
60
|
+
```
|
|
61
|
+
4. If using file storage, show warning: "⚠ Using file storage — consider installing keytar for better security"
|
|
62
|
+
|
|
63
|
+
### delete <provider>
|
|
64
|
+
1. Validate provider exists
|
|
65
|
+
2. Confirm deletion (ask user or require --force)
|
|
66
|
+
3. Call deleteCredential(provider)
|
|
67
|
+
4. Output: "✓ Credential deleted for {provider}"
|
|
68
|
+
|
|
69
|
+
### test
|
|
70
|
+
1. Check isKeychainAvailable()
|
|
71
|
+
2. Output:
|
|
72
|
+
```
|
|
73
|
+
Credential System Test
|
|
74
|
+
|
|
75
|
+
Keychain (keytar): {Available | Unavailable}
|
|
76
|
+
Storage mode: {System keychain | Fallback file}
|
|
77
|
+
|
|
78
|
+
Stored providers: {count}
|
|
79
|
+
```
|
|
80
|
+
3. If keytar unavailable, show install hint: "npm install keytar"
|
|
81
|
+
|
|
82
|
+
## Error Handling
|
|
83
|
+
|
|
84
|
+
- **Invalid provider:** "Unknown provider '{name}'. Valid: anthropic, moonshot, alibaba, qwen, openai"
|
|
85
|
+
- **Credential not found:** "No credential found for {provider}. Use 'save' to add one."
|
|
86
|
+
- **Keychain error:** "Failed to access keychain: {error}. Using fallback storage."
|
|
87
|
+
</process>
|
|
@@ -12,7 +12,7 @@ Display the Discord invite link for the EZ Agents community server.
|
|
|
12
12
|
|
|
13
13
|
Connect with other EZ Agents users, get help, share what you're building, and stay updated.
|
|
14
14
|
|
|
15
|
-
**Invite link:** https://discord.gg/
|
|
15
|
+
**Invite link:** https://discord.gg/ez-agents
|
|
16
16
|
|
|
17
17
|
Click the link or paste it into your browser to join.
|
|
18
18
|
</output>
|
|
@@ -139,6 +139,8 @@ const milestone = require('./lib/milestone.cjs');
|
|
|
139
139
|
const commands = require('./lib/commands.cjs');
|
|
140
140
|
const init = require('./lib/init.cjs');
|
|
141
141
|
const frontmatter = require('./lib/frontmatter.cjs');
|
|
142
|
+
const HealthCheck = require('./lib/health-check.cjs');
|
|
143
|
+
const auth = require('./lib/auth.cjs');
|
|
142
144
|
|
|
143
145
|
// ─── CLI Router ───────────────────────────────────────────────────────────────
|
|
144
146
|
|
|
@@ -172,7 +174,7 @@ async function main() {
|
|
|
172
174
|
const command = args[0];
|
|
173
175
|
|
|
174
176
|
if (!command) {
|
|
175
|
-
error('Usage: ez-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init');
|
|
177
|
+
error('Usage: ez-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init, health');
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
switch (command) {
|
|
@@ -247,6 +249,122 @@ async function main() {
|
|
|
247
249
|
break;
|
|
248
250
|
}
|
|
249
251
|
|
|
252
|
+
case 'health': {
|
|
253
|
+
const health = new HealthCheck();
|
|
254
|
+
const result = health.runAll();
|
|
255
|
+
console.log(JSON.stringify(result, null, 2));
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case 'auth': {
|
|
260
|
+
const subcommand = args[1];
|
|
261
|
+
if (!subcommand) {
|
|
262
|
+
error('Usage: ez-tools auth <save|list|delete|test> [provider] [secret]\nSubcommands: save, list, delete, test');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
switch (subcommand) {
|
|
266
|
+
case 'save': {
|
|
267
|
+
const provider = args[2];
|
|
268
|
+
const secret = args[3];
|
|
269
|
+
if (!provider || !secret) {
|
|
270
|
+
error('Usage: ez-tools auth save <provider> <secret>');
|
|
271
|
+
}
|
|
272
|
+
const success = await auth.saveCredential(provider, secret);
|
|
273
|
+
if (success) {
|
|
274
|
+
console.log(`✓ Credential saved for ${provider}`);
|
|
275
|
+
} else {
|
|
276
|
+
console.error('Failed to save credential');
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
case 'list': {
|
|
283
|
+
const providers = await auth.listProviders();
|
|
284
|
+
const usingKeychain = auth.isKeychainAvailable();
|
|
285
|
+
|
|
286
|
+
console.log('');
|
|
287
|
+
console.log('Provider Storage');
|
|
288
|
+
console.log('─────────────────────────');
|
|
289
|
+
if (providers.length === 0) {
|
|
290
|
+
console.log('No credentials stored');
|
|
291
|
+
} else {
|
|
292
|
+
for (const p of providers) {
|
|
293
|
+
const storage = usingKeychain ? 'Keychain ✓' : 'File (fallback)';
|
|
294
|
+
console.log(`${p.padEnd(14)} ${storage}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!usingKeychain) {
|
|
298
|
+
console.log('');
|
|
299
|
+
console.log('⚠ Using file storage — consider installing keytar for better security');
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
case 'delete': {
|
|
305
|
+
const provider = args[2];
|
|
306
|
+
const force = args.includes('--force');
|
|
307
|
+
if (!provider) {
|
|
308
|
+
error('Usage: ez-tools auth delete <provider> [--force]');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!force) {
|
|
312
|
+
const readline = require('readline');
|
|
313
|
+
const rl = readline.createInterface({
|
|
314
|
+
input: process.stdin,
|
|
315
|
+
output: process.stdout
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const answer = await new Promise(resolve => {
|
|
319
|
+
rl.question(`Delete credential for ${provider}? [y/N] `, resolve);
|
|
320
|
+
rl.close();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (answer.toLowerCase() !== 'y') {
|
|
324
|
+
console.log('Delete cancelled');
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const success = await auth.deleteCredential(provider);
|
|
330
|
+
if (success) {
|
|
331
|
+
console.log(`✓ Credential deleted for ${provider}`);
|
|
332
|
+
} else {
|
|
333
|
+
console.error(`No credential found for ${provider}`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
case 'test': {
|
|
340
|
+
const usingKeychain = auth.isKeychainAvailable();
|
|
341
|
+
const providers = await auth.listProviders();
|
|
342
|
+
|
|
343
|
+
console.log('');
|
|
344
|
+
console.log('Credential System Test');
|
|
345
|
+
console.log('══════════════════════');
|
|
346
|
+
console.log(`Keychain (keytar): ${usingKeychain ? 'Available ✓' : 'Unavailable'}`);
|
|
347
|
+
console.log(`Storage mode: ${usingKeychain ? 'System keychain' : 'Fallback file'}`);
|
|
348
|
+
console.log('');
|
|
349
|
+
console.log(`Stored providers: ${providers.length}`);
|
|
350
|
+
if (providers.length > 0) {
|
|
351
|
+
console.log(` ${providers.join(', ')}`);
|
|
352
|
+
}
|
|
353
|
+
if (!usingKeychain) {
|
|
354
|
+
console.log('');
|
|
355
|
+
console.log('Tip: Install keytar for better security:');
|
|
356
|
+
console.log(' npm install keytar');
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
default: {
|
|
362
|
+
error('Unknown auth subcommand: ' + subcommand + '\nValid: save, list, delete, test');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
|
|
250
368
|
case 'resolve-model': {
|
|
251
369
|
commands.cmdResolveModel(cwd, args[1], raw);
|
|
252
370
|
break;
|
|
@@ -526,7 +644,7 @@ async function main() {
|
|
|
526
644
|
init.cmdInitPlanPhase(cwd, args[2], raw);
|
|
527
645
|
break;
|
|
528
646
|
case 'new-project':
|
|
529
|
-
init.cmdInitNewProject(cwd, raw);
|
|
647
|
+
await init.cmdInitNewProject(cwd, raw);
|
|
530
648
|
break;
|
|
531
649
|
case 'new-milestone':
|
|
532
650
|
init.cmdInitNewMilestone(cwd, raw);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* EZ Assistant Adapters — Unified interface for AI coding assistants
|
|
5
5
|
*
|
|
6
6
|
* Adapters for: Claude Code, OpenCode, Gemini CLI, Codex
|
|
7
7
|
*
|
|
@@ -165,6 +165,63 @@ class CodexAdapter extends AssistantAdapter {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Qwen Code adapter
|
|
170
|
+
*/
|
|
171
|
+
class QwenAdapter extends AssistantAdapter {
|
|
172
|
+
constructor() {
|
|
173
|
+
super('qwen');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async spawnAgent(type, options) {
|
|
177
|
+
logger.info('Qwen Code: spawning agent', { type });
|
|
178
|
+
// Qwen Code uses its own agent system
|
|
179
|
+
return { type, status: 'completed', result: '[Qwen Code agent result]' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async callTool(tool, params) {
|
|
183
|
+
logger.info('Qwen Code: calling tool', { tool });
|
|
184
|
+
return { tool, status: 'success' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
selectModel(taskType) {
|
|
188
|
+
const models = {
|
|
189
|
+
planning: 'qwen-max',
|
|
190
|
+
execution: 'qwen-plus',
|
|
191
|
+
verification: 'qwen-plus'
|
|
192
|
+
};
|
|
193
|
+
return models[taskType] || models.execution;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Kimi Code adapter
|
|
199
|
+
*/
|
|
200
|
+
class KimiAdapter extends AssistantAdapter {
|
|
201
|
+
constructor() {
|
|
202
|
+
super('kimi');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async spawnAgent(type, options) {
|
|
206
|
+
logger.info('Kimi Code: spawning agent', { type });
|
|
207
|
+
return { type, status: 'completed', result: '[Kimi Code agent result]' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async callTool(tool, params) {
|
|
211
|
+
logger.info('Kimi Code: calling tool', { tool });
|
|
212
|
+
return { tool, status: 'success' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
selectModel(taskType) {
|
|
216
|
+
const models = {
|
|
217
|
+
planning: 'moonshot-v1-32k',
|
|
218
|
+
execution: 'moonshot-v1-8k',
|
|
219
|
+
verification: 'moonshot-v1-8k'
|
|
220
|
+
};
|
|
221
|
+
return models[taskType] || models.execution;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
168
225
|
/**
|
|
169
226
|
* Factory function to create adapter
|
|
170
227
|
* @param {string} type - Adapter type
|
|
@@ -175,7 +232,9 @@ function createAdapter(type) {
|
|
|
175
232
|
'claude-code': ClaudeCodeAdapter,
|
|
176
233
|
'opencode': OpenCodeAdapter,
|
|
177
234
|
'gemini': GeminiAdapter,
|
|
178
|
-
'codex': CodexAdapter
|
|
235
|
+
'codex': CodexAdapter,
|
|
236
|
+
'qwen': QwenAdapter,
|
|
237
|
+
'kimi': KimiAdapter
|
|
179
238
|
};
|
|
180
239
|
|
|
181
240
|
const AdapterClass = adapters[type];
|
|
@@ -191,7 +250,7 @@ function createAdapter(type) {
|
|
|
191
250
|
* @returns {string[]} - List of adapter names
|
|
192
251
|
*/
|
|
193
252
|
function getAvailableAdapters() {
|
|
194
|
-
return ['claude-code', 'opencode', 'gemini', 'codex'];
|
|
253
|
+
return ['claude-code', 'opencode', 'gemini', 'codex', 'qwen', 'kimi'];
|
|
195
254
|
}
|
|
196
255
|
|
|
197
256
|
module.exports = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* EZ Audit Exec — Command execution with full audit logging
|
|
5
5
|
*
|
|
6
6
|
* Logs all command executions to audit file for security review:
|
|
7
7
|
* - Timestamp, command, arguments, context
|
|
@@ -22,13 +22,25 @@ const logger = new Logger();
|
|
|
22
22
|
|
|
23
23
|
const execFileAsync = promisify(execFile);
|
|
24
24
|
|
|
25
|
-
// Audit log file path
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// Audit log file path - lazy init
|
|
26
|
+
let _AUDIT_DIR;
|
|
27
|
+
let _AUDIT_FILE;
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
if (!
|
|
31
|
-
|
|
29
|
+
function getAuditDir() {
|
|
30
|
+
if (!_AUDIT_DIR) {
|
|
31
|
+
_AUDIT_DIR = join('.planning', 'logs');
|
|
32
|
+
if (!existsSync(_AUDIT_DIR)) {
|
|
33
|
+
mkdirSync(_AUDIT_DIR, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return _AUDIT_DIR;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getAuditFile() {
|
|
40
|
+
if (!_AUDIT_FILE) {
|
|
41
|
+
_AUDIT_FILE = join(getAuditDir(), `audit-${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
42
|
+
}
|
|
43
|
+
return _AUDIT_FILE;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
/**
|
|
@@ -37,7 +49,7 @@ if (!existsSync(AUDIT_DIR)) {
|
|
|
37
49
|
*/
|
|
38
50
|
function writeAudit(entry) {
|
|
39
51
|
const line = JSON.stringify(entry) + '\n';
|
|
40
|
-
appendFileSync(
|
|
52
|
+
appendFileSync(getAuditFile(), line, 'utf-8');
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* EZ Auth — Secure credential storage using system keychain
|
|
5
5
|
*
|
|
6
6
|
* Stores API keys securely using:
|
|
7
7
|
* - keytar for system keychain (Windows Credential Manager, macOS Keychain, libsecret)
|
|
@@ -25,6 +25,7 @@ const PROVIDERS = {
|
|
|
25
25
|
ANTHROPIC: 'anthropic',
|
|
26
26
|
MOONSHOT: 'moonshot',
|
|
27
27
|
ALIBABA: 'alibaba',
|
|
28
|
+
QWEN: 'qwen',
|
|
28
29
|
OPENAI: 'openai'
|
|
29
30
|
};
|
|
30
31
|
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
7
6
|
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
|
|
8
7
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
8
|
+
const { defaultLogger: logger } = require('./logger.cjs');
|
|
9
9
|
|
|
10
10
|
function cmdGenerateSlug(text, raw) {
|
|
11
11
|
if (!text) {
|
|
@@ -70,9 +70,13 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
70
70
|
area: todoArea,
|
|
71
71
|
path: toPosixPath(path.join('.planning', 'todos', 'pending', file)),
|
|
72
72
|
});
|
|
73
|
-
} catch {
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logger.warn('Failed to parse todo file in cmdListTodos', { file, error: err.message });
|
|
75
|
+
}
|
|
74
76
|
}
|
|
75
|
-
} catch {
|
|
77
|
+
} catch (err) {
|
|
78
|
+
logger.warn('Failed to list pending todos in cmdListTodos', { pendingDir, error: err.message });
|
|
79
|
+
}
|
|
76
80
|
|
|
77
81
|
const result = { count, todos };
|
|
78
82
|
output(result, raw, count.toString());
|
|
@@ -90,7 +94,8 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
|
|
|
90
94
|
const type = stats.isDirectory() ? 'directory' : stats.isFile() ? 'file' : 'other';
|
|
91
95
|
const result = { exists: true, type };
|
|
92
96
|
output(result, raw, 'true');
|
|
93
|
-
} catch {
|
|
97
|
+
} catch (err) {
|
|
98
|
+
logger.warn('Path verification failed in cmdVerifyPathExists', { fullPath, error: err.message });
|
|
94
99
|
const result = { exists: false, type: null };
|
|
95
100
|
output(result, raw, 'false');
|
|
96
101
|
}
|
|
@@ -119,7 +124,9 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
119
124
|
for (const dir of currentDirs) {
|
|
120
125
|
allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null });
|
|
121
126
|
}
|
|
122
|
-
} catch {
|
|
127
|
+
} catch (err) {
|
|
128
|
+
logger.warn('Failed to enumerate current phase directories in cmdHistoryDigest', { phasesDir, error: err.message });
|
|
129
|
+
}
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
if (allPhaseDirs.length === 0) {
|
|
@@ -177,8 +184,8 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
177
184
|
fm['tech-stack'].added.forEach(t => digest.tech_stack.add(typeof t === 'string' ? t : t.name));
|
|
178
185
|
}
|
|
179
186
|
|
|
180
|
-
} catch (
|
|
181
|
-
|
|
187
|
+
} catch (err) {
|
|
188
|
+
logger.warn('Skipping malformed summary in cmdHistoryDigest', { summary, dirPath, error: err.message });
|
|
182
189
|
}
|
|
183
190
|
}
|
|
184
191
|
}
|
|
@@ -192,8 +199,9 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
192
199
|
digest.tech_stack = [...digest.tech_stack];
|
|
193
200
|
|
|
194
201
|
output(digest, raw);
|
|
195
|
-
} catch (
|
|
196
|
-
error('Failed to generate history digest
|
|
202
|
+
} catch (err) {
|
|
203
|
+
logger.error('Failed to generate history digest', { error: err.message });
|
|
204
|
+
error('Failed to generate history digest: ' + err.message);
|
|
197
205
|
}
|
|
198
206
|
}
|
|
199
207
|
|
|
@@ -213,7 +221,7 @@ function cmdResolveModel(cwd, agentType, raw) {
|
|
|
213
221
|
output(result, raw, model);
|
|
214
222
|
}
|
|
215
223
|
|
|
216
|
-
function cmdCommit(cwd, message, files, raw, amend) {
|
|
224
|
+
async function cmdCommit(cwd, message, files, raw, amend) {
|
|
217
225
|
if (!message && !amend) {
|
|
218
226
|
error('commit message required');
|
|
219
227
|
}
|
|
@@ -228,7 +236,7 @@ function cmdCommit(cwd, message, files, raw, amend) {
|
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
// Check if .planning is gitignored
|
|
231
|
-
if (isGitIgnored(cwd, '.planning')) {
|
|
239
|
+
if (await isGitIgnored(cwd, '.planning')) {
|
|
232
240
|
const result = { committed: false, hash: null, reason: 'skipped_gitignored' };
|
|
233
241
|
output(result, raw, 'skipped');
|
|
234
242
|
return;
|
|
@@ -237,12 +245,12 @@ function cmdCommit(cwd, message, files, raw, amend) {
|
|
|
237
245
|
// Stage files
|
|
238
246
|
const filesToStage = files && files.length > 0 ? files : ['.planning/'];
|
|
239
247
|
for (const file of filesToStage) {
|
|
240
|
-
execGit(cwd, ['add', file]);
|
|
248
|
+
await execGit(cwd, ['add', file]);
|
|
241
249
|
}
|
|
242
250
|
|
|
243
251
|
// Commit
|
|
244
252
|
const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
|
|
245
|
-
const commitResult = execGit(cwd, commitArgs);
|
|
253
|
+
const commitResult = await execGit(cwd, commitArgs);
|
|
246
254
|
if (commitResult.exitCode !== 0) {
|
|
247
255
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
248
256
|
const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
|
|
@@ -255,7 +263,7 @@ function cmdCommit(cwd, message, files, raw, amend) {
|
|
|
255
263
|
}
|
|
256
264
|
|
|
257
265
|
// Get short hash
|
|
258
|
-
const hashResult = execGit(cwd, ['rev-parse', '--short', 'HEAD']);
|
|
266
|
+
const hashResult = await execGit(cwd, ['rev-parse', '--short', 'HEAD']);
|
|
259
267
|
const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
260
268
|
const result = { committed: true, hash, reason: 'committed' };
|
|
261
269
|
output(result, raw, hash || 'committed');
|
|
@@ -375,6 +383,7 @@ async function cmdWebsearch(query, options, raw) {
|
|
|
375
383
|
results
|
|
376
384
|
}, raw, results.map(r => `${r.title}\n${r.url}\n${r.description}`).join('\n\n'));
|
|
377
385
|
} catch (err) {
|
|
386
|
+
logger.warn('Websearch request failed in cmdWebsearch', { query, error: err.message });
|
|
378
387
|
output({ available: false, error: err.message }, raw, '');
|
|
379
388
|
}
|
|
380
389
|
}
|
|
@@ -411,7 +420,9 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
411
420
|
|
|
412
421
|
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
|
|
413
422
|
}
|
|
414
|
-
} catch {
|
|
423
|
+
} catch (err) {
|
|
424
|
+
logger.warn('Failed to enumerate phase directories in cmdProgressRender', { phasesDir, error: err.message });
|
|
425
|
+
}
|
|
415
426
|
|
|
416
427
|
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
|
417
428
|
|
|
@@ -492,7 +503,7 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
492
503
|
switch (type) {
|
|
493
504
|
case 'context': {
|
|
494
505
|
filePath = path.join(phaseDir, `${padded}-CONTEXT.md`);
|
|
495
|
-
content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during /
|
|
506
|
+
content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during /ez-discuss-phase ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
|
|
496
507
|
break;
|
|
497
508
|
}
|
|
498
509
|
case 'uat': {
|
|
@@ -532,7 +543,7 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
532
543
|
output({ created: true, path: relPath }, raw, relPath);
|
|
533
544
|
}
|
|
534
545
|
|
|
535
|
-
function cmdStats(cwd, format, raw) {
|
|
546
|
+
async function cmdStats(cwd, format, raw) {
|
|
536
547
|
const phasesDir = path.join(cwd, '.planning', 'phases');
|
|
537
548
|
const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
|
|
538
549
|
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
|
@@ -566,7 +577,9 @@ function cmdStats(cwd, format, raw) {
|
|
|
566
577
|
|
|
567
578
|
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
|
|
568
579
|
}
|
|
569
|
-
} catch {
|
|
580
|
+
} catch (err) {
|
|
581
|
+
logger.warn('Failed to enumerate phase directories in cmdStats', { phasesDir, error: err.message });
|
|
582
|
+
}
|
|
570
583
|
|
|
571
584
|
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
|
572
585
|
|
|
@@ -581,7 +594,9 @@ function cmdStats(cwd, format, raw) {
|
|
|
581
594
|
requirementsComplete = checked ? checked.length : 0;
|
|
582
595
|
requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
|
|
583
596
|
}
|
|
584
|
-
} catch {
|
|
597
|
+
} catch (err) {
|
|
598
|
+
logger.warn('Failed to parse REQUIREMENTS.md in cmdStats', { reqPath, error: err.message });
|
|
599
|
+
}
|
|
585
600
|
|
|
586
601
|
// Last activity from STATE.md
|
|
587
602
|
let lastActivity = null;
|
|
@@ -591,17 +606,21 @@ function cmdStats(cwd, format, raw) {
|
|
|
591
606
|
const activityMatch = stateContent.match(/\*\*Last Activity:\*\*\s*(.+)/);
|
|
592
607
|
if (activityMatch) lastActivity = activityMatch[1].trim();
|
|
593
608
|
}
|
|
594
|
-
} catch {
|
|
609
|
+
} catch (err) {
|
|
610
|
+
logger.warn('Failed to read STATE.md in cmdStats', { statePath, error: err.message });
|
|
611
|
+
}
|
|
595
612
|
|
|
596
613
|
// Git stats
|
|
597
614
|
let gitCommits = 0;
|
|
598
615
|
let gitFirstCommitDate = null;
|
|
599
616
|
try {
|
|
600
|
-
const commitCount = execGit(cwd, ['rev-list', '--count', 'HEAD']);
|
|
617
|
+
const commitCount = await execGit(cwd, ['rev-list', '--count', 'HEAD']);
|
|
601
618
|
gitCommits = parseInt(commitCount.trim(), 10) || 0;
|
|
602
|
-
const firstDate = execGit(cwd, ['log', '--reverse', '--format=%as', '--max-count=1']);
|
|
619
|
+
const firstDate = await execGit(cwd, ['log', '--reverse', '--format=%as', '--max-count=1']);
|
|
603
620
|
gitFirstCommitDate = firstDate.trim() || null;
|
|
604
|
-
} catch {
|
|
621
|
+
} catch (err) {
|
|
622
|
+
logger.warn('Failed to compute git stats in cmdStats', { cwd, error: err.message });
|
|
623
|
+
}
|
|
605
624
|
|
|
606
625
|
const completedPhases = phases.filter(p => p.status === 'Complete').length;
|
|
607
626
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { output, error } = require('./core.cjs');
|
|
8
|
+
const { safePlanningWriteSync } = require('./planning-write.cjs');
|
|
8
9
|
|
|
9
10
|
const VALID_CONFIG_KEYS = new Set([
|
|
10
11
|
'mode', 'granularity', 'parallelization', 'commit_docs', 'model_profile',
|
|
@@ -36,16 +37,22 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
// Detect Brave Search API key availability
|
|
40
|
+
// Detect Brave Search API key availability (prefer ~/.ez)
|
|
40
41
|
const homedir = require('os').homedir();
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const braveKeyCandidates = [
|
|
43
|
+
path.join(homedir, '.ez', 'brave_api_key'),
|
|
44
|
+
];
|
|
45
|
+
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || braveKeyCandidates.some(p => fs.existsSync(p)));
|
|
46
|
+
|
|
47
|
+
// Load user-level defaults from ~/.ez/defaults.json
|
|
48
|
+
const defaultsCandidates = [
|
|
49
|
+
path.join(homedir, '.ez', 'defaults.json'),
|
|
50
|
+
];
|
|
51
|
+
const existingDefaultsPath = defaultsCandidates.find(p => fs.existsSync(p));
|
|
52
|
+
const globalDefaultsPath = existingDefaultsPath || defaultsCandidates[0];
|
|
46
53
|
let userDefaults = {};
|
|
47
54
|
try {
|
|
48
|
-
if (
|
|
55
|
+
if (existingDefaultsPath) {
|
|
49
56
|
userDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf-8'));
|
|
50
57
|
// Migrate deprecated "depth" key to "granularity"
|
|
51
58
|
if ('depth' in userDefaults && !('granularity' in userDefaults)) {
|
|
@@ -65,8 +72,8 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
65
72
|
commit_docs: true,
|
|
66
73
|
search_gitignored: false,
|
|
67
74
|
branching_strategy: 'none',
|
|
68
|
-
phase_branch_template: '
|
|
69
|
-
milestone_branch_template: '
|
|
75
|
+
phase_branch_template: 'ez/phase-{phase}-{slug}',
|
|
76
|
+
milestone_branch_template: 'ez/{milestone}-{slug}',
|
|
70
77
|
workflow: {
|
|
71
78
|
research: true,
|
|
72
79
|
plan_check: true,
|
|
@@ -83,7 +90,7 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
83
90
|
};
|
|
84
91
|
|
|
85
92
|
try {
|
|
86
|
-
|
|
93
|
+
safePlanningWriteSync(configPath, JSON.stringify(defaults, null, 2));
|
|
87
94
|
const result = { created: true, path: '.planning/config.json' };
|
|
88
95
|
output(result, raw, 'created');
|
|
89
96
|
} catch (err) {
|
|
@@ -132,7 +139,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
132
139
|
|
|
133
140
|
// Write back
|
|
134
141
|
try {
|
|
135
|
-
|
|
142
|
+
safePlanningWriteSync(configPath, JSON.stringify(config, null, 2));
|
|
136
143
|
const result = { updated: true, key: keyPath, value: parsedValue };
|
|
137
144
|
output(result, raw, `${keyPath}=${parsedValue}`);
|
|
138
145
|
} catch (err) {
|