smash-os-install 0.4.3 → 0.4.6
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/install.mjs +252 -131
- package/package.json +64 -58
- package/templates.mjs +3954 -1028
package/install.mjs
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
*
|
|
11
11
|
* What it does:
|
|
12
12
|
* 1. Prompts for project name and tech stack
|
|
13
|
-
* 2. Writes CLAUDE.md + /
|
|
13
|
+
* 2. Writes CLAUDE.md + /.smashOS/ skeleton + .smash-os-mode=local
|
|
14
14
|
* 3. Installs SmashOS skills globally (~/.claude/skills/)
|
|
15
15
|
* 4. No web app, no API keys, no cloud dependencies required
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { execSync } from 'child_process';
|
|
19
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'fs';
|
|
20
20
|
import { join, dirname, basename } from 'path';
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
22
|
import { homedir } from 'os';
|
|
@@ -25,14 +25,14 @@ import chalk from 'chalk';
|
|
|
25
25
|
import {
|
|
26
26
|
getClaudeMd, getAiFiles, getFrontendFiles,
|
|
27
27
|
agentFiles, settingsJson, bootHook, syncHook,
|
|
28
|
-
localSkills, localSkillExtras,
|
|
28
|
+
localSkills, localSkillExtras, osDocsFiles,
|
|
29
29
|
} from './templates.mjs';
|
|
30
30
|
|
|
31
31
|
const cwd = process.cwd();
|
|
32
32
|
const isUpgrade = process.argv.includes('--upgrade');
|
|
33
33
|
const isVerify = process.argv.includes('--verify');
|
|
34
34
|
const isUninstall = process.argv.includes('--uninstall');
|
|
35
|
-
const SMASH_VERSION = '0.4.
|
|
35
|
+
const SMASH_VERSION = '0.4.6';
|
|
36
36
|
const vaultConventions = join(process.env.USERPROFILE || process.env.HOME || homedir(), 'Desktop', 'SmashBurgerBar', 'SmashVault', 'Architecture', 'conventions.md');
|
|
37
37
|
const globalConventions = join(homedir(), '.claude', 'conventions.md');
|
|
38
38
|
const conventionsFile = existsSync(vaultConventions) ? vaultConventions : globalConventions;
|
|
@@ -226,7 +226,10 @@ function findClaude() {
|
|
|
226
226
|
|
|
227
227
|
function loadRegistry() {
|
|
228
228
|
if (!existsSync(REGISTRY_FILE)) return [];
|
|
229
|
-
try {
|
|
229
|
+
try {
|
|
230
|
+
const raw = JSON.parse(readFileSync(REGISTRY_FILE, 'utf8'));
|
|
231
|
+
return raw.filter(p => p && p.path && !p.path.includes('..'));
|
|
232
|
+
} catch { return []; }
|
|
230
233
|
}
|
|
231
234
|
|
|
232
235
|
function saveRegistry(projects) {
|
|
@@ -235,7 +238,7 @@ function saveRegistry(projects) {
|
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
function schtask(name, batPath, schedule) {
|
|
238
|
-
const user = process.env.USERNAME || process.env.USER || 'Administrator';
|
|
241
|
+
const user = (process.env.USERNAME || process.env.USER || 'Administrator').replace(/[^a-zA-Z0-9._\-]/g, '_');
|
|
239
242
|
try {
|
|
240
243
|
autoRun(`schtasks /create /tn "SmashOS\\${name}" /tr "${batPath}" ${schedule} /f /ru "${user}"`);
|
|
241
244
|
console.log(' ' + chalk.green('✓') + ' SmashOS\\' + name);
|
|
@@ -246,7 +249,7 @@ function schtask(name, batPath, schedule) {
|
|
|
246
249
|
|
|
247
250
|
function buildBats(projects, claudeExe) {
|
|
248
251
|
const base = `"${claudeExe}" --dangerously-skip-permissions --print`;
|
|
249
|
-
const home = process.env.USERPROFILE || process.env.HOME ||
|
|
252
|
+
const home = process.env.USERPROFILE || process.env.HOME || homedir();
|
|
250
253
|
|
|
251
254
|
// Scoped tool allowlists per task type — never grant unrestricted access
|
|
252
255
|
const SCOPE = {
|
|
@@ -257,25 +260,29 @@ function buildBats(projects, claudeExe) {
|
|
|
257
260
|
const C_MED = `${base} --max-turns 50 ${SCOPE.medium}`;
|
|
258
261
|
const hdr = `@echo off\nset CLAUDE=${base} --max-turns 20 ${SCOPE.light}\n`;
|
|
259
262
|
|
|
263
|
+
const safeN = n => n.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
264
|
+
const safePath = p => p.replace(/"/g, '');
|
|
260
265
|
function block(p, prompt, slug, cmd = C) {
|
|
261
266
|
const log = join(SMASH_BASE, p.name, 'logs', `${slug}.log`);
|
|
262
|
-
return `\n:: === ${p.name} ===\ncd /d "${p.path}"\necho [%date% %time%] Running ${slug}... >> "${log}"\n${cmd} "${prompt}" >> "${log}" 2>&1\necho [%date% %time%] Done. >> "${log}"`;
|
|
267
|
+
return `\n:: === ${safeN(p.name)} ===\ncd /d "${safePath(p.path)}"\necho [%date% %time%] Running ${slug}... >> "${log}"\n${cmd} "${prompt}" >> "${log}" 2>&1\necho [%date% %time%] Done. >> "${log}"`;
|
|
263
268
|
}
|
|
264
269
|
function blockCmd(p, cmd, slug, exe = C) {
|
|
265
270
|
const log = join(SMASH_BASE, p.name, 'logs', `${slug}.log`);
|
|
266
|
-
return `\n:: === ${p.name} ===\ncd /d "${p.path}"\necho [%date% %time%] Running ${slug}... >> "${log}"\n${exe} "${cmd}" >> "${log}" 2>&1\necho [%date% %time%] Done. >> "${log}"`;
|
|
271
|
+
return `\n:: === ${safeN(p.name)} ===\ncd /d "${safePath(p.path)}"\necho [%date% %time%] Running ${slug}... >> "${log}"\n${exe} "${cmd}" >> "${log}" 2>&1\necho [%date% %time%] Done. >> "${log}"`;
|
|
267
272
|
}
|
|
268
273
|
function bat(slug, getBlock) { return hdr + '\n' + projects.map(p => getBlock(p)).join('\n') + '\n'; }
|
|
269
|
-
function batMed(slug, getBlock) { return `@echo off\nset CLAUDE=${base} ${SCOPE.medium}\n` + '\n' + projects.map(p => getBlock(p)).join('\n') + '\n'; }
|
|
274
|
+
function batMed(slug, getBlock) { return `@echo off\nset CLAUDE=${base} --max-turns 50 ${SCOPE.medium}\n` + '\n' + projects.map(p => getBlock(p)).join('\n') + '\n'; }
|
|
270
275
|
|
|
271
276
|
return [
|
|
272
277
|
{ taskName: 'Projects\\LockCleanup', batFile: join(SCRIPTS_DIR, 'lock-cleanup.bat'), schedule: '/sc hourly /st 00:00', content: bat('lock-cleanup', p => block(p, 'Review .claude/scheduled_tasks.lock and any stale pipeline lock files. Delete any locks older than 2 hours and report what was cleaned. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', 'lock-cleanup')) },
|
|
273
|
-
{ taskName: 'Projects\\MemoryConsolidation', batFile: join(SCRIPTS_DIR, 'memory-consolidation.bat'), schedule: '/sc daily /st 01:00', content: bat('memory-consolidation', p => block(p, 'Read all files in
|
|
278
|
+
{ taskName: 'Projects\\MemoryConsolidation', batFile: join(SCRIPTS_DIR, 'memory-consolidation.bat'), schedule: '/sc daily /st 01:00', content: bat('memory-consolidation', p => block(p, 'Read all files in .smashOS/memory/. Consolidate duplicates and summarise entries older than 30 days into a single summary entry. Save the updated files. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', 'memory-consolidation')) },
|
|
274
279
|
{ taskName: 'Projects\\NightlyAudit', batFile: join(SCRIPTS_DIR, 'nightly-audit.bat'), schedule: '/sc daily /st 02:00', content: batMed('nightly-audit', p => blockCmd(p, '/smash-os:audit', 'nightly-audit', C_MED)) },
|
|
275
280
|
{ taskName: '_skills\\DreamMemory', batFile: join(SKILL_SCRIPTS, 'dream-memory.bat'), schedule: '/sc daily /st 04:00', content: `@echo off\ncd /d "${home}"\necho [%date% %time%] Running dream-memory... >> "${join(SKILL_LOGS, 'dream-memory.log')}"\n"${claudeExe}" --dangerously-skip-permissions --print --max-turns 20 --allowedTools Read,Write,Glob,Grep "/dream-memory" >> "${join(SKILL_LOGS, 'dream-memory.log')}" 2>&1\necho [%date% %time%] Done. >> "${join(SKILL_LOGS, 'dream-memory.log')}"\n` },
|
|
276
281
|
{ taskName: 'Projects\\WeeklyImprovement', batFile: join(SCRIPTS_DIR, 'weekly-improvement.bat'), schedule: '/sc weekly /d MON /st 05:00', content: batMed('weekly-improvement', p => blockCmd(p, '/smash-os:run weekly-improvement', 'weekly-improvement', C_MED)) },
|
|
277
282
|
{ taskName: '_skills\\SkillEvolution', batFile: join(SKILL_SCRIPTS, 'skill-evolution.bat'), schedule: '/sc weekly /d MON /st 06:00', content: `@echo off\ncd /d "${home}"\necho [%date% %time%] Running skill evolution... >> "${join(SKILL_LOGS, 'skill-evolution.log')}"\n"${claudeExe}" --dangerously-skip-permissions --print --max-turns 20 --allowedTools Read,Write,Glob,Grep "/skill-evolution" >> "${join(SKILL_LOGS, 'skill-evolution.log')}" 2>&1\necho [%date% %time%] Done. >> "${join(SKILL_LOGS, 'skill-evolution.log')}"\n` },
|
|
278
283
|
{ taskName: 'Projects\\RoleEvolution', batFile: join(SCRIPTS_DIR, 'role-evolution.bat'), schedule: '/sc weekly /d MON /st 06:30', content: batMed('role-evolution', p => blockCmd(p, '/smash-os:role-improve', 'role-evolution', C_MED)) },
|
|
284
|
+
{ taskName: 'Projects\\HarnessEvolution', batFile: join(SCRIPTS_DIR, 'harness-evolution.bat'), schedule: '/sc weekly /d MON /st 07:00', content: batMed('harness-evolution', p => blockCmd(p, '/smash-os:evolve', 'harness-evolution', C_MED)) },
|
|
285
|
+
{ taskName: 'Projects\\DailyLogReport', batFile: join(SCRIPTS_DIR, 'daily-log-report.bat'), schedule: '/sc daily /st 03:00', content: bat('daily-log-report', p => block(p, `Scan all .log files in ${join(SMASH_BASE, p.name, 'logs')}. For each log file identify errors, warnings, task failures, and notable events. Append a new dated section to .smashOS/todo/todo.md with: (1) status row per scheduled task showing ran/failed/skipped, (2) errors or issues requiring attention with context, (3) concrete next-step suggestions. Create .smashOS/todo/todo.md if it does not exist. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.`, 'daily-log-report')) },
|
|
279
286
|
];
|
|
280
287
|
}
|
|
281
288
|
|
|
@@ -299,7 +306,7 @@ function setupWindowsTaskScheduler(projects, claudeExe, projectName) {
|
|
|
299
306
|
for (const b of bats) schtask(b.taskName, b.batFile, b.schedule);
|
|
300
307
|
|
|
301
308
|
console.log('');
|
|
302
|
-
console.log(chalk.dim(' Schedule: hourly lock-cleanup · 1am memory · 2am audit · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution'));
|
|
309
|
+
console.log(chalk.dim(' Schedule: hourly lock-cleanup · 1am memory · 2am audit · 3am log-report · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution · Mon 7am harness-evolution'));
|
|
303
310
|
console.log(chalk.dim(` Logs: ${join(SMASH_BASE, projectName, 'logs')}`));
|
|
304
311
|
}
|
|
305
312
|
|
|
@@ -319,8 +326,9 @@ function setupMacOSLaunchAgents(projects, claudeExe, projectName) {
|
|
|
319
326
|
medium: 'Read,Write,Edit,Bash,Glob,Grep',
|
|
320
327
|
};
|
|
321
328
|
|
|
329
|
+
const xmlEsc = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
322
330
|
function plist(label, args, workDir, logFile, calendarInterval) {
|
|
323
|
-
const argXml = args.map(a => `\t\t<string>${a}</string>`).join('\n');
|
|
331
|
+
const argXml = args.map(a => `\t\t<string>${xmlEsc(a)}</string>`).join('\n');
|
|
324
332
|
const intervalXml = Object.entries(calendarInterval)
|
|
325
333
|
.map(([k, v]) => `\t\t<key>${k}</key>\n\t\t<integer>${v}</integer>`)
|
|
326
334
|
.join('\n');
|
|
@@ -335,92 +343,77 @@ function setupMacOSLaunchAgents(projects, claudeExe, projectName) {
|
|
|
335
343
|
${argXml}
|
|
336
344
|
\t</array>
|
|
337
345
|
\t<key>WorkingDirectory</key>
|
|
338
|
-
\t<string>${workDir}</string>
|
|
346
|
+
\t<string>${xmlEsc(workDir)}</string>
|
|
339
347
|
\t<key>StartCalendarInterval</key>
|
|
340
348
|
\t<dict>
|
|
341
349
|
${intervalXml}
|
|
342
350
|
\t</dict>
|
|
343
351
|
\t<key>StandardOutPath</key>
|
|
344
|
-
\t<string>${logFile}</string>
|
|
352
|
+
\t<string>${xmlEsc(logFile)}</string>
|
|
345
353
|
\t<key>StandardErrorPath</key>
|
|
346
|
-
\t<string>${logFile}</string>
|
|
354
|
+
\t<string>${xmlEsc(logFile)}</string>
|
|
347
355
|
\t<key>RunAtLoad</key>
|
|
348
356
|
\t<false/>
|
|
349
357
|
</dict>
|
|
350
358
|
</plist>`;
|
|
351
359
|
}
|
|
352
360
|
|
|
353
|
-
function
|
|
361
|
+
function paArgs(prompt, tools, turns = 20) {
|
|
354
362
|
return [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', String(turns), '--allowedTools', tools, prompt];
|
|
355
363
|
}
|
|
364
|
+
const safePName = n => n.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
356
365
|
|
|
357
|
-
|
|
358
|
-
{
|
|
359
|
-
|
|
360
|
-
args: projectArgs('Review .claude/scheduled_tasks.lock and any stale pipeline lock files. Delete any locks older than 2 hours and report what was cleaned. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', scope.light),
|
|
361
|
-
workDir: projects[0]?.path || homedir(),
|
|
362
|
-
logFile: join(SMASH_BASE, projectName, 'logs', 'lock-cleanup.log'),
|
|
363
|
-
interval: { Hour: 0, Minute: 0 },
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
label: 'com.smash-os.memory-consolidation',
|
|
367
|
-
args: projectArgs('Read all files in ai/memory/. Consolidate duplicates and summarise entries older than 30 days into a single summary entry. Save the updated files. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', scope.light),
|
|
368
|
-
workDir: projects[0]?.path || homedir(),
|
|
369
|
-
logFile: join(SMASH_BASE, projectName, 'logs', 'memory-consolidation.log'),
|
|
370
|
-
interval: { Hour: 1, Minute: 0 },
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
label: 'com.smash-os.nightly-audit',
|
|
374
|
-
args: [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:audit'],
|
|
375
|
-
workDir: projects[0]?.path || homedir(),
|
|
376
|
-
logFile: join(SMASH_BASE, projectName, 'logs', 'nightly-audit.log'),
|
|
377
|
-
interval: { Hour: 2, Minute: 0 },
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
label: 'com.smash-os.dream-memory',
|
|
381
|
-
args: [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '20', '--allowedTools', scope.light, '/dream-memory'],
|
|
382
|
-
workDir: homedir(),
|
|
383
|
-
logFile: join(SKILL_LOGS, 'dream-memory.log'),
|
|
384
|
-
interval: { Hour: 4, Minute: 0 },
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
label: 'com.smash-os.weekly-improvement',
|
|
388
|
-
args: [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:run weekly-improvement'],
|
|
389
|
-
workDir: projects[0]?.path || homedir(),
|
|
390
|
-
logFile: join(SMASH_BASE, projectName, 'logs', 'weekly-improvement.log'),
|
|
391
|
-
interval: { Weekday: 1, Hour: 5, Minute: 0 },
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
label: 'com.smash-os.skill-evolution',
|
|
395
|
-
args: [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '20', '--allowedTools', scope.light, '/skill-evolution'],
|
|
396
|
-
workDir: homedir(),
|
|
397
|
-
logFile: join(SKILL_LOGS, 'skill-evolution.log'),
|
|
398
|
-
interval: { Weekday: 1, Hour: 6, Minute: 0 },
|
|
399
|
-
},
|
|
400
|
-
{
|
|
401
|
-
label: 'com.smash-os.role-evolution',
|
|
402
|
-
args: [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:role-improve'],
|
|
403
|
-
workDir: projects[0]?.path || homedir(),
|
|
404
|
-
logFile: join(SMASH_BASE, projectName, 'logs', 'role-evolution.log'),
|
|
405
|
-
interval: { Weekday: 1, Hour: 6, Minute: 30 },
|
|
406
|
-
},
|
|
407
|
-
];
|
|
408
|
-
|
|
409
|
-
let written = 0;
|
|
410
|
-
for (const t of tasks) {
|
|
411
|
-
const plistPath = join(launchAgentsDir, `${t.label}.plist`);
|
|
412
|
-
writeFileSync(plistPath, plist(t.label, t.args, t.workDir, t.logFile, t.interval), 'utf8');
|
|
366
|
+
function writePlist(label, args, workDir, logFile, interval) {
|
|
367
|
+
const plistPath = join(launchAgentsDir, `${label}.plist`);
|
|
368
|
+
writeFileSync(plistPath, plist(label, args, workDir, logFile, interval), 'utf8');
|
|
413
369
|
try {
|
|
414
370
|
autoRun(`launchctl load "${plistPath}"`);
|
|
415
|
-
console.log(' ' + chalk.green('✓') + ' ' +
|
|
371
|
+
console.log(' ' + chalk.green('✓') + ' ' + label);
|
|
416
372
|
} catch {
|
|
417
|
-
console.log(' ' + chalk.yellow('⚠') + ' ' +
|
|
373
|
+
console.log(' ' + chalk.yellow('⚠') + ' ' + label + chalk.dim(' — written but not loaded (run launchctl load manually)'));
|
|
418
374
|
}
|
|
419
|
-
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Skill tasks — not project-specific, always run from homedir (one plist each)
|
|
378
|
+
writePlist('com.smash-os.dream-memory',
|
|
379
|
+
[claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '20', '--allowedTools', scope.light, '/dream-memory'],
|
|
380
|
+
homedir(), join(SKILL_LOGS, 'dream-memory.log'), { Hour: 4, Minute: 0 });
|
|
381
|
+
writePlist('com.smash-os.skill-evolution',
|
|
382
|
+
[claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '20', '--allowedTools', scope.light, '/skill-evolution'],
|
|
383
|
+
homedir(), join(SKILL_LOGS, 'skill-evolution.log'), { Weekday: 1, Hour: 6, Minute: 0 });
|
|
384
|
+
|
|
385
|
+
// Project tasks — one plist per (task, project) so all registered projects get automation
|
|
386
|
+
let written = 2; // skill tasks already written above
|
|
387
|
+
for (const p of projects) {
|
|
388
|
+
const pn = safePName(p.name);
|
|
389
|
+
const pLogDir = join(SMASH_BASE, p.name, 'logs');
|
|
390
|
+
mkdirSync(pLogDir, { recursive: true });
|
|
391
|
+
writePlist(`com.smash-os.lock-cleanup.${pn}`,
|
|
392
|
+
paArgs('Review .claude/scheduled_tasks.lock and any stale pipeline lock files. Delete any locks older than 2 hours and report what was cleaned. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', scope.light),
|
|
393
|
+
p.path, join(pLogDir, 'lock-cleanup.log'), { Hour: 0, Minute: 0 });
|
|
394
|
+
writePlist(`com.smash-os.memory-consolidation.${pn}`,
|
|
395
|
+
paArgs('Read all files in .smashOS/memory/. Consolidate duplicates and summarise entries older than 30 days into a single summary entry. Save the updated files. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', scope.light),
|
|
396
|
+
p.path, join(pLogDir, 'memory-consolidation.log'), { Hour: 1, Minute: 0 });
|
|
397
|
+
writePlist(`com.smash-os.nightly-audit.${pn}`,
|
|
398
|
+
[claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:audit'],
|
|
399
|
+
p.path, join(pLogDir, 'nightly-audit.log'), { Hour: 2, Minute: 0 });
|
|
400
|
+
writePlist(`com.smash-os.daily-log-report.${pn}`,
|
|
401
|
+
paArgs(`Scan all .log files in ${pLogDir}. For each log file identify errors, warnings, task failures, and notable events. Append a new dated section to .smashOS/todo/todo.md with: (1) status row per scheduled task showing ran/failed/skipped, (2) errors or issues requiring attention with context, (3) concrete next-step suggestions. Create .smashOS/todo/todo.md if it does not exist. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.`, scope.light),
|
|
402
|
+
p.path, join(pLogDir, 'daily-log-report.log'), { Hour: 3, Minute: 0 });
|
|
403
|
+
writePlist(`com.smash-os.weekly-improvement.${pn}`,
|
|
404
|
+
[claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:run weekly-improvement'],
|
|
405
|
+
p.path, join(pLogDir, 'weekly-improvement.log'), { Weekday: 1, Hour: 5, Minute: 0 });
|
|
406
|
+
writePlist(`com.smash-os.role-evolution.${pn}`,
|
|
407
|
+
[claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:role-improve'],
|
|
408
|
+
p.path, join(pLogDir, 'role-evolution.log'), { Weekday: 1, Hour: 6, Minute: 30 });
|
|
409
|
+
writePlist(`com.smash-os.harness-evolution.${pn}`,
|
|
410
|
+
[claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:evolve'],
|
|
411
|
+
p.path, join(pLogDir, 'harness-evolution.log'), { Weekday: 1, Hour: 7, Minute: 0 });
|
|
412
|
+
written += 7;
|
|
420
413
|
}
|
|
421
414
|
|
|
422
415
|
console.log('');
|
|
423
|
-
console.log(chalk.dim(` ${written} LaunchAgents installed (hourly lock-cleanup · 1am memory · 2am audit · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution)`));
|
|
416
|
+
console.log(chalk.dim(` ${written} LaunchAgents installed (hourly lock-cleanup · 1am memory · 2am audit · 3am log-report · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution · Mon 7am harness-evolution)`));
|
|
424
417
|
console.log(chalk.dim(` Logs: ${join(SMASH_BASE, projectName, 'logs')}`));
|
|
425
418
|
}
|
|
426
419
|
|
|
@@ -438,31 +431,52 @@ function setupLinuxCron(projects, claudeExe, projectName) {
|
|
|
438
431
|
medium: 'Read,Write,Edit,Bash,Glob,Grep',
|
|
439
432
|
};
|
|
440
433
|
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const entries = [
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
434
|
+
const primaryLogDir = join(SMASH_BASE, projectName, 'logs');
|
|
435
|
+
const C = `"${claudeExe}" --dangerously-skip-permissions --print --max-turns 20 --allowedTools ${scope.light}`;
|
|
436
|
+
const C_MED = `"${claudeExe}" --dangerously-skip-permissions --print --max-turns 50 --allowedTools ${scope.medium}`;
|
|
437
|
+
|
|
438
|
+
// Build per-project entries (one set per registered project)
|
|
439
|
+
const entries = [];
|
|
440
|
+
for (const p of projects) {
|
|
441
|
+
const pDir = p.path;
|
|
442
|
+
const pLogDir = join(SMASH_BASE, p.name, 'logs');
|
|
443
|
+
mkdirSync(pLogDir, { recursive: true });
|
|
444
|
+
entries.push(
|
|
445
|
+
`0 * * * * cd "${pDir}" && ${C} "Review .claude/scheduled_tasks.lock and any stale pipeline lock files. Delete any locks older than 2 hours. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning." >> "${join(pLogDir, 'lock-cleanup.log')}" 2>&1`,
|
|
446
|
+
`0 1 * * * cd "${pDir}" && ${C} "Read all files in .smashOS/memory/. Consolidate duplicates and summarise entries older than 30 days. Save the updated files. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning." >> "${join(pLogDir, 'memory-consolidation.log')}" 2>&1`,
|
|
447
|
+
`0 2 * * * cd "${pDir}" && ${C_MED} "/smash-os:audit" >> "${join(pLogDir, 'nightly-audit.log')}" 2>&1`,
|
|
448
|
+
`0 5 * * 1 cd "${pDir}" && ${C_MED} "/smash-os:run weekly-improvement" >> "${join(pLogDir, 'weekly-improvement.log')}" 2>&1`,
|
|
449
|
+
`30 6 * * 1 cd "${pDir}" && ${C_MED} "/smash-os:role-improve" >> "${join(pLogDir, 'role-evolution.log')}" 2>&1`,
|
|
450
|
+
`0 7 * * 1 cd "${pDir}" && ${C_MED} "/smash-os:evolve" >> "${join(pLogDir, 'harness-evolution.log')}" 2>&1`,
|
|
451
|
+
`0 3 * * * cd "${pDir}" && ${C} "Scan all .log files in ${pLogDir}. For each log file identify errors, warnings, task failures, and notable events. Append a new dated section to .smashOS/todo/todo.md with: (1) status row per scheduled task showing ran/failed/skipped, (2) errors or issues requiring attention with context, (3) concrete next-step suggestions. Create .smashOS/todo/todo.md if it does not exist. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning." >> "${join(pLogDir, 'daily-log-report.log')}" 2>&1`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
// Skill tasks (not project-specific — always run from homedir)
|
|
455
|
+
entries.push(
|
|
450
456
|
`0 4 * * * cd "${homedir()}" && ${C} "/dream-memory" >> "${join(SKILL_LOGS, 'dream-memory.log')}" 2>&1`,
|
|
451
|
-
`0 5 * * 1 cd "${projectDir}" && ${C_MED} "/smash-os:run weekly-improvement" >> "${join(logDir, 'weekly-improvement.log')}" 2>&1`,
|
|
452
457
|
`0 6 * * 1 cd "${homedir()}" && ${C} "/skill-evolution" >> "${join(SKILL_LOGS, 'skill-evolution.log')}" 2>&1`,
|
|
453
|
-
|
|
454
|
-
];
|
|
458
|
+
);
|
|
455
459
|
|
|
456
460
|
const smashMarker = '# SmashOS — managed by smash-os-install';
|
|
457
461
|
let currentCrontab = '';
|
|
458
462
|
try { currentCrontab = autoRun('crontab -l'); } catch { /* no existing crontab */ }
|
|
459
463
|
|
|
460
|
-
// Strip existing SmashOS block
|
|
464
|
+
// Strip existing SmashOS block (find end dynamically to handle variable entry counts)
|
|
461
465
|
const lines = currentCrontab.split('\n');
|
|
462
466
|
const startIdx = lines.findIndex(l => l === smashMarker);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
467
|
+
let filteredLines;
|
|
468
|
+
if (startIdx >= 0) {
|
|
469
|
+
let endIdx = lines.length;
|
|
470
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
471
|
+
if (lines[i].trim() === '' || (!lines[i].startsWith('#') && !lines[i].includes('smash-os') && lines[i].trim() !== '')) {
|
|
472
|
+
endIdx = i - 1;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
filteredLines = lines.filter((_, i) => i < startIdx || i > endIdx);
|
|
477
|
+
} else {
|
|
478
|
+
filteredLines = lines;
|
|
479
|
+
}
|
|
466
480
|
|
|
467
481
|
const newCrontab = [...filteredLines.filter(l => l !== ''), '', smashMarker, ...entries, ''].join('\n');
|
|
468
482
|
|
|
@@ -476,8 +490,8 @@ function setupLinuxCron(projects, claudeExe, projectName) {
|
|
|
476
490
|
}
|
|
477
491
|
|
|
478
492
|
console.log('');
|
|
479
|
-
console.log(chalk.dim(' Schedule: hourly lock-cleanup · 1am memory · 2am audit · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution'));
|
|
480
|
-
console.log(chalk.dim(` Logs: ${
|
|
493
|
+
console.log(chalk.dim(' Schedule: hourly lock-cleanup · 1am memory · 2am audit · 3am log-report · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution · Mon 7am harness-evolution'));
|
|
494
|
+
console.log(chalk.dim(` Logs: ${primaryLogDir}`));
|
|
481
495
|
}
|
|
482
496
|
|
|
483
497
|
// ─── Platform-agnostic automation dispatcher ──────────────────────────────────
|
|
@@ -542,12 +556,15 @@ async function runUpgrade() {
|
|
|
542
556
|
const { toUpdate } = await prompts({
|
|
543
557
|
type: 'multiselect',
|
|
544
558
|
name: 'toUpdate',
|
|
545
|
-
message: 'What to update? (
|
|
559
|
+
message: 'What to update? (.smashOS/memory/ and .smashOS/context/ are never touched)',
|
|
546
560
|
choices: [
|
|
547
561
|
{ title: 'Agents (.claude/agents/) — sub-agent definitions', value: 'agents', selected: true },
|
|
548
562
|
{ title: 'Hooks (.claude/hooks/) — boot + sync hooks', value: 'hooks', selected: true },
|
|
549
563
|
{ title: 'Skills (~/.claude/skills/) — slash commands', value: 'skills', selected: true },
|
|
550
|
-
{ title: '
|
|
564
|
+
{ title: 'Docs (.smashOS/docs/) — SmashOS documentation', value: 'docs', selected: true },
|
|
565
|
+
{ title: 'Workflows (.smashOS/workflows/) — pipeline definitions', value: 'workflows', selected: true },
|
|
566
|
+
{ title: 'Roles (.smashOS/roles/) — role definitions', value: 'roles', selected: false },
|
|
567
|
+
{ title: 'CLAUDE.md — patch missing sections (non-destructive)', value: 'claudemd', selected: true },
|
|
551
568
|
],
|
|
552
569
|
hint: '← Space to toggle, Enter to confirm',
|
|
553
570
|
}, { onCancel: () => { console.log(chalk.yellow('\n Upgrade cancelled.')); process.exit(0); } });
|
|
@@ -592,17 +609,73 @@ async function runUpgrade() {
|
|
|
592
609
|
}
|
|
593
610
|
}
|
|
594
611
|
|
|
612
|
+
if (toUpdate.includes('docs')) {
|
|
613
|
+
console.log(chalk.dim(' Updating docs...'));
|
|
614
|
+
for (const [relPath, content] of Object.entries(osDocsFiles)) {
|
|
615
|
+
writeFile(relPath, content);
|
|
616
|
+
console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Detect frontend project from installed files (avoids hardcoding isFrontend=false)
|
|
621
|
+
const isFrontend = existsSync(join(cwd, '.smashOS', 'roles', 'frontend-developer.md'));
|
|
622
|
+
|
|
623
|
+
if (toUpdate.includes('workflows')) {
|
|
624
|
+
console.log(chalk.dim(' Updating workflows...'));
|
|
625
|
+
const workflowFiles = getAiFiles(basename(cwd), 'TypeScript', isFrontend, '', '', '');
|
|
626
|
+
for (const [relPath, content] of Object.entries(workflowFiles)) {
|
|
627
|
+
if (relPath.startsWith('.smashOS/workflows/')) {
|
|
628
|
+
writeFile(relPath, content);
|
|
629
|
+
console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
595
634
|
if (toUpdate.includes('roles')) {
|
|
596
635
|
console.log(chalk.dim(' Updating AI roles...'));
|
|
597
|
-
const roleFiles = getAiFiles(basename(cwd), 'TypeScript',
|
|
636
|
+
const roleFiles = getAiFiles(basename(cwd), 'TypeScript', isFrontend, '', '', '');
|
|
598
637
|
for (const [relPath, content] of Object.entries(roleFiles)) {
|
|
599
|
-
if (relPath.startsWith('
|
|
638
|
+
if (relPath.startsWith('.smashOS/roles/')) {
|
|
600
639
|
writeFile(relPath, content);
|
|
601
640
|
console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
|
|
602
641
|
}
|
|
603
642
|
}
|
|
604
643
|
}
|
|
605
644
|
|
|
645
|
+
if (toUpdate.includes('claudemd')) {
|
|
646
|
+
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
647
|
+
if (existsSync(claudeMdPath)) {
|
|
648
|
+
const existing = readFileSync(claudeMdPath, 'utf8');
|
|
649
|
+
if (!existing.includes('## Agent Dispatch Rules')) {
|
|
650
|
+
const agentDispatchSection = `
|
|
651
|
+
## Agent Dispatch Rules
|
|
652
|
+
|
|
653
|
+
**Fresh agents** (use \`subagent_type\`): any agent that writes files, produces a verdict, or gates the pipeline.
|
|
654
|
+
**Forks** (no \`subagent_type\`): noisy intermediate work — only the structured summary returns to main context.
|
|
655
|
+
|
|
656
|
+
**Parallel dispatch:**
|
|
657
|
+
- Independent read-only agents: fire in a single message (same Agent tool call batch)
|
|
658
|
+
- Write agents on non-overlapping files: dispatch with \`isolation: "worktree"\`
|
|
659
|
+
- Maximum 3 parallel agents at once; merge before proceeding
|
|
660
|
+
|
|
661
|
+
**Background agents:**
|
|
662
|
+
- Long-running read-only work (test suite, large scans): use \`run_in_background: true\`
|
|
663
|
+
- Collect background results before the synthesis/reporter step
|
|
664
|
+
|
|
665
|
+
**Brief discipline:** Write a 3-5 sentence brief per agent — what to do, exact file paths, output schema, pipeline context. Do NOT pass a context dump.
|
|
666
|
+
|
|
667
|
+
`;
|
|
668
|
+
const patched = existing.replace(/(\n## Tech Stack\b)/, `${agentDispatchSection}$1`);
|
|
669
|
+
writeFileSync(claudeMdPath, patched, 'utf8');
|
|
670
|
+
console.log(' ' + chalk.green('✓') + ' CLAUDE.md' + chalk.dim(' (Agent Dispatch Rules patched in)'));
|
|
671
|
+
} else {
|
|
672
|
+
console.log(' ' + chalk.dim('·') + ' CLAUDE.md already has Agent Dispatch Rules — skipped');
|
|
673
|
+
}
|
|
674
|
+
} else {
|
|
675
|
+
console.log(' ' + chalk.yellow('⚠') + ' CLAUDE.md not found — skipped');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
606
679
|
writeFile('.smash-os-version', SMASH_VERSION + '\n');
|
|
607
680
|
console.log('');
|
|
608
681
|
console.log(chalk.bold.green(` ✓ SmashOS upgraded to v${SMASH_VERSION}`));
|
|
@@ -616,14 +689,37 @@ async function runVerify() {
|
|
|
616
689
|
const checks = [
|
|
617
690
|
['.smash-os-mode', 'mode flag'],
|
|
618
691
|
['CLAUDE.md', 'CLAUDE.md'],
|
|
619
|
-
['
|
|
620
|
-
['
|
|
621
|
-
['
|
|
622
|
-
['
|
|
623
|
-
['
|
|
624
|
-
['
|
|
692
|
+
['.smashOS/orchestrator.md', 'orchestrator'],
|
|
693
|
+
['.smashOS/context/product.md', 'product context'],
|
|
694
|
+
['.smashOS/context/architecture.md', 'architecture context'],
|
|
695
|
+
['.smashOS/context/coding-standards.md', 'coding standards'],
|
|
696
|
+
['.smashOS/context/evaluation-rubrics.md', 'evaluation rubrics'],
|
|
697
|
+
['.smashOS/roles/staff-engineer.md', 'Staff Engineer role'],
|
|
698
|
+
['.smashOS/roles/senior-developer.md', 'Senior Developer role'],
|
|
699
|
+
['.smashOS/roles/qa-engineer.md', 'QA Engineer role'],
|
|
700
|
+
['.smashOS/roles/security-engineer.md', 'Security Engineer role'],
|
|
701
|
+
['.smashOS/roles/product-manager.md', 'Product Manager role'],
|
|
702
|
+
['.smashOS/roles/devops-engineer.md', 'DevOps Engineer role'],
|
|
703
|
+
['.smashOS/workflows/feature.md', 'feature workflow'],
|
|
704
|
+
['.smashOS/workflows/harness-evolution.md', 'harness-evolution workflow'],
|
|
705
|
+
['.smashOS/docs/01-quickstart.md', 'docs'],
|
|
706
|
+
['.claude/agents/senior-dev-implementation-planner.md', 'senior-dev-implementation-planner agent'],
|
|
707
|
+
['.claude/agents/qa-test-runner.md', 'qa-test-runner agent'],
|
|
708
|
+
['.claude/agents/senior-dev-code-writer.md', 'senior-dev-code-writer agent'],
|
|
709
|
+
['.claude/agents/senior-dev-test-writer.md', 'senior-dev-test-writer agent'],
|
|
710
|
+
['.claude/agents/devops-deploy-runner.md', 'devops-deploy-runner agent'],
|
|
711
|
+
['.claude/agents/devops-smoke-tester.md', 'devops-smoke-tester agent'],
|
|
712
|
+
['.smashOS/context/hard-rules.md', 'hard rules'],
|
|
713
|
+
['.smashOS/context/memory-schema.md', 'memory schema'],
|
|
714
|
+
['.smashOS/context/pipeline-continuation.md', 'pipeline continuation'],
|
|
715
|
+
['.smashOS/context/role-protocols.md', 'role protocols'],
|
|
716
|
+
['.smashOS/workflows/bug-fix.md', 'bug-fix workflow'],
|
|
717
|
+
['.smashOS/workflows/weekly-improvement.md', 'weekly-improvement workflow'],
|
|
718
|
+
['.smashOS/roles/ui-ux-designer.md', 'UI/UX Designer role'],
|
|
719
|
+
['.smashOS/roles/frontend-developer.md', 'Frontend Developer role'],
|
|
625
720
|
['.claude/settings.json', 'settings.json'],
|
|
626
721
|
['.claude/hooks/smash-os-boot.mjs', 'boot hook'],
|
|
722
|
+
['.claude/hooks/smash-os-sync.mjs', 'sync hook'],
|
|
627
723
|
];
|
|
628
724
|
let passed = 0;
|
|
629
725
|
let failed = 0;
|
|
@@ -661,7 +757,7 @@ async function runUninstall() {
|
|
|
661
757
|
console.log(chalk.bold(' SmashOS Uninstall'));
|
|
662
758
|
console.log('');
|
|
663
759
|
console.log(chalk.dim(' This will remove all SmashOS harness files from this project.'));
|
|
664
|
-
console.log(chalk.yellow(' ⚠
|
|
760
|
+
console.log(chalk.yellow(' ⚠ .smashOS/memory/ and .smashOS/context/ will be preserved by default.'));
|
|
665
761
|
console.log('');
|
|
666
762
|
|
|
667
763
|
const { confirm } = await prompts({
|
|
@@ -679,7 +775,7 @@ async function runUninstall() {
|
|
|
679
775
|
const { removeUserData } = await prompts({
|
|
680
776
|
type: 'confirm',
|
|
681
777
|
name: 'removeUserData',
|
|
682
|
-
message: 'Also delete
|
|
778
|
+
message: 'Also delete .smashOS/memory/ and .smashOS/context/? (your project notes — NOT recommended)',
|
|
683
779
|
initial: false,
|
|
684
780
|
}, { onCancel: () => ({ removeUserData: false }) });
|
|
685
781
|
|
|
@@ -699,18 +795,29 @@ async function runUninstall() {
|
|
|
699
795
|
remove('CLAUDE.md', 'CLAUDE.md');
|
|
700
796
|
|
|
701
797
|
// Harness directories (never touch user content by default)
|
|
702
|
-
remove('.claude-state', '.claude-state/');
|
|
703
798
|
remove('.planning', '.planning/');
|
|
704
|
-
remove('
|
|
705
|
-
remove('
|
|
706
|
-
remove('
|
|
799
|
+
remove('.smashOS/orchestrator.md', '.smashOS/orchestrator.md');
|
|
800
|
+
remove('.smashOS/roles', '.smashOS/roles/');
|
|
801
|
+
remove('.smashOS/workflows', '.smashOS/workflows/');
|
|
802
|
+
remove('.smashOS/docs', '.smashOS/docs/');
|
|
707
803
|
remove('.claude/agents', '.claude/agents/');
|
|
708
804
|
remove('.claude/hooks/smash-os-boot.mjs', '.claude/hooks/smash-os-boot.mjs');
|
|
709
805
|
remove('.claude/hooks/smash-os-sync.mjs', '.claude/hooks/smash-os-sync.mjs');
|
|
710
806
|
|
|
711
807
|
if (removeUserData) {
|
|
712
|
-
remove('
|
|
713
|
-
remove('
|
|
808
|
+
remove('.claude-state', '.claude-state/');
|
|
809
|
+
remove('.smashOS/memory', '.smashOS/memory/');
|
|
810
|
+
remove('.smashOS/context', '.smashOS/context/');
|
|
811
|
+
remove('.smashOS/todo', '.smashOS/todo/');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Remove globally-installed skills
|
|
815
|
+
for (const skillName of Object.keys(localSkills)) {
|
|
816
|
+
const skillDir = join(homedir(), '.claude', 'skills', skillName);
|
|
817
|
+
if (existsSync(skillDir)) {
|
|
818
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
819
|
+
console.log(' ' + chalk.red('–') + ' ' + chalk.white(`~/.claude/skills/${skillName}/`));
|
|
820
|
+
}
|
|
714
821
|
}
|
|
715
822
|
|
|
716
823
|
// Scrub smash-os hooks from .claude/settings.json
|
|
@@ -721,8 +828,10 @@ async function runUninstall() {
|
|
|
721
828
|
if (s.hooks) {
|
|
722
829
|
for (const event of Object.keys(s.hooks)) {
|
|
723
830
|
s.hooks[event] = (s.hooks[event] || []).filter(h => {
|
|
724
|
-
|
|
725
|
-
return !
|
|
831
|
+
if (typeof h === 'string') return !h.includes('smash-os');
|
|
832
|
+
if (h.command) return !h.command.includes('smash-os');
|
|
833
|
+
if (h.hooks) return !h.hooks.some(inner => inner.command?.includes('smash-os'));
|
|
834
|
+
return true;
|
|
726
835
|
});
|
|
727
836
|
if (s.hooks[event].length === 0) delete s.hooks[event];
|
|
728
837
|
}
|
|
@@ -748,15 +857,13 @@ async function runUninstall() {
|
|
|
748
857
|
// macOS: unload and remove LaunchAgents
|
|
749
858
|
if (process.platform === 'darwin') {
|
|
750
859
|
const laDir = join(homedir(), 'Library', 'LaunchAgents');
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
for (const
|
|
754
|
-
const p = join(laDir,
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
console.log(' ' + chalk.red('–') + ` com.smash-os.${name}.plist`);
|
|
759
|
-
}
|
|
860
|
+
let smashPlists = [];
|
|
861
|
+
try { smashPlists = readdirSync(laDir).filter(f => f.startsWith('com.smash-os.') && f.endsWith('.plist')); } catch { /* dir not found */ }
|
|
862
|
+
for (const plistFile of smashPlists) {
|
|
863
|
+
const p = join(laDir, plistFile);
|
|
864
|
+
try { execSync(`launchctl unload "${p}"`, { stdio: 'pipe' }); } catch { /* not loaded */ }
|
|
865
|
+
rmSync(p, { force: true });
|
|
866
|
+
console.log(' ' + chalk.red('–') + ` ${plistFile}`);
|
|
760
867
|
}
|
|
761
868
|
}
|
|
762
869
|
|
|
@@ -768,7 +875,15 @@ async function runUninstall() {
|
|
|
768
875
|
if (current.includes(marker)) {
|
|
769
876
|
const lines = current.split('\n');
|
|
770
877
|
const start = lines.findIndex(l => l === marker);
|
|
771
|
-
|
|
878
|
+
// Find block end dynamically: next blank line or end of file after the marker
|
|
879
|
+
let end = lines.length;
|
|
880
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
881
|
+
if (lines[i].trim() === '' || (!lines[i].startsWith('#') && !lines[i].includes('smash-os') && lines[i].trim() !== '')) {
|
|
882
|
+
end = i - 1;
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const filtered = lines.filter((_, i) => i < start || i > end).filter(l => l !== '');
|
|
772
887
|
execSync('crontab -', { input: filtered.join('\n') + '\n', encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
773
888
|
console.log(' ' + chalk.red('–') + ' Linux crontab: SmashOS entries removed');
|
|
774
889
|
}
|
|
@@ -1009,6 +1124,14 @@ if (isFrontend) {
|
|
|
1009
1124
|
}
|
|
1010
1125
|
}
|
|
1011
1126
|
|
|
1127
|
+
console.log('');
|
|
1128
|
+
console.log(chalk.dim(' Docs (.smashOS/docs/):'));
|
|
1129
|
+
for (const [relPath, content] of Object.entries(osDocsFiles)) {
|
|
1130
|
+
writeFile(relPath, content);
|
|
1131
|
+
console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
|
|
1132
|
+
written++;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1012
1135
|
writeFile('.claude/hooks/smash-os-boot.mjs', bootHook);
|
|
1013
1136
|
console.log(' ' + chalk.green('✓') + ' ' + chalk.white('.claude/hooks/smash-os-boot.mjs'));
|
|
1014
1137
|
written++;
|
|
@@ -1049,7 +1172,7 @@ console.log(' ' + chalk.white('/smash-os:status') + chalk.dim('
|
|
|
1049
1172
|
console.log(' ' + chalk.white('/smash-os:doctor') + chalk.dim(' — diagnose any missing files'));
|
|
1050
1173
|
console.log(' ' + chalk.white('/smash-os:run feature "Add X"') + chalk.dim(' — run your first pipeline'));
|
|
1051
1174
|
console.log('');
|
|
1052
|
-
console.log(chalk.dim(' Next: fill in
|
|
1175
|
+
console.log(chalk.dim(' Next: fill in .smashOS/context/ with your project details.'));
|
|
1053
1176
|
console.log('');
|
|
1054
1177
|
|
|
1055
1178
|
// ── Guided tour opt-in ────────────────────────────────────────────────────────
|
|
@@ -1072,9 +1195,9 @@ if (wantsTour) {
|
|
|
1072
1195
|
console.log(chalk.dim(' Shows your harness state, recent decisions, and pipeline health.'));
|
|
1073
1196
|
console.log('');
|
|
1074
1197
|
console.log(' 3. Fill in your project context:');
|
|
1075
|
-
console.log(' ' + chalk.white('
|
|
1076
|
-
console.log(' ' + chalk.white('
|
|
1077
|
-
console.log(' ' + chalk.white('
|
|
1198
|
+
console.log(' ' + chalk.white('.smashOS/context/product.md') + chalk.dim(' ← what your product does'));
|
|
1199
|
+
console.log(' ' + chalk.white('.smashOS/context/architecture.md') + chalk.dim(' ← your tech stack and key decisions'));
|
|
1200
|
+
console.log(' ' + chalk.white('.smashOS/context/coding-standards.md') + chalk.dim(' ← your style rules'));
|
|
1078
1201
|
console.log('');
|
|
1079
1202
|
console.log(' 4. Run your first pipeline:');
|
|
1080
1203
|
console.log(' ' + chalk.white('/smash-os:run feature "Add a health check endpoint"'));
|
|
@@ -1089,5 +1212,3 @@ if (wantsTour) {
|
|
|
1089
1212
|
|
|
1090
1213
|
if (setupAutomation) runAutomation(cwd);
|
|
1091
1214
|
|
|
1092
|
-
console.log(chalk.dim(' Optional: run /smash-os:setup-mcps to add MCP integrations (context7, playwright, chrome-devtools, etc.)'));
|
|
1093
|
-
console.log('');
|