@robbiesrobotics/alice-agents 1.5.9 → 1.5.11
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 +152 -132
- package/bin/alice-install.mjs +27 -35
- package/lib/hermes-agent.mjs +449 -0
- package/lib/hermes-installer.mjs +338 -0
- package/lib/installer.mjs +302 -22
- package/lib/runtime-installer.mjs +314 -0
- package/lib/skills.mjs +128 -4
- package/package.json +3 -3
- package/templates/workspaces/_shared/AGENTS-hermes.md +54 -0
- package/templates/workspaces/_shared/AGENTS.md +25 -0
- package/templates/workspaces/_shared/SOUL-hermes.md +35 -0
- package/templates/workspaces/_shared/hermes-agent-skill.md +40 -0
- package/templates/workspaces/_shared/hermes-orchestrator-skill.md +150 -0
- package/templates/workspaces/_shared/hermes-specialist-skill.md +109 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// lib/hermes-installer.mjs
|
|
2
|
+
// Installs and configures A.L.I.C.E. for Hermes-only runtime
|
|
3
|
+
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, rmSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const HERMES_DIR = join(homedir(), '.hermes');
|
|
12
|
+
const ALICE_SKILLS_DIR = join(HERMES_DIR, 'skills', 'alice');
|
|
13
|
+
const ALICE_MEMORIES_DIR = join(HERMES_DIR, 'memories', 'alice');
|
|
14
|
+
const ALICE_CONFIG_JSON = join(HERMES_DIR, '.alice-config.json');
|
|
15
|
+
const TEMPLATES_DIR = join(__dirname, '..', 'templates', 'workspaces', '_shared');
|
|
16
|
+
|
|
17
|
+
// ── Runtime detection ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export function isHermesInstalled() {
|
|
20
|
+
if (!existsSync(join(homedir(), '.hermes'))) return false;
|
|
21
|
+
if (!existsSync(join(homedir(), '.hermes', 'config.yaml'))) return false;
|
|
22
|
+
try {
|
|
23
|
+
execSync('hermes version', { stdio: 'pipe' });
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getHermesVersion() {
|
|
31
|
+
try {
|
|
32
|
+
// Suppress stderr to avoid "Failed to load config" warnings
|
|
33
|
+
return execSync('hermes version 2>/dev/null', { encoding: 'utf8', stdio: 'pipe' })
|
|
34
|
+
.trim()
|
|
35
|
+
.split('\n')[0]; // only first line
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read the default model from ~/.hermes/config.yaml
|
|
43
|
+
* Returns null if no model is configured.
|
|
44
|
+
*/
|
|
45
|
+
export function getHermesDefaultModel() {
|
|
46
|
+
try {
|
|
47
|
+
const configPath = join(homedir(), '.hermes', 'config.yaml');
|
|
48
|
+
const content = readFileSync(configPath, 'utf8');
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
let inModel = false;
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (trimmed === 'model:') { inModel = true; continue; }
|
|
54
|
+
if (inModel) {
|
|
55
|
+
// 'default: <modelname>' at the same or deeper indent
|
|
56
|
+
const match = trimmed.match(/^default:\s*(.+)$/);
|
|
57
|
+
if (match) return match[1].trim();
|
|
58
|
+
// If we hit a new top-level key, stop
|
|
59
|
+
if (trimmed.match(/^[a-z]/) && !trimmed.startsWith('default:')) break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Hermes config helpers ─────────────────────────────────────────────────────
|
|
69
|
+
// Uses `hermes config set` for safe updates — Hermes rewrites the file correctly.
|
|
70
|
+
|
|
71
|
+
const ALICE_PERSONALITY = `You are A.L.I.C.E. — the Chief Orchestration Officer of the A.L.I.C.E. multi-agent AI team. Your role is to receive user requests, route them to the right specialist agents (dylan for development, selena for security, devon for DevOps, etc.), wait for their results, and synthesize one clear response. You delegate ALL specialist work — you do not do it yourself. You are sharp, confident, and efficient. Users may call you Alice, A.L.I.C.E., or Olivia.`;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Configure Hermes with alice personality and display settings.
|
|
75
|
+
* Uses `hermes config set` which safely rewrites the YAML.
|
|
76
|
+
*/
|
|
77
|
+
function configureHermesAlice() {
|
|
78
|
+
// Set display personality to alice
|
|
79
|
+
execSync(`hermes config set display.personality alice`, {
|
|
80
|
+
stdio: 'pipe',
|
|
81
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Set alice personality
|
|
85
|
+
execSync(`hermes config set agent.personalities.alice "${ALICE_PERSONALITY.replace(/"/g, '\\"')}"`, {
|
|
86
|
+
stdio: 'pipe',
|
|
87
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Alice skill file generation ───────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function generateOrchestratorSkill() {
|
|
94
|
+
const templatePath = join(TEMPLATES_DIR, 'hermes-orchestrator-skill.md');
|
|
95
|
+
if (!existsSync(templatePath)) {
|
|
96
|
+
// Fallback inline if template not found
|
|
97
|
+
return readFileSync(templatePath, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
return readFileSync(templatePath, 'utf8');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function generateSpecialistSkill(agentId, agentName, domain, domainDescription) {
|
|
103
|
+
let templatePath = join(TEMPLATES_DIR, 'hermes-specialist-skill.md');
|
|
104
|
+
if (!existsSync(templatePath)) {
|
|
105
|
+
// Inline fallback template
|
|
106
|
+
return `---
|
|
107
|
+
name: ${agentId}
|
|
108
|
+
description: ${domain} specialist on the A.L.I.C.E. team.
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
# ${agentName} — ${domain} Specialist
|
|
112
|
+
|
|
113
|
+
**${agentName}** is a ${domain} specialist on the A.L.I.C.E. team.
|
|
114
|
+
A.L.I.C.E. (orchestrator) assigns you tasks. Complete them and report back.
|
|
115
|
+
|
|
116
|
+
## Domain
|
|
117
|
+
${domainDescription}
|
|
118
|
+
|
|
119
|
+
## How You Work
|
|
120
|
+
1. Receive task from A.L.I.C.E.
|
|
121
|
+
2. Use your tools to complete the work
|
|
122
|
+
3. Return structured results to A.L.I.C.E.
|
|
123
|
+
|
|
124
|
+
## Response Format
|
|
125
|
+
**Summary:** one-line answer
|
|
126
|
+
**Details:** what you did/found
|
|
127
|
+
**Next Steps:** recommended actions
|
|
128
|
+
**Blockers:** anything blocking progress (or "None")
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let content = readFileSync(templatePath, 'utf8');
|
|
133
|
+
content = content
|
|
134
|
+
.replace(/\{\{agentId\}\}/g, agentId)
|
|
135
|
+
.replace(/\{\{agentName\}\}/g, agentName)
|
|
136
|
+
.replace(/\{\{domain\}\}/g, domain)
|
|
137
|
+
.replace(/\{\{domainDescription\}\}/g, domainDescription || `${domain} specialist work.`);
|
|
138
|
+
return content;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Alice agent registry ─────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
const ALICE_AGENTS = [
|
|
144
|
+
// Starter agents
|
|
145
|
+
{ id: 'alice', name: 'A.L.I.C.E.', domain: 'Orchestration', isOrchestrator: true, tier: 'core' },
|
|
146
|
+
{ id: 'dylan', name: 'Dylan', domain: 'Development', description: 'Full-stack code, APIs, debugging, architecture, database design', tier: 'starter' },
|
|
147
|
+
{ id: 'selena', name: 'Selena', domain: 'Security', description: 'Security audits, hardening, access controls, incident response, vulnerability assessment', tier: 'starter' },
|
|
148
|
+
{ id: 'devon', name: 'Devon', domain: 'DevOps', description: 'CI/CD pipelines, infrastructure, deployment, Docker, Kubernetes, monitoring', tier: 'starter' },
|
|
149
|
+
{ id: 'quinn', name: 'Quinn', domain: 'QA', description: 'Test design, automation, bug verification, quality assurance, test coverage', tier: 'starter' },
|
|
150
|
+
{ id: 'felix', name: 'Felix', domain: 'Frontend', description: 'UI implementation, React, responsive design, component libraries, CSS', tier: 'starter' },
|
|
151
|
+
{ id: 'daphne', name: 'Daphne', domain: 'Documentation', description: 'API docs, guides, runbooks, READMEs, technical writing', tier: 'starter' },
|
|
152
|
+
{ id: 'rowan', name: 'Rowan', domain: 'Research', description: 'Web research, competitive analysis, fact-finding, technology landscape', tier: 'starter' },
|
|
153
|
+
{ id: 'darius', name: 'Darius', domain: 'Data', description: 'Data pipelines, SQL, analytics, warehousing, ETL processes', tier: 'starter' },
|
|
154
|
+
{ id: 'sophie', name: 'Sophie', domain: 'Support', description: 'Customer support, triage, drafting responses, FAQ creation', tier: 'starter' },
|
|
155
|
+
// Pro agents
|
|
156
|
+
{ id: 'hannah', name: 'Hannah', domain: 'HR', description: 'Onboarding, HR policy, organizational structure, people operations', tier: 'pro' },
|
|
157
|
+
{ id: 'aiden', name: 'Aiden', domain: 'Analytics', description: 'Dashboards, KPI tracking, business intelligence, analytics models', tier: 'pro' },
|
|
158
|
+
{ id: 'clara', name: 'Clara', domain: 'Communications', description: 'Writing, email sequences, press releases, messaging frameworks', tier: 'pro' },
|
|
159
|
+
{ id: 'avery', name: 'Avery', domain: 'Automation', description: 'Workflow automation, no-code automation, process engineering', tier: 'pro' },
|
|
160
|
+
{ id: 'owen', name: 'Owen', domain: 'Operations', description: 'Vendor management, process efficiency, day-to-day operations', tier: 'pro' },
|
|
161
|
+
{ id: 'isaac', name: 'Isaac', domain: 'Integrations', description: 'API integrations, webhook configurations, third-party connections', tier: 'pro' },
|
|
162
|
+
{ id: 'tommy', name: 'Tommy', domain: 'Travel', description: 'Travel booking, itineraries, logistics, trip planning', tier: 'pro' },
|
|
163
|
+
{ id: 'sloane', name: 'Sloane', domain: 'Sales', description: 'Sales pipeline, outreach, deal management, revenue strategy', tier: 'pro' },
|
|
164
|
+
{ id: 'nadia', name: 'Nadia', domain: 'UI/UX Design', description: 'UX wireframes, visual systems, prototypes, design reviews', tier: 'pro' },
|
|
165
|
+
{ id: 'morgan', name: 'Morgan', domain: 'Marketing', description: 'Content marketing, campaigns, SEO, social media, positioning', tier: 'pro' },
|
|
166
|
+
{ id: 'alex', name: 'Alex', domain: 'API Crawling', description: 'Web scraping, API data extraction, large-scale crawling', tier: 'pro' },
|
|
167
|
+
{ id: 'uma', name: 'Uma', domain: 'UX Research', description: 'User interviews, usability studies, qualitative research, personas', tier: 'pro' },
|
|
168
|
+
{ id: 'caleb', name: 'Caleb', domain: 'CRM', description: 'CRM administration, pipeline management, contact lifecycle', tier: 'pro' },
|
|
169
|
+
{ id: 'elena', name: 'Elena', domain: 'Estimation', description: 'Project estimation, effort scoping, technical planning', tier: 'pro' },
|
|
170
|
+
{ id: 'audrey', name: 'Audrey', domain: 'Accounting', description: 'Financial tracking, budgets, expense management, reconciliation', tier: 'pro' },
|
|
171
|
+
{ id: 'logan', name: 'Logan', domain: 'Legal', description: 'Contract review, terms of service, NDAs, regulatory compliance', tier: 'pro' },
|
|
172
|
+
{ id: 'eva', name: 'Eva', domain: 'Executive Assistant', description: 'Scheduling, meeting preparation, executive briefs, follow-ups', tier: 'pro' },
|
|
173
|
+
{ id: 'parker', name: 'Parker', domain: 'Project Management', description: 'Project milestones, tracking, cross-functional coordination', tier: 'pro' },
|
|
174
|
+
{ id: 'nate', name: 'Nate', domain: 'n8n Automation', description: 'n8n workflow building, node-based automation, workflow design', tier: 'pro' },
|
|
175
|
+
{ id: 'aria', name: 'Aria', domain: 'Autonomous Research', description: 'Deep investigative research, longitudinal studies, synthesis', tier: 'pro' },
|
|
176
|
+
{ id: 'maxxipro', name: 'MaxxiPro', domain: 'Roof Maxx Expert', description: 'Roofing estimation, contracting, roof inspection workflows', tier: 'pro' },
|
|
177
|
+
{ id: 'accuscope', name: 'AccuScope', domain: 'AccuLynx CRM Auditor', description: 'AccuLynx CRM data quality, auditing, pipeline health', tier: 'pro' },
|
|
178
|
+
{ id: 'smoketestagent', name: 'SmokeTestAgent', domain: 'QA Specialist', description: 'Smoke testing, regression testing, automated test suites', tier: 'pro' },
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
export function getAliceAgents(tier = 'both') {
|
|
182
|
+
if (tier === 'starter') return ALICE_AGENTS.filter(a => a.tier === 'starter' || a.isOrchestrator);
|
|
183
|
+
if (tier === 'pro') return ALICE_AGENTS;
|
|
184
|
+
return ALICE_AGENTS;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Install A.L.I.C.E. on Hermes ─────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
export async function installForHermes(options = {}) {
|
|
190
|
+
const { tier = 'starter', auto = false, userInfo = null } = options;
|
|
191
|
+
const agents = getAliceAgents(tier);
|
|
192
|
+
const result = { skills: [], memories: [], config: null, errors: [] };
|
|
193
|
+
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
196
|
+
console.log(` ${'🧠'.padStart(15)} ${'A.L.I.C.E. on Hermes'}`);
|
|
197
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
198
|
+
console.log('');
|
|
199
|
+
|
|
200
|
+
// 1. Verify Hermes is installed
|
|
201
|
+
if (!isHermesInstalled()) {
|
|
202
|
+
throw new Error('Hermes is not installed. Run: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const version = getHermesVersion();
|
|
206
|
+
console.log(` ${'✓'.padStart(15)} ${version}`);
|
|
207
|
+
console.log('');
|
|
208
|
+
|
|
209
|
+
// 2. Ensure alice skill directory
|
|
210
|
+
if (!existsSync(ALICE_SKILLS_DIR)) {
|
|
211
|
+
mkdirSync(ALICE_SKILLS_DIR, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 3. Install orchestrator skill
|
|
215
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
216
|
+
console.log(` ${'🤖'.padStart(15)} ${'A.L.I.C.E. orchestrator skill'}`);
|
|
217
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
218
|
+
const orchestratorDir = join(ALICE_SKILLS_DIR, 'alice');
|
|
219
|
+
mkdirSync(orchestratorDir, { recursive: true });
|
|
220
|
+
const orchestratorContent = generateOrchestratorSkill();
|
|
221
|
+
writeFileSync(join(orchestratorDir, 'SKILL.md'), orchestratorContent, 'utf8');
|
|
222
|
+
console.log(` ${'✓'.padStart(15)} alice/SKILL.md — orchestrator`);
|
|
223
|
+
result.skills.push('alice');
|
|
224
|
+
|
|
225
|
+
// 4. Install specialist skills
|
|
226
|
+
const specialistAgents = agents.filter(a => !a.isOrchestrator);
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
229
|
+
console.log(` ${'👥'.padStart(15)} ${specialistAgents.length} specialist skills`);
|
|
230
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
231
|
+
|
|
232
|
+
for (const agent of specialistAgents) {
|
|
233
|
+
const skillDir = join(ALICE_SKILLS_DIR, agent.id);
|
|
234
|
+
mkdirSync(skillDir, { recursive: true });
|
|
235
|
+
const skillContent = generateSpecialistSkill(agent.id, agent.name, agent.domain, agent.description || '');
|
|
236
|
+
writeFileSync(join(skillDir, 'SKILL.md'), skillContent, 'utf8');
|
|
237
|
+
console.log(` ${'✓'.padStart(15)} ${agent.id.padEnd(15)} — ${agent.domain}`);
|
|
238
|
+
result.skills.push(agent.id);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 5. Create memory directories
|
|
242
|
+
console.log('');
|
|
243
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
244
|
+
console.log(` ${'🧠'.padStart(15)} ${'Memory directories'}`);
|
|
245
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
246
|
+
|
|
247
|
+
for (const agent of agents) {
|
|
248
|
+
const memDir = join(HERMES_DIR, 'memories', agent.id);
|
|
249
|
+
if (!existsSync(memDir)) {
|
|
250
|
+
mkdirSync(memDir, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
result.memories.push(agent.id);
|
|
253
|
+
}
|
|
254
|
+
console.log(` ${'✓'.padStart(15)} ${agents.length} memory dirs created`);
|
|
255
|
+
|
|
256
|
+
// 6. Patch Hermes config with alice personality
|
|
257
|
+
console.log('');
|
|
258
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
259
|
+
console.log(` ${'⚙️'.padStart(15)} ${'Hermes configuration'}`);
|
|
260
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
261
|
+
|
|
262
|
+
configureHermesAlice();
|
|
263
|
+
result.config = 'config.yaml updated';
|
|
264
|
+
console.log(` ${'✓'.padStart(15)} personality 'alice' added to config.yaml`);
|
|
265
|
+
console.log(` ${'✓'.padStart(15)} display.personality set to alice`);
|
|
266
|
+
|
|
267
|
+
// 7. Write alice-config.json for installer tracking
|
|
268
|
+
const aliceConfig = {
|
|
269
|
+
runtime: 'hermes',
|
|
270
|
+
version,
|
|
271
|
+
installedAt: new Date().toISOString(),
|
|
272
|
+
agents: agents.map(a => a.id),
|
|
273
|
+
tier,
|
|
274
|
+
userName: userInfo?.name || 'unknown',
|
|
275
|
+
};
|
|
276
|
+
writeFileSync(ALICE_CONFIG_JSON, JSON.stringify(aliceConfig, null, 2), 'utf8');
|
|
277
|
+
|
|
278
|
+
console.log('');
|
|
279
|
+
console.log(` ${'✓'.padStart(15)} alice-config.json written`);
|
|
280
|
+
console.log('');
|
|
281
|
+
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Uninstall A.L.I.C.E. from Hermes ────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
export function uninstallFromHermes(options = {}) {
|
|
288
|
+
const { auto = false } = options;
|
|
289
|
+
const result = { removed: [], errors: [] };
|
|
290
|
+
|
|
291
|
+
// Remove alice skill tree
|
|
292
|
+
if (existsSync(ALICE_SKILLS_DIR)) {
|
|
293
|
+
rmSync(ALICE_SKILLS_DIR, { recursive: true, force: true });
|
|
294
|
+
result.removed.push('alice skills');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Remove alice memories
|
|
298
|
+
const memoriesBase = join(HERMES_DIR, 'memories', 'alice');
|
|
299
|
+
if (existsSync(memoriesBase)) {
|
|
300
|
+
rmSync(memoriesBase, { recursive: true, force: true });
|
|
301
|
+
result.removed.push('alice memories');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Remove config tracking
|
|
305
|
+
if (existsSync(ALICE_CONFIG_JSON)) {
|
|
306
|
+
rmSync(ALICE_CONFIG_JSON, { force: true });
|
|
307
|
+
result.removed.push('alice-config.json');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Remove alice personality using hermes config set (deletes by unsetting)
|
|
311
|
+
try {
|
|
312
|
+
// Setting to empty/null effectively removes it
|
|
313
|
+
execSync(`hermes config set display.personality helpful`, {
|
|
314
|
+
stdio: 'pipe',
|
|
315
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
316
|
+
});
|
|
317
|
+
result.removed.push('display.personality reset to helpful');
|
|
318
|
+
} catch (err) {
|
|
319
|
+
result.errors.push(`display.personality reset: ${err.message}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Status ─────────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
export function getHermesAliceStatus() {
|
|
328
|
+
const configPath = join(ALICE_CONFIG_JSON);
|
|
329
|
+
if (!existsSync(configPath)) {
|
|
330
|
+
return { installed: false };
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const data = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
334
|
+
return { installed: true, ...data };
|
|
335
|
+
} catch {
|
|
336
|
+
return { installed: false };
|
|
337
|
+
}
|
|
338
|
+
}
|