thepopebot 1.2.75-beta.15 → 1.2.75-beta.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +194 -4
- package/lib/chat/components/code-mode-toggle.js +0 -1
- package/lib/chat/components/code-mode-toggle.jsx +0 -3
- package/lib/code/terminal-view.js +9 -0
- package/lib/code/terminal-view.jsx +9 -0
- package/package.json +1 -1
- package/templates/.gitignore.template +1 -1
- package/templates/data/CLAUDE.md.template +5 -0
- package/templates/logs/CLAUDE.md.template +5 -0
- package/templates/.pi/extensions/env-sanitizer/index.ts +0 -48
- package/templates/.pi/extensions/env-sanitizer/package.json +0 -5
- package/templates/agents/.gitkeep +0 -0
- package/templates/logs/.gitkeep +0 -0
package/bin/cli.js
CHANGED
|
@@ -56,6 +56,7 @@ Commands:
|
|
|
56
56
|
setup-telegram Reconfigure Telegram webhook
|
|
57
57
|
reset-auth Regenerate AUTH_SECRET (invalidates all sessions)
|
|
58
58
|
reset [file] Restore a template file (or list available templates)
|
|
59
|
+
reset-all Nuclear reset — restore entire project to fresh init state
|
|
59
60
|
diff [file] Show differences between project files and package templates
|
|
60
61
|
sync <path> Sync local package to a test install (build, pack, Docker)
|
|
61
62
|
sync --fast <path> Fast sync — copy source into running container, rebuild .next
|
|
@@ -211,10 +212,13 @@ async function init() {
|
|
|
211
212
|
const tmplPath = templatePath(relPath, templatesDir);
|
|
212
213
|
const templateExists = fs.existsSync(path.join(templatesDir, tmplPath));
|
|
213
214
|
if (!templateExists) {
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
const bd = getBackupDir();
|
|
216
|
+
const dest = path.join(bd, relPath);
|
|
217
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
218
|
+
fs.renameSync(fullPath, dest);
|
|
219
|
+
backedUp.push(relPath);
|
|
216
220
|
deleted.push(relPath);
|
|
217
|
-
console.log(`
|
|
221
|
+
console.log(` Removed ${relPath} (stale managed file)`);
|
|
218
222
|
}
|
|
219
223
|
}
|
|
220
224
|
}
|
|
@@ -370,6 +374,36 @@ THEPOPEBOT_VERSION=${version}
|
|
|
370
374
|
console.log('\nDone! Run: npm run setup\n');
|
|
371
375
|
}
|
|
372
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Create a timestamped backup directory and return { dir, ts }.
|
|
379
|
+
*/
|
|
380
|
+
function createBackupDir(cwd) {
|
|
381
|
+
const now = new Date();
|
|
382
|
+
const ts = now.getFullYear().toString()
|
|
383
|
+
+ String(now.getMonth() + 1).padStart(2, '0')
|
|
384
|
+
+ String(now.getDate()).padStart(2, '0')
|
|
385
|
+
+ '-'
|
|
386
|
+
+ String(now.getHours()).padStart(2, '0')
|
|
387
|
+
+ String(now.getMinutes()).padStart(2, '0')
|
|
388
|
+
+ String(now.getSeconds()).padStart(2, '0');
|
|
389
|
+
return { dir: path.join(cwd, '.backups', ts), ts };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Move a file or symlink to the backup directory.
|
|
394
|
+
*/
|
|
395
|
+
function backupAndRemove(fullPath, relPath, backupDir) {
|
|
396
|
+
const dest = path.join(backupDir, relPath);
|
|
397
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
398
|
+
if (fs.lstatSync(fullPath).isSymbolicLink()) {
|
|
399
|
+
const target = fs.readlinkSync(fullPath);
|
|
400
|
+
fs.symlinkSync(target, dest);
|
|
401
|
+
fs.unlinkSync(fullPath);
|
|
402
|
+
} else {
|
|
403
|
+
fs.renameSync(fullPath, dest);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
373
407
|
/**
|
|
374
408
|
* List all available template files, or restore a specific one.
|
|
375
409
|
*/
|
|
@@ -399,13 +433,38 @@ function reset(filePath) {
|
|
|
399
433
|
process.exit(1);
|
|
400
434
|
}
|
|
401
435
|
|
|
436
|
+
// Back up existing file before overwriting
|
|
437
|
+
if (fs.existsSync(dest)) {
|
|
438
|
+
const { dir, ts } = createBackupDir(cwd);
|
|
439
|
+
if (fs.statSync(src).isDirectory()) {
|
|
440
|
+
// Back up all files in the directory
|
|
441
|
+
function walkBackup(d) {
|
|
442
|
+
const items = fs.readdirSync(d, { withFileTypes: true });
|
|
443
|
+
for (const item of items) {
|
|
444
|
+
const full = path.join(d, item.name);
|
|
445
|
+
const rel = path.relative(cwd, full);
|
|
446
|
+
if (item.isDirectory()) {
|
|
447
|
+
walkBackup(full);
|
|
448
|
+
} else {
|
|
449
|
+
backupAndRemove(full, rel, dir);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
walkBackup(dest);
|
|
454
|
+
console.log(`\n Backed up to .backups/${ts}/`);
|
|
455
|
+
} else {
|
|
456
|
+
backupAndRemove(dest, filePath, dir);
|
|
457
|
+
console.log(`\n Backed up to .backups/${ts}/${filePath}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
402
461
|
if (fs.statSync(src).isDirectory()) {
|
|
403
462
|
console.log(`\nRestoring ${filePath}/...\n`);
|
|
404
463
|
copyDirSyncForce(src, dest, tmplPath);
|
|
405
464
|
} else {
|
|
406
465
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
407
466
|
fs.copyFileSync(src, dest);
|
|
408
|
-
console.log(
|
|
467
|
+
console.log(`Restored ${filePath}\n`);
|
|
409
468
|
}
|
|
410
469
|
}
|
|
411
470
|
|
|
@@ -491,6 +550,134 @@ function copyDirSyncForce(src, dest, templateRelBase = '') {
|
|
|
491
550
|
}
|
|
492
551
|
}
|
|
493
552
|
|
|
553
|
+
// Paths that reset-all must never touch (relative to project root).
|
|
554
|
+
// Entries ending with '/' are directory prefixes.
|
|
555
|
+
const PROTECTED_PATHS = [
|
|
556
|
+
'.env',
|
|
557
|
+
'.env.local',
|
|
558
|
+
'data/',
|
|
559
|
+
'logs/',
|
|
560
|
+
'.git/',
|
|
561
|
+
'.backups/',
|
|
562
|
+
'package-lock.json',
|
|
563
|
+
'package.json',
|
|
564
|
+
'docker-compose.custom.yml',
|
|
565
|
+
'.claude/',
|
|
566
|
+
'.pi/',
|
|
567
|
+
'skills/',
|
|
568
|
+
'node_modules/',
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
function isProtected(relPath) {
|
|
572
|
+
return PROTECTED_PATHS.some(p =>
|
|
573
|
+
p.endsWith('/') ? relPath === p.slice(0, -1) || relPath.startsWith(p) : relPath === p
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function resetAll() {
|
|
578
|
+
const cwd = process.cwd();
|
|
579
|
+
|
|
580
|
+
// Verify this is a thepopebot project
|
|
581
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
582
|
+
if (!fs.existsSync(pkgPath)) {
|
|
583
|
+
console.error('\n Not a thepopebot project (no package.json found).\n');
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
587
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
588
|
+
if (!deps.thepopebot) {
|
|
589
|
+
console.error('\n Not a thepopebot project (thepopebot not in dependencies).\n');
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Dry run — collect all files that would be moved
|
|
594
|
+
const filesToMove = [];
|
|
595
|
+
function collectFiles(dir) {
|
|
596
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
597
|
+
for (const item of items) {
|
|
598
|
+
const fullPath = path.join(dir, item.name);
|
|
599
|
+
const relPath = path.relative(cwd, fullPath);
|
|
600
|
+
if (isProtected(relPath)) continue;
|
|
601
|
+
if (item.isDirectory() && !item.isSymbolicLink()) {
|
|
602
|
+
collectFiles(fullPath);
|
|
603
|
+
} else {
|
|
604
|
+
filesToMove.push(relPath);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
collectFiles(cwd);
|
|
609
|
+
|
|
610
|
+
if (filesToMove.length === 0) {
|
|
611
|
+
console.log('\n Nothing to reset — no non-protected files found.\n');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const { confirm, isCancel } = await import('@clack/prompts');
|
|
616
|
+
|
|
617
|
+
console.log('\n This will reset your entire project to a fresh thepopebot init state.');
|
|
618
|
+
console.log(` ${filesToMove.length} file(s) will be moved to .backups/:\n`);
|
|
619
|
+
for (const f of filesToMove) {
|
|
620
|
+
console.log(` ${f}`);
|
|
621
|
+
}
|
|
622
|
+
console.log('\n Protected (will NOT be touched):');
|
|
623
|
+
for (const p of PROTECTED_PATHS) {
|
|
624
|
+
console.log(` ${p}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const ok = await confirm({ message: '\nAre you sure? This is the nuclear option.' });
|
|
628
|
+
if (isCancel(ok) || !ok) {
|
|
629
|
+
console.log('\n Cancelled.\n');
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Move all files to backup
|
|
634
|
+
const { dir: backupDir, ts } = createBackupDir(cwd);
|
|
635
|
+
|
|
636
|
+
for (const relPath of filesToMove) {
|
|
637
|
+
const fullPath = path.join(cwd, relPath);
|
|
638
|
+
backupAndRemove(fullPath, relPath, backupDir);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Remove empty directories left behind
|
|
642
|
+
function removeEmptyDirs(dir) {
|
|
643
|
+
if (!fs.existsSync(dir)) return;
|
|
644
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
645
|
+
for (const item of items) {
|
|
646
|
+
if (item.isDirectory()) {
|
|
647
|
+
removeEmptyDirs(path.join(dir, item.name));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const relPath = path.relative(cwd, dir);
|
|
651
|
+
if (relPath && !isProtected(relPath) && fs.readdirSync(dir).length === 0) {
|
|
652
|
+
fs.rmdirSync(dir);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
removeEmptyDirs(cwd);
|
|
656
|
+
|
|
657
|
+
console.log(`\n Moved ${filesToMove.length} file(s) to .backups/${ts}/`);
|
|
658
|
+
|
|
659
|
+
// Run init to rebuild from templates
|
|
660
|
+
console.log('\n Running init to rebuild project...\n');
|
|
661
|
+
try {
|
|
662
|
+
execSync('npx thepopebot init --no-install', { stdio: 'inherit', cwd });
|
|
663
|
+
} catch {
|
|
664
|
+
console.error('\n Init failed. Your backup is at .backups/' + ts + '/\n');
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Run npm install separately
|
|
669
|
+
console.log('\nInstalling dependencies...\n');
|
|
670
|
+
try {
|
|
671
|
+
execSync('npm install', { stdio: 'inherit', cwd });
|
|
672
|
+
} catch {
|
|
673
|
+
console.error('\n npm install failed. Your backup is at .backups/' + ts + '/\n');
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
console.log('\n Reset complete. Project restored to fresh init state.');
|
|
678
|
+
console.log(` Backup: .backups/${ts}/\n`);
|
|
679
|
+
}
|
|
680
|
+
|
|
494
681
|
function setup() {
|
|
495
682
|
const setupScript = path.join(__dirname, '..', 'setup', 'setup.mjs');
|
|
496
683
|
try {
|
|
@@ -801,6 +988,9 @@ switch (command) {
|
|
|
801
988
|
case 'reset':
|
|
802
989
|
reset(args[0]);
|
|
803
990
|
break;
|
|
991
|
+
case 'reset-all':
|
|
992
|
+
await resetAll();
|
|
993
|
+
break;
|
|
804
994
|
case 'diff':
|
|
805
995
|
diff(args[0]);
|
|
806
996
|
break;
|
|
@@ -293,7 +293,6 @@ function WorkspaceCommandButton({ workspaceId, diffStats, onDiffStatsRefresh, on
|
|
|
293
293
|
onClick: onShowDiff,
|
|
294
294
|
className: "text-xs leading-4 px-2.5 h-[28px] flex items-center gap-1.5 font-medium border border-border rounded-md whitespace-nowrap hover:bg-accent transition-colors cursor-pointer",
|
|
295
295
|
children: [
|
|
296
|
-
diffStats?.currentBranch && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground truncate max-w-[120px]", title: diffStats.currentBranch, children: diffStats.currentBranch }),
|
|
297
296
|
/* @__PURE__ */ jsxs("span", { className: "text-green-500", children: [
|
|
298
297
|
"+",
|
|
299
298
|
diffStats?.insertions ?? 0
|
|
@@ -341,9 +341,6 @@ function WorkspaceCommandButton({ workspaceId, diffStats, onDiffStatsRefresh, on
|
|
|
341
341
|
onClick={onShowDiff}
|
|
342
342
|
className="text-xs leading-4 px-2.5 h-[28px] flex items-center gap-1.5 font-medium border border-border rounded-md whitespace-nowrap hover:bg-accent transition-colors cursor-pointer"
|
|
343
343
|
>
|
|
344
|
-
{diffStats?.currentBranch && (
|
|
345
|
-
<span className="text-muted-foreground truncate max-w-[120px]" title={diffStats.currentBranch}>{diffStats.currentBranch}</span>
|
|
346
|
-
)}
|
|
347
344
|
<span className="text-green-500">+{diffStats?.insertions ?? 0}</span>
|
|
348
345
|
<span className="text-destructive">-{diffStats?.deletions ?? 0}</span>
|
|
349
346
|
</button>
|
|
@@ -675,6 +675,15 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
675
675
|
] })
|
|
676
676
|
] })
|
|
677
677
|
] }),
|
|
678
|
+
diffStats?.currentBranch && /* @__PURE__ */ jsx(
|
|
679
|
+
"button",
|
|
680
|
+
{
|
|
681
|
+
className: "code-toolbar-btn",
|
|
682
|
+
style: { cursor: "default", opacity: 0.7, maxWidth: 140, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
683
|
+
title: diffStats.currentBranch,
|
|
684
|
+
children: diffStats.currentBranch
|
|
685
|
+
}
|
|
686
|
+
),
|
|
678
687
|
/* @__PURE__ */ jsx(
|
|
679
688
|
ToolbarCommandButton,
|
|
680
689
|
{
|
|
@@ -741,6 +741,15 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
741
741
|
)}
|
|
742
742
|
</div>
|
|
743
743
|
)}
|
|
744
|
+
{diffStats?.currentBranch && (
|
|
745
|
+
<button
|
|
746
|
+
className="code-toolbar-btn"
|
|
747
|
+
style={{ cursor: 'default', opacity: 0.7, maxWidth: 140, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
|
|
748
|
+
title={diffStats.currentBranch}
|
|
749
|
+
>
|
|
750
|
+
{diffStats.currentBranch}
|
|
751
|
+
</button>
|
|
752
|
+
)}
|
|
744
753
|
<ToolbarCommandButton
|
|
745
754
|
codeWorkspaceId={codeWorkspaceId}
|
|
746
755
|
diffStats={diffStats}
|
package/package.json
CHANGED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Env Sanitizer Extension - Protects credentials from AI agent access
|
|
3
|
-
*
|
|
4
|
-
* Uses Pi's spawnHook to filter sensitive env vars from bash subprocess calls
|
|
5
|
-
* while keeping them available in the main process for:
|
|
6
|
-
* - Anthropic SDK (needs ANTHROPIC_API_KEY at init)
|
|
7
|
-
* - GitHub CLI (needs GH_TOKEN)
|
|
8
|
-
* - Other extensions that may need credentials
|
|
9
|
-
*
|
|
10
|
-
* Dynamically filters all keys defined in the SECRETS JSON env var.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
14
|
-
import { createBashTool } from "@mariozechner/pi-coding-agent";
|
|
15
|
-
|
|
16
|
-
// Parse SECRETS JSON to get list of keys to filter
|
|
17
|
-
function getSecretKeys(): string[] {
|
|
18
|
-
const keys: string[] = [];
|
|
19
|
-
if (process.env.SECRETS) {
|
|
20
|
-
try {
|
|
21
|
-
const secrets = JSON.parse(process.env.SECRETS);
|
|
22
|
-
keys.push(...Object.keys(secrets));
|
|
23
|
-
} catch {
|
|
24
|
-
// Invalid JSON, ignore
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
// Always filter SECRETS itself
|
|
28
|
-
keys.push("SECRETS");
|
|
29
|
-
return [...new Set(keys)]; // Dedupe
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export default function (pi: ExtensionAPI) {
|
|
33
|
-
const secretKeys = getSecretKeys();
|
|
34
|
-
|
|
35
|
-
// Override bash tool with filtered environment for subprocesses
|
|
36
|
-
const bashTool = createBashTool(process.cwd(), {
|
|
37
|
-
spawnHook: ({ command, cwd, env }) => {
|
|
38
|
-
// Filter all secret keys from subprocess environment
|
|
39
|
-
const filteredEnv = { ...env };
|
|
40
|
-
for (const key of secretKeys) {
|
|
41
|
-
delete filteredEnv[key];
|
|
42
|
-
}
|
|
43
|
-
return { command, cwd, env: filteredEnv };
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
pi.registerTool(bashTool);
|
|
48
|
-
}
|
|
File without changes
|
package/templates/logs/.gitkeep
DELETED
|
File without changes
|