@matt82198/aesop 0.1.0-beta.4 → 0.1.0-rc.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/CHANGELOG.md +187 -5
- package/LICENSE +66 -21
- package/README.md +143 -23
- package/aesop.config.example.json +14 -1
- package/bin/CLAUDE.md +42 -5
- package/bin/cli.js +479 -95
- package/daemons/CLAUDE.md +5 -2
- package/daemons/backup-fleet.sh +209 -51
- package/daemons/run-watchdog.sh +211 -62
- package/dash/dash-extra.mjs +73 -4
- package/dash/watchdog-gui.sh +41 -35
- package/docs/ARCHITECTURE.md +296 -0
- package/docs/CONCEPTS.md +254 -0
- package/docs/CONFIGURE.md +256 -0
- package/docs/FIRST-WAVE.md +276 -0
- package/docs/INSTALL.md +224 -0
- package/docs/README.md +176 -27
- package/docs/RELEASING.md +159 -0
- package/docs/archive/README.md +3 -0
- package/docs/{spikes → archive/spikes}/tiered-cognition/README.md +1 -3
- package/docs/autonomous-swe.md +149 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/reproduce.md +121 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +51 -0
- package/hooks/install-waveguard.sh +68 -0
- package/hooks/pre-commit-waveguard.sh +26 -0
- package/hooks/pre-push-policy.sh +253 -15
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/CHARTER.md +76 -110
- package/monitor/CLAUDE.md +3 -3
- package/monitor/collect-signals.mjs +218 -20
- package/package.json +17 -6
- package/scan/CLAUDE.md +30 -0
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +1 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +41 -0
- package/state_store/projections.py +126 -0
- package/state_store/store.py +197 -0
- package/tools/CLAUDE.md +37 -149
- package/tools/alert_bridge.py +453 -0
- package/tools/bench_runner.py +438 -0
- package/tools/buildlog.py +4 -7
- package/tools/ci_merge_wait.py +308 -14
- package/tools/common.py +62 -0
- package/tools/cost_ceiling.py +191 -0
- package/tools/dash.js +102 -0
- package/tools/doctor.js +220 -0
- package/tools/fleet_ledger.py +189 -17
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/halt.py +172 -0
- package/tools/healthcheck.py +299 -0
- package/tools/heartbeat.py +4 -7
- package/tools/metrics_gate.py +8 -2
- package/tools/orchestrator_status.py +4 -9
- package/tools/reconcile.py +277 -0
- package/tools/rotate_logs.py +152 -62
- package/tools/scanner_selftest.py +28 -3
- package/tools/secret_scan.py +367 -91
- package/tools/self_stats.py +797 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/status.js +187 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_agent_inspector.py +289 -0
- package/tools/verify_dash.py +361 -542
- package/tools/verify_prboard.py +344 -0
- package/tools/verify_submit_encoding.py +12 -4
- package/tools/watch.js +49 -0
- package/ui/CLAUDE.md +59 -41
- package/ui/agents.py +337 -34
- package/ui/api/submit.py +44 -5
- package/ui/api/tracker.py +15 -7
- package/ui/collectors.py +294 -52
- package/ui/config.py +31 -2
- package/ui/cost.py +355 -0
- package/ui/csrf.py +9 -4
- package/ui/handler.py +450 -12
- package/ui/render.py +26 -6
- package/ui/sse.py +114 -13
- package/ui/wave_prs.py +230 -0
- package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
- package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/monitor/.signal-state.json +0 -3
- package/monitor/ACTIONS.log +0 -1
- package/monitor/BRIEF.md +0 -24
- package/monitor/SIGNALS.json +0 -54
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/templates/dashboard.html +0 -1202
- /package/docs/{spikes → archive/spikes}/tiered-cognition/ACTIVATION.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/DESIGN.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/FINDINGS.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/aesop-cognition.example.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/force-model-policy.merged.mjs +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/strip-tools-hook.mjs +0 -0
package/bin/cli.js
CHANGED
|
@@ -3,10 +3,73 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
6
8
|
|
|
7
9
|
const args = process.argv.slice(2);
|
|
8
10
|
const helpFlag = args.includes('--help') || args.includes('-h');
|
|
9
11
|
const forceFlag = args.includes('--force');
|
|
12
|
+
const yesFlag = args.includes('--yes');
|
|
13
|
+
|
|
14
|
+
// Check for runtime subcommands (doctor, watch, dash, status)
|
|
15
|
+
const runtimeCommands = ['doctor', 'watch', 'dash', 'status'];
|
|
16
|
+
const isRuntimeCommand = runtimeCommands.includes(args[0]);
|
|
17
|
+
|
|
18
|
+
if (isRuntimeCommand) {
|
|
19
|
+
const commandMap = {
|
|
20
|
+
'doctor': '../tools/doctor.js',
|
|
21
|
+
'watch': '../tools/watch.js',
|
|
22
|
+
'dash': '../tools/dash.js',
|
|
23
|
+
'status': '../tools/status.js'
|
|
24
|
+
};
|
|
25
|
+
// Load and run the appropriate runtime module
|
|
26
|
+
// These modules run async code that sets process.exitCode and will cause Node to exit
|
|
27
|
+
require(commandMap[args[0]]);
|
|
28
|
+
// Return immediately to prevent further CLI scaffolding logic from running
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Detect if stdin is a TTY (interactive terminal)
|
|
33
|
+
function isInteractive() {
|
|
34
|
+
return process.stdin.isTTY && process.stdout.isTTY;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Discover git repositories under a base directory (non-recursive first level)
|
|
38
|
+
function discoverRepos(baseDir) {
|
|
39
|
+
const repos = [];
|
|
40
|
+
try {
|
|
41
|
+
if (!fs.existsSync(baseDir)) return repos;
|
|
42
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
45
|
+
const fullPath = path.join(baseDir, entry.name);
|
|
46
|
+
const gitDir = path.join(fullPath, '.git');
|
|
47
|
+
if (fs.existsSync(gitDir)) {
|
|
48
|
+
repos.push({ name: entry.name, path: fullPath });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// Silently ignore discovery errors
|
|
54
|
+
}
|
|
55
|
+
return repos;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Interactive prompt via readline (exported for testing)
|
|
59
|
+
async function promptUser(rl, prompt, defaultValue) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const displayPrompt = defaultValue ? `${prompt} (${defaultValue}): ` : `${prompt}: `;
|
|
62
|
+
rl.question(displayPrompt, (answer) => {
|
|
63
|
+
resolve(answer.trim() || defaultValue || '');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Validate port is a number
|
|
69
|
+
function validatePort(port) {
|
|
70
|
+
const p = parseInt(port, 10);
|
|
71
|
+
return !isNaN(p) && p > 0 && p < 65536 ? p : null;
|
|
72
|
+
}
|
|
10
73
|
|
|
11
74
|
// Parse named flags
|
|
12
75
|
function getFlag(flagName) {
|
|
@@ -23,6 +86,18 @@ aesop — Multi-agent orchestration template scaffolder
|
|
|
23
86
|
|
|
24
87
|
Usage:
|
|
25
88
|
npx @matt82198/aesop [target-dir] [options]
|
|
89
|
+
npx @matt82198/aesop wizard [options]
|
|
90
|
+
npx @matt82198/aesop doctor
|
|
91
|
+
npx @matt82198/aesop watch
|
|
92
|
+
npx @matt82198/aesop dash
|
|
93
|
+
npx @matt82198/aesop status
|
|
94
|
+
|
|
95
|
+
Commands:
|
|
96
|
+
doctor Preflight readiness check (Node.js, Python, git, config, dirs, hook, port)
|
|
97
|
+
watch Launch the watchdog daemon (spawns daemons/run-watchdog.sh)
|
|
98
|
+
dash Launch the web dashboard (spawns python3 ui/serve.py or python fallback)
|
|
99
|
+
status One-shot fleet status snapshot (heartbeats, dashboard port, git branch)
|
|
100
|
+
wizard Interactive onboarding (prompts for project name, repos, port)
|
|
26
101
|
|
|
27
102
|
Arguments:
|
|
28
103
|
target-dir Directory to scaffold the template into (default: "aesop-fleet")
|
|
@@ -30,13 +105,20 @@ Arguments:
|
|
|
30
105
|
Options:
|
|
31
106
|
--help, -h Show this help message
|
|
32
107
|
--force Replace any existing .git/hooks/pre-push during scaffold
|
|
108
|
+
--yes Skip interactive prompts, use defaults (CI-safe)
|
|
33
109
|
--name <name> Project name (for headless scaffolding; generates CLAUDE.md + aesop.config.json)
|
|
34
110
|
--domains <list> Comma-separated domain list (e.g., "api,worker,monitoring")
|
|
35
111
|
--repos <paths> Comma-separated repo paths (e.g., "/path/to/repo1,/path/to/repo2")
|
|
112
|
+
--repo-urls <urls> Comma-separated repo URLs (e.g., "https://github.com/user/repo1.git,https://github.com/user/repo2.git")
|
|
36
113
|
|
|
37
114
|
Examples:
|
|
115
|
+
npx @matt82198/aesop doctor # Run preflight checks before starting
|
|
116
|
+
npx @matt82198/aesop watch # Launch watchdog daemon
|
|
117
|
+
npx @matt82198/aesop dash # Launch web dashboard (default localhost:8770)
|
|
118
|
+
npx @matt82198/aesop status # Show fleet status (heartbeats, port, git)
|
|
38
119
|
npx @matt82198/aesop # Creates ./aesop-fleet/ with template
|
|
39
120
|
npx @matt82198/aesop my-fleet # Creates ./my-fleet/ with template
|
|
121
|
+
npx @matt82198/aesop wizard # Interactive onboarding (60-second setup)
|
|
40
122
|
npx @matt82198/aesop my-fleet --force # Re-scaffold and replace hooks
|
|
41
123
|
npx @matt82198/aesop orchestrator --name "my-service" # Headless: generates CLAUDE.md + config
|
|
42
124
|
|
|
@@ -46,19 +128,46 @@ Examples:
|
|
|
46
128
|
Full headless with domains and repos (PowerShell):
|
|
47
129
|
npx @matt82198/aesop orchestrator --name "api" --domains "server,worker" --repos "C:\path\to\api"
|
|
48
130
|
|
|
49
|
-
|
|
131
|
+
Interactive wizard flow:
|
|
132
|
+
1. Run: npx @matt82198/aesop wizard
|
|
133
|
+
2. Answer prompts (press Enter for defaults): project name, repos to watch, port, brain root
|
|
134
|
+
3. Scaffolds template, writes aesop.config.json, prints next steps
|
|
135
|
+
4. Optionally runs watchdog smoke test
|
|
136
|
+
|
|
137
|
+
After scaffolding, cd into the directory and:
|
|
50
138
|
1. Review CLAUDE.md (pre-filled with your project info)
|
|
51
139
|
2. Review aesop.config.json (pre-configured for your repos)
|
|
52
|
-
3. Run: bash daemons/run-watchdog.sh
|
|
53
|
-
4. Launch dashboard:
|
|
140
|
+
3. Run: aesop watch (or bash daemons/run-watchdog.sh)
|
|
141
|
+
4. Launch dashboard: aesop dash (or python3 ui/serve.py)
|
|
54
142
|
`);
|
|
55
143
|
process.exit(0);
|
|
56
144
|
}
|
|
57
145
|
|
|
146
|
+
// Check if wizard mode is requested (either as first arg or after targetDir)
|
|
147
|
+
// e.g., "aesop wizard" or "aesop my-dir wizard"
|
|
148
|
+
let wizardModeRequested = false;
|
|
149
|
+
let wizardArgIndex = -1;
|
|
150
|
+
|
|
151
|
+
// Check if 'wizard' is in the args
|
|
152
|
+
for (let i = 0; i < args.length; i++) {
|
|
153
|
+
if (args[i] === 'wizard') {
|
|
154
|
+
wizardModeRequested = true;
|
|
155
|
+
wizardArgIndex = i;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const wizardMode = wizardModeRequested || (args.length === 0 && isInteractive());
|
|
161
|
+
|
|
162
|
+
// Handle wizard mode by removing 'wizard' from args
|
|
163
|
+
if (wizardModeRequested && wizardArgIndex >= 0) {
|
|
164
|
+
args.splice(wizardArgIndex, 1);
|
|
165
|
+
}
|
|
166
|
+
|
|
58
167
|
// Extract targetDir (first non-flag argument, excluding flag values)
|
|
59
|
-
// Build set of indices consumed as flag values (tokens after --name/--domains/--repos)
|
|
168
|
+
// Build set of indices consumed as flag values (tokens after --name/--domains/--repos/--repo-urls)
|
|
60
169
|
const consumedIndices = new Set();
|
|
61
|
-
const flagsWithValues = ['--name', '--domains', '--repos'];
|
|
170
|
+
const flagsWithValues = ['--name', '--domains', '--repos', '--repo-urls'];
|
|
62
171
|
for (let i = 0; i < args.length; i++) {
|
|
63
172
|
if (flagsWithValues.includes(args[i]) && i + 1 < args.length) {
|
|
64
173
|
consumedIndices.add(i + 1);
|
|
@@ -75,12 +184,13 @@ const targetDir = args.find((arg, idx) =>
|
|
|
75
184
|
const projectName = getFlag('--name');
|
|
76
185
|
const domainsStr = getFlag('--domains');
|
|
77
186
|
const reposStr = getFlag('--repos');
|
|
187
|
+
const repoUrlsStr = getFlag('--repo-urls');
|
|
78
188
|
|
|
79
189
|
// Validate target directory doesn't exist or is empty (except for .git and aesop files)
|
|
80
190
|
if (fs.existsSync(targetDir)) {
|
|
81
191
|
const contents = fs.readdirSync(targetDir);
|
|
82
192
|
// Allow .git and aesop scaffolded files to already exist (for idempotency)
|
|
83
|
-
const aesopDirs = ['daemons', 'dash', 'monitor', 'tools', 'ui', 'docs', '.git', 'state'];
|
|
193
|
+
const aesopDirs = ['daemons', 'dash', 'monitor', 'tools', 'ui', 'docs', 'state_store', 'skills', 'mcp', 'scan', 'hooks', '.git', 'state'];
|
|
84
194
|
const aesopFiles = [
|
|
85
195
|
'aesop.config.example.json',
|
|
86
196
|
'aesop.config.json',
|
|
@@ -133,6 +243,11 @@ const filesToCopy = [
|
|
|
133
243
|
'tools',
|
|
134
244
|
'ui',
|
|
135
245
|
'docs',
|
|
246
|
+
'state_store',
|
|
247
|
+
'skills',
|
|
248
|
+
'mcp',
|
|
249
|
+
'scan',
|
|
250
|
+
'hooks',
|
|
136
251
|
'aesop.config.example.json',
|
|
137
252
|
'README.md',
|
|
138
253
|
'LICENSE',
|
|
@@ -199,7 +314,7 @@ function substituteTemplate(templateContent, projectName, domainsStr, reposStr)
|
|
|
199
314
|
return result;
|
|
200
315
|
}
|
|
201
316
|
|
|
202
|
-
function generateConfigJson(targetDir, templateRoot, projectName, reposStr) {
|
|
317
|
+
function generateConfigJson(targetDir, templateRoot, projectName, reposStr, repoUrlsStr) {
|
|
203
318
|
// Read example config
|
|
204
319
|
const exampleConfigPath = path.join(templateRoot, 'aesop.config.example.json');
|
|
205
320
|
let exampleConfig;
|
|
@@ -214,6 +329,7 @@ function generateConfigJson(targetDir, templateRoot, projectName, reposStr) {
|
|
|
214
329
|
brain_root: path.join(os.homedir(), '.claude'),
|
|
215
330
|
scripts_root: path.join(os.homedir(), 'scripts'),
|
|
216
331
|
temp_root: path.join(os.tmpdir()),
|
|
332
|
+
fleet_root: os.homedir(),
|
|
217
333
|
repos: [],
|
|
218
334
|
watchdog: {
|
|
219
335
|
cycle_seconds: 150,
|
|
@@ -249,23 +365,190 @@ function generateConfigJson(targetDir, templateRoot, projectName, reposStr) {
|
|
|
249
365
|
exampleConfig.brain_root = '~/.claude';
|
|
250
366
|
exampleConfig.scripts_root = '~/scripts';
|
|
251
367
|
exampleConfig.temp_root = '~/.aesop-temp';
|
|
368
|
+
exampleConfig.fleet_root = os.homedir();
|
|
252
369
|
exampleConfig._generated_note = 'This config uses portable ~ paths which expand at runtime on all platforms. Config loaders in ui/config.py (Python) and monitor/collect-signals.mjs (Node.js) automatically expand ~ paths to your home directory.';
|
|
253
370
|
|
|
254
371
|
// Parse repos if provided
|
|
255
372
|
if (reposStr) {
|
|
256
373
|
const repos = reposStr.split(',').map(r => r.trim()).filter(r => r);
|
|
374
|
+
const repoUrls = repoUrlsStr ? repoUrlsStr.split(',').map(u => u.trim()).filter(u => u) : [];
|
|
375
|
+
|
|
257
376
|
exampleConfig.repos = repos.map((repoPath, idx) => ({
|
|
258
377
|
path: repoPath,
|
|
259
378
|
name: path.basename(repoPath),
|
|
260
|
-
url: `https://github.com/user/${path.basename(repoPath)}.git`,
|
|
379
|
+
url: repoUrls[idx] || `https://github.com/user/${path.basename(repoPath)}.git`,
|
|
261
380
|
primary_branch: 'main',
|
|
262
381
|
backup_branch: 'backup/wip'
|
|
263
382
|
}));
|
|
383
|
+
|
|
384
|
+
// Add _repos_note if URLs weren't provided (user must edit them)
|
|
385
|
+
if (!repoUrlsStr) {
|
|
386
|
+
exampleConfig._repos_note = 'Repository URLs are currently placeholders (https://github.com/user/<name>.git). Update each repo.url in the repos array with the actual repository URL. This is a required manual step before the daemon can clone repositories.';
|
|
387
|
+
}
|
|
264
388
|
}
|
|
265
389
|
|
|
266
390
|
return exampleConfig;
|
|
267
391
|
}
|
|
268
392
|
|
|
393
|
+
// Print next steps and optionally run watchdog (returns a Promise)
|
|
394
|
+
async function printNextStepsAndWatchdog(rl, targetDir, configPath, port) {
|
|
395
|
+
console.log('\n🎯 Next 3 commands to get started:\n');
|
|
396
|
+
console.log(` 1. cd ${targetDir}`);
|
|
397
|
+
console.log(' 2. bash daemons/run-watchdog.sh --once (one-time watchdog smoke test)');
|
|
398
|
+
console.log(` 3. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:${port})`);
|
|
399
|
+
console.log('\n📖 After that, review:');
|
|
400
|
+
console.log(' • CLAUDE.md (pre-filled with your project info)');
|
|
401
|
+
console.log(' • aesop.config.json (pre-configured for your repos)');
|
|
402
|
+
console.log(' • docs/MEMORY-TEMPLATE.md (edit ~/.claude/MEMORY.md with your facts)\n');
|
|
403
|
+
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
rl.question('Run watchdog --once now? (y/N): ', (answer) => {
|
|
406
|
+
if (answer.toLowerCase() === 'y') {
|
|
407
|
+
console.log('\nRunning watchdog smoke test...');
|
|
408
|
+
try {
|
|
409
|
+
const watchdogScript = path.join(targetDir, 'daemons', 'run-watchdog.sh');
|
|
410
|
+
if (fs.existsSync(watchdogScript)) {
|
|
411
|
+
// Use bash to run the script
|
|
412
|
+
execSync(`bash "${watchdogScript}" --once`, { stdio: 'inherit', cwd: targetDir });
|
|
413
|
+
console.log('\n✓ Watchdog smoke test completed');
|
|
414
|
+
}
|
|
415
|
+
} catch (e) {
|
|
416
|
+
console.error('\n⚠ Watchdog test failed (this is OK, continue manually)');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
rl.close();
|
|
420
|
+
resolve();
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function installPreCommitWaveguard(targetDir, templateRoot) {
|
|
426
|
+
// Install the pre-commit waveguard hook to prevent commits during a wave
|
|
427
|
+
// Try to locate .git directory
|
|
428
|
+
const gitDir = path.join(targetDir, '.git');
|
|
429
|
+
if (!fs.existsSync(gitDir)) {
|
|
430
|
+
// No git repo, skip hook installation silently
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// SECURITY: Check if .git itself is a symlink/junction (refuse to follow it outside targetDir)
|
|
435
|
+
try {
|
|
436
|
+
const gitDirLstat = fs.lstatSync(gitDir);
|
|
437
|
+
if (gitDirLstat.isSymbolicLink()) {
|
|
438
|
+
console.warn('⚠ Warning: .git is a symlink (security risk)');
|
|
439
|
+
console.warn(' Skipping pre-commit hook installation. Please remove the symlink and re-run scaffold.');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
} catch (e) {
|
|
443
|
+
// lstat failed; proceed (may be a permission issue)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Ensure hooks directory exists
|
|
447
|
+
const gitHooksDir = path.join(gitDir, 'hooks');
|
|
448
|
+
if (!fs.existsSync(gitHooksDir)) {
|
|
449
|
+
fs.mkdirSync(gitHooksDir, { recursive: true });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// SECURITY: Check if gitHooksDir is a symlink (refuse to install through symlinked dir)
|
|
453
|
+
try {
|
|
454
|
+
const hooksLstat = fs.lstatSync(gitHooksDir);
|
|
455
|
+
if (hooksLstat.isSymbolicLink()) {
|
|
456
|
+
console.warn('⚠ Warning: .git/hooks directory is a symlink (security risk)');
|
|
457
|
+
console.warn(' Skipping pre-commit hook installation. Please remove the symlink and re-run scaffold.');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
} catch (e) {
|
|
461
|
+
// lstat failed; proceed (may be a permission issue)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const waveguardSource = path.join(templateRoot, 'hooks', 'pre-commit-waveguard.sh');
|
|
465
|
+
const preCommitDest = path.join(gitHooksDir, 'pre-commit');
|
|
466
|
+
|
|
467
|
+
if (!fs.existsSync(waveguardSource)) {
|
|
468
|
+
// Waveguard source doesn't exist, skip
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Check if pre-commit hook already exists
|
|
473
|
+
if (fs.existsSync(preCommitDest)) {
|
|
474
|
+
// SECURITY: Check if existing pre-commit is a symlink (refuse to follow it)
|
|
475
|
+
try {
|
|
476
|
+
const destLstat = fs.lstatSync(preCommitDest);
|
|
477
|
+
if (destLstat.isSymbolicLink()) {
|
|
478
|
+
console.warn('⚠ Warning: Existing .git/hooks/pre-commit is a symlink (security risk)');
|
|
479
|
+
console.warn(' Skipping pre-commit hook installation. Please remove the symlink and re-run scaffold.');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
} catch (e) {
|
|
483
|
+
// lstat failed; proceed
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Backup existing pre-commit hook
|
|
487
|
+
const backupPath = path.join(gitHooksDir, 'pre-commit.waveguard-backup');
|
|
488
|
+
if (!fs.existsSync(backupPath)) {
|
|
489
|
+
// Only backup if we haven't already
|
|
490
|
+
try {
|
|
491
|
+
const existingContent = fs.readFileSync(preCommitDest, 'utf8');
|
|
492
|
+
// Check if waveguard is already in the hook
|
|
493
|
+
if (existingContent.includes('pre-commit-waveguard')) {
|
|
494
|
+
console.log('✓ Pre-commit waveguard already installed (no changes)');
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Backup the existing hook
|
|
498
|
+
fs.copyFileSync(preCommitDest, backupPath);
|
|
499
|
+
console.log('✓ Backed up existing pre-commit hook to .git/hooks/pre-commit.waveguard-backup');
|
|
500
|
+
} catch (e) {
|
|
501
|
+
// Failed to backup, still try to install
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Create wrapper script that calls the waveguard hook
|
|
507
|
+
const wrapperScript = `#!/usr/bin/env bash
|
|
508
|
+
set -uo pipefail
|
|
509
|
+
|
|
510
|
+
repo_root=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
511
|
+
if [ -z "$repo_root" ]; then
|
|
512
|
+
exit 1
|
|
513
|
+
fi
|
|
514
|
+
|
|
515
|
+
waveguard_hook="$repo_root/hooks/pre-commit-waveguard.sh"
|
|
516
|
+
if [ -f "$waveguard_hook" ]; then
|
|
517
|
+
bash "$waveguard_hook"
|
|
518
|
+
waveguard_exit=$?
|
|
519
|
+
if [ $waveguard_exit -ne 0 ]; then
|
|
520
|
+
exit $waveguard_exit
|
|
521
|
+
fi
|
|
522
|
+
fi
|
|
523
|
+
|
|
524
|
+
backup_hook="$repo_root/.git/hooks/pre-commit.waveguard-backup"
|
|
525
|
+
if [ -f "$backup_hook" ] && [ -x "$backup_hook" ]; then
|
|
526
|
+
bash "$backup_hook"
|
|
527
|
+
exit $?
|
|
528
|
+
fi
|
|
529
|
+
|
|
530
|
+
exit 0
|
|
531
|
+
`;
|
|
532
|
+
|
|
533
|
+
// Install the hook using copyFileSync (not symlink) to avoid dangling links
|
|
534
|
+
if (fs.existsSync(preCommitDest)) {
|
|
535
|
+
fs.unlinkSync(preCommitDest);
|
|
536
|
+
}
|
|
537
|
+
fs.writeFileSync(preCommitDest, wrapperScript);
|
|
538
|
+
console.log('✓ Installed pre-commit waveguard hook to .git/hooks/pre-commit');
|
|
539
|
+
|
|
540
|
+
// Ensure hook is executable
|
|
541
|
+
try {
|
|
542
|
+
fs.chmodSync(preCommitDest, 0o755);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
// On Windows, chmod fails harmlessly; on POSIX, warn the user to chmod manually
|
|
545
|
+
if (process.platform !== 'win32') {
|
|
546
|
+
console.warn('⚠ Warning: Failed to chmod +x the pre-commit hook. Please run manually:');
|
|
547
|
+
console.warn(` chmod +x "${preCommitDest}"`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
269
552
|
function installPrePushHook(targetDir, templateRoot) {
|
|
270
553
|
// Try to locate .git directory
|
|
271
554
|
const gitDir = path.join(targetDir, '.git');
|
|
@@ -350,107 +633,208 @@ function installPrePushHook(targetDir, templateRoot) {
|
|
|
350
633
|
}
|
|
351
634
|
|
|
352
635
|
// Install the hook
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
} else {
|
|
358
|
-
// Unix: symlink for easy updates
|
|
359
|
-
// First remove if exists
|
|
360
|
-
if (fs.existsSync(hookDest)) {
|
|
361
|
-
fs.unlinkSync(hookDest);
|
|
362
|
-
}
|
|
363
|
-
// Create symlink relative to .git/hooks/
|
|
364
|
-
const relPath = path.relative(gitHooksDir, hookSource);
|
|
365
|
-
fs.symlinkSync(relPath, hookDest);
|
|
366
|
-
console.log('✓ Symlinked pre-push policy hook to .git/hooks/pre-push');
|
|
636
|
+
// Use copyFileSync on all platforms to avoid dangling symlinks after npx cache clean
|
|
637
|
+
// which would disable branch protection and the secret gate
|
|
638
|
+
if (fs.existsSync(hookDest)) {
|
|
639
|
+
fs.unlinkSync(hookDest);
|
|
367
640
|
}
|
|
641
|
+
fs.copyFileSync(hookSource, hookDest);
|
|
642
|
+
console.log('✓ Copied pre-push policy hook to .git/hooks/pre-push');
|
|
368
643
|
|
|
369
644
|
// Ensure hook is executable
|
|
370
645
|
try {
|
|
371
646
|
fs.chmodSync(hookDest, 0o755);
|
|
372
647
|
} catch (e) {
|
|
373
|
-
// On Windows, chmod
|
|
648
|
+
// On Windows, chmod fails harmlessly; on POSIX, warn the user to chmod manually
|
|
649
|
+
if (process.platform !== 'win32') {
|
|
650
|
+
console.warn('⚠ Warning: Failed to chmod +x the pre-push hook. Please run manually:');
|
|
651
|
+
console.warn(` chmod +x "${hookDest}"`);
|
|
652
|
+
}
|
|
374
653
|
}
|
|
375
654
|
}
|
|
376
655
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
656
|
+
// Main execution function (skip if runtime command detected)
|
|
657
|
+
if (!isRuntimeCommand) {
|
|
658
|
+
(async () => {
|
|
659
|
+
try {
|
|
660
|
+
let finalProjectName = projectName;
|
|
661
|
+
let finalReposStr = reposStr;
|
|
662
|
+
let finalDomainsStr = domainsStr;
|
|
663
|
+
let finalRepoUrlsStr = repoUrlsStr;
|
|
664
|
+
let finalTargetDir = targetDir;
|
|
665
|
+
let configPath = path.join(targetDir, 'aesop.config.json');
|
|
666
|
+
let dashboardPort = 8770;
|
|
667
|
+
|
|
668
|
+
let wizardRl = null;
|
|
669
|
+
|
|
670
|
+
// Handle wizard mode
|
|
671
|
+
if (wizardMode && isInteractive() && !yesFlag) {
|
|
672
|
+
wizardRl = readline.createInterface({
|
|
673
|
+
input: process.stdin,
|
|
674
|
+
output: process.stdout
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
console.log('\n🪄 Aesop interactive onboarding wizard');
|
|
678
|
+
console.log('═'.repeat(50));
|
|
679
|
+
console.log('Answer these 4 questions to set up your fleet in 60 seconds.');
|
|
680
|
+
console.log('Press Enter to accept defaults (shown in parentheses).\n');
|
|
681
|
+
|
|
682
|
+
// Q1: Project name
|
|
683
|
+
finalProjectName = await promptUser(wizardRl, 'Project name', 'my-fleet');
|
|
684
|
+
|
|
685
|
+
// Q2: Repos to watch
|
|
686
|
+
console.log('\nDiscovering git repos in your home directory...');
|
|
687
|
+
const discoveredRepos = discoverRepos(os.homedir());
|
|
688
|
+
let selectedRepos = '';
|
|
689
|
+
|
|
690
|
+
if (discoveredRepos.length > 0) {
|
|
691
|
+
console.log(`Found ${discoveredRepos.length} git repo(s):\n`);
|
|
692
|
+
discoveredRepos.forEach((repo, i) => {
|
|
693
|
+
console.log(` ${i + 1}. ${repo.name} (${repo.path})`);
|
|
694
|
+
});
|
|
695
|
+
console.log('\nEnter repo paths to watch (comma-separated), or press Enter to skip:');
|
|
696
|
+
selectedRepos = await promptUser(wizardRl, 'Repos', '');
|
|
697
|
+
if (!selectedRepos && discoveredRepos.length > 0) {
|
|
698
|
+
// Auto-select the first discovered repo if user presses Enter
|
|
699
|
+
selectedRepos = discoveredRepos[0].path;
|
|
700
|
+
console.log(` → Using: ${selectedRepos}`);
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
selectedRepos = await promptUser(wizardRl, 'Repos to watch (paths, comma-separated)', '');
|
|
704
|
+
}
|
|
705
|
+
finalReposStr = selectedRepos;
|
|
706
|
+
|
|
707
|
+
// Q3: Dashboard port
|
|
708
|
+
let port = '';
|
|
709
|
+
while (!port) {
|
|
710
|
+
port = await promptUser(wizardRl, 'Dashboard port', '8770');
|
|
711
|
+
const validPort = validatePort(port);
|
|
712
|
+
if (!validPort) {
|
|
713
|
+
console.log(' ✗ Invalid port. Must be a number between 1 and 65535.');
|
|
714
|
+
port = '';
|
|
715
|
+
} else {
|
|
716
|
+
dashboardPort = validPort;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Q4: Brain root
|
|
721
|
+
const brainRoot = await promptUser(wizardRl, 'Brain root directory', '~/.claude');
|
|
722
|
+
|
|
723
|
+
console.log('\n✨ Scaffolding your fleet...\n');
|
|
724
|
+
|
|
725
|
+
// Update config path for wizard-generated config
|
|
726
|
+
configPath = path.join(finalTargetDir, 'aesop.config.json');
|
|
727
|
+
} else if (yesFlag && wizardMode) {
|
|
728
|
+
// Non-interactive defaults for wizard mode with --yes
|
|
729
|
+
finalProjectName = 'my-fleet';
|
|
730
|
+
finalReposStr = '';
|
|
731
|
+
dashboardPort = 8770;
|
|
732
|
+
console.log('🪄 Running onboarding wizard with defaults (--yes)');
|
|
384
733
|
}
|
|
385
|
-
});
|
|
386
734
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
735
|
+
// Copy template files
|
|
736
|
+
filesToCopy.forEach(item => {
|
|
737
|
+
const src = path.join(templateRoot, item);
|
|
738
|
+
const dest = path.join(finalTargetDir, item);
|
|
739
|
+
if (fs.existsSync(src)) {
|
|
740
|
+
copyRecursive(src, dest);
|
|
741
|
+
console.log(`✓ Copied ${item}`);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
393
744
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (fs.existsSync(templatePath)) {
|
|
400
|
-
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
401
|
-
const claudeContent = substituteTemplate(templateContent, projectName, domainsStr, reposStr);
|
|
402
|
-
fs.writeFileSync(claudeMdPath, claudeContent);
|
|
403
|
-
console.log('✓ Generated CLAUDE.md (from template with substitutions)');
|
|
745
|
+
// Create state/ directory
|
|
746
|
+
const stateDir = path.join(finalTargetDir, 'state');
|
|
747
|
+
if (!fs.existsSync(stateDir)) {
|
|
748
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
749
|
+
console.log('✓ Created state/ directory');
|
|
404
750
|
}
|
|
405
751
|
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
752
|
+
// If --name or wizard provided, generate CLAUDE.md and aesop.config.json
|
|
753
|
+
if (finalProjectName) {
|
|
754
|
+
// Generate CLAUDE.md from template
|
|
755
|
+
const templatePath = path.join(finalTargetDir, 'CLAUDE-TEMPLATE.md');
|
|
756
|
+
const claudeMdPath = path.join(finalTargetDir, 'CLAUDE.md');
|
|
757
|
+
if (fs.existsSync(templatePath)) {
|
|
758
|
+
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
759
|
+
const claudeContent = substituteTemplate(templateContent, finalProjectName, finalDomainsStr, finalReposStr);
|
|
760
|
+
fs.writeFileSync(claudeMdPath, claudeContent);
|
|
761
|
+
console.log('✓ Generated CLAUDE.md (from template with substitutions)');
|
|
762
|
+
}
|
|
412
763
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
764
|
+
// Check if aesop.config.json already exists (for wizard mode)
|
|
765
|
+
if (fs.existsSync(configPath) && wizardMode && isInteractive()) {
|
|
766
|
+
// This should not happen in wizard mode since we're creating a new targetDir
|
|
767
|
+
// But if it does, warn and skip
|
|
768
|
+
console.warn(`⚠ Warning: aesop.config.json already exists at ${configPath}`);
|
|
769
|
+
} else {
|
|
770
|
+
// Generate aesop.config.json
|
|
771
|
+
const config = generateConfigJson(finalTargetDir, templateRoot, finalProjectName, finalReposStr, finalRepoUrlsStr);
|
|
772
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
773
|
+
console.log('✓ Generated aesop.config.json (configured for your repos)');
|
|
774
|
+
}
|
|
775
|
+
}
|
|
420
776
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
console.log(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
777
|
+
// Copy MEMORY-TEMPLATE.md as MEMORY-SEED.md
|
|
778
|
+
const memoryTemplatePath = path.join(templateRoot, 'docs', 'MEMORY-TEMPLATE.md');
|
|
779
|
+
const memorySeedPath = path.join(finalTargetDir, 'MEMORY-SEED.md');
|
|
780
|
+
if (fs.existsSync(memoryTemplatePath) && !fs.existsSync(memorySeedPath)) {
|
|
781
|
+
fs.copyFileSync(memoryTemplatePath, memorySeedPath);
|
|
782
|
+
console.log('✓ Copied MEMORY-SEED.md (template for your facts)');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Install the pre-push hook
|
|
786
|
+
installPrePushHook(finalTargetDir, templateRoot);
|
|
787
|
+
|
|
788
|
+
// Install the pre-commit waveguard hook
|
|
789
|
+
installPreCommitWaveguard(finalTargetDir, templateRoot);
|
|
790
|
+
|
|
791
|
+
console.log(`\n✅ Scaffolded aesop template into "${finalTargetDir}" (${copiedCount} files)`);
|
|
792
|
+
|
|
793
|
+
if (wizardMode && wizardRl) {
|
|
794
|
+
// Wizard mode: print next steps and offer to run watchdog
|
|
795
|
+
await printNextStepsAndWatchdog(wizardRl, finalTargetDir, configPath, dashboardPort);
|
|
796
|
+
process.exit(0);
|
|
797
|
+
} else if (wizardMode) {
|
|
798
|
+
// Wizard mode with --yes flag (non-interactive)
|
|
799
|
+
console.log('\n🎯 Next 3 commands to get started:\n');
|
|
800
|
+
console.log(` 1. cd ${finalTargetDir}`);
|
|
801
|
+
console.log(' 2. bash daemons/run-watchdog.sh --once (one-time watchdog smoke test)');
|
|
802
|
+
console.log(` 3. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:${dashboardPort})`);
|
|
803
|
+
console.log('\n📖 After that, review:');
|
|
804
|
+
console.log(' • CLAUDE.md (pre-filled with your project info)');
|
|
805
|
+
console.log(' • aesop.config.json (pre-configured for your repos)');
|
|
806
|
+
console.log(' • docs/MEMORY-TEMPLATE.md (edit ~/.claude/MEMORY.md with your facts)');
|
|
807
|
+
process.exit(0);
|
|
808
|
+
} else if (finalProjectName) {
|
|
809
|
+
console.log('\nHeadless scaffolding complete! Next steps:');
|
|
810
|
+
console.log(` 1. cd ${finalTargetDir}`);
|
|
811
|
+
console.log(' 2. Review CLAUDE.md (pre-filled with your project info)');
|
|
812
|
+
console.log(' 3. Review aesop.config.json (pre-configured for your repos)');
|
|
813
|
+
console.log('\nInitialize your brain (Claude Code team memory):');
|
|
814
|
+
console.log(' 4. mkdir -p ~/.claude/memory');
|
|
815
|
+
console.log(` 5. cp ${finalTargetDir}/CLAUDE.md ~/.claude/CLAUDE.md (or review existing ~/.claude/CLAUDE.md)`);
|
|
816
|
+
console.log(` 6. cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
|
|
817
|
+
console.log('\nRun the daemon and dashboard:');
|
|
818
|
+
console.log(' 7. bash daemons/run-watchdog.sh --once (test run)');
|
|
819
|
+
console.log(' 8. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:8770)');
|
|
820
|
+
} else {
|
|
821
|
+
console.log('\nConfiguration steps:');
|
|
822
|
+
console.log(` 1. cd ${finalTargetDir}`);
|
|
823
|
+
console.log(' 2. cp aesop.config.example.json aesop.config.json');
|
|
824
|
+
console.log(' 3. Edit aesop.config.json with your configuration');
|
|
825
|
+
console.log('\nInitialize your brain (Claude Code team memory):');
|
|
826
|
+
console.log(' 4. mkdir -p ~/.claude/memory');
|
|
827
|
+
console.log(` 5. cp ${finalTargetDir}/CLAUDE-TEMPLATE.md ~/.claude/CLAUDE.md (then edit domains/team info)`);
|
|
828
|
+
console.log(` 6. cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
|
|
829
|
+
console.log('\nRun the daemon and dashboard:');
|
|
830
|
+
console.log(' 7. bash daemons/run-watchdog.sh --once (test run)');
|
|
831
|
+
console.log(' 8. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:8770)');
|
|
832
|
+
}
|
|
833
|
+
console.log('\nFor full documentation, see the README.md in the scaffolded directory.');
|
|
834
|
+
process.exit(0);
|
|
835
|
+
} catch (err) {
|
|
836
|
+
console.error(`Error scaffolding template: ${err.message}`);
|
|
837
|
+
process.exit(1);
|
|
838
|
+
}
|
|
839
|
+
})();
|
|
456
840
|
}
|