@piut/cli 1.0.2 → 1.1.0
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 +58 -25
- package/dist/cli.js +1163 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pıut
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Your AI context, everywhere. Your config files, backed up.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
pıut does three things that build on each other:
|
|
6
|
+
|
|
7
|
+
1. **Host your brain** — Centralized personal context as an MCP server. Connect once, and every AI tool you use knows who you are, how you work, and what matters to you.
|
|
8
|
+
2. **Keep it updated** — 6 MCP tools let your AI read, write, search, and organize your context automatically. Add a skill reference and it happens without you lifting a finger.
|
|
9
|
+
3. **Back up your files** — Cloud backup of all your agent config files (CLAUDE.md, .cursorrules, AGENTS.md, etc.) with version history, restore, and cross-machine sync.
|
|
6
10
|
|
|
7
11
|
## Quick Start
|
|
8
12
|
|
|
@@ -10,7 +14,10 @@ pıut is a personal context service that works via [MCP (Model Context Protocol)
|
|
|
10
14
|
npx @piut/cli
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
The CLI does everything in one interactive flow:
|
|
18
|
+
- Configures your AI tools with your MCP server
|
|
19
|
+
- Adds skill.md references to your rules files
|
|
20
|
+
- Scans and backs up your agent config files to the cloud
|
|
14
21
|
|
|
15
22
|
Or set up manually:
|
|
16
23
|
|
|
@@ -39,9 +46,19 @@ See [piut.com/docs](https://piut.com/docs#add-to-ai) for setup guides for 14+ AI
|
|
|
39
46
|
Install globally or run with `npx`:
|
|
40
47
|
|
|
41
48
|
```bash
|
|
42
|
-
|
|
49
|
+
# Setup & Configuration
|
|
50
|
+
npx @piut/cli # Interactive setup: MCP + skill.md + cloud backup
|
|
43
51
|
npx @piut/cli status # Show which tools are connected
|
|
44
52
|
npx @piut/cli remove # Remove pıut from selected tools
|
|
53
|
+
|
|
54
|
+
# Cloud Backup
|
|
55
|
+
npx @piut/cli sync # Show backup status for current workspace
|
|
56
|
+
npx @piut/cli sync --install # Scan workspace, detect files, upload to cloud
|
|
57
|
+
npx @piut/cli sync --push # Push local changes to cloud
|
|
58
|
+
npx @piut/cli sync --pull # Pull cloud changes to local files
|
|
59
|
+
npx @piut/cli sync --history # Show version history for a file
|
|
60
|
+
npx @piut/cli sync --diff # Show diff between local and cloud
|
|
61
|
+
npx @piut/cli sync --restore # Restore files from cloud backup
|
|
45
62
|
```
|
|
46
63
|
|
|
47
64
|
**Options:**
|
|
@@ -50,17 +67,16 @@ npx @piut/cli remove # Remove pıut from selected tools
|
|
|
50
67
|
npx @piut/cli --key pb_... # Pass API key non-interactively
|
|
51
68
|
npx @piut/cli --tool cursor # Configure a single tool
|
|
52
69
|
npx @piut/cli --skip-skill # Skip skill.md file placement
|
|
70
|
+
npx @piut/cli sync --install --yes # Non-interactive backup setup
|
|
53
71
|
```
|
|
54
72
|
|
|
55
73
|
**Supported tools:** Claude Code, Claude Desktop, Cursor, Windsurf, GitHub Copilot, Amazon Q, Zed
|
|
56
74
|
|
|
57
|
-
##
|
|
75
|
+
## The Three Features
|
|
58
76
|
|
|
59
|
-
1.
|
|
60
|
-
2. **Connect your tools** — Run `npx @piut/cli` or add one config, and every connected AI knows your context
|
|
61
|
-
3. **Stay in sync** — Update your context once, and it's reflected everywhere
|
|
77
|
+
### 1. Host Your Brain (MCP Server)
|
|
62
78
|
|
|
63
|
-
Your
|
|
79
|
+
Your brain is organized into 5 sections, accessible from every AI tool via MCP:
|
|
64
80
|
|
|
65
81
|
| Section | What it stores |
|
|
66
82
|
|---------|---------------|
|
|
@@ -70,21 +86,9 @@ Your context is organized into 5 sections:
|
|
|
70
86
|
| **Projects** | Active, time-bound work with goals and deadlines |
|
|
71
87
|
| **Memory** | Bookmarks, links, ideas, notes, reference material |
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
| Document | Description |
|
|
76
|
-
|----------|-------------|
|
|
77
|
-
| [**skill.md**](skill.md) | AI skill file — MCP tools, rate limits, error codes |
|
|
78
|
-
| [**Add to your AI**](https://piut.com/docs#add-to-ai) | Setup guides for Claude, ChatGPT, Cursor, Copilot, and more |
|
|
79
|
-
| [**API Reference**](https://piut.com/docs#api-examples) | Code examples in cURL, Python, Node.js, Go, and Ruby |
|
|
80
|
-
| [**Rate Limits**](https://piut.com/docs#limits) | Limits by plan, error codes, and response headers |
|
|
81
|
-
| [**Context Files**](https://piut.com/docs#context-files) | Where to find your existing context in 14 AI platforms |
|
|
82
|
-
|
|
83
|
-
All documentation is maintained at [piut.com/docs](https://piut.com/docs) — the interactive version with credential auto-fill and setup guides.
|
|
84
|
-
|
|
85
|
-
## MCP Tools
|
|
89
|
+
### 2. Keep It Updated (MCP Tools + Skill)
|
|
86
90
|
|
|
87
|
-
|
|
91
|
+
6 tools let your AI read and write your brain:
|
|
88
92
|
|
|
89
93
|
| Tool | Purpose |
|
|
90
94
|
|------|---------|
|
|
@@ -95,7 +99,36 @@ pıut provides 6 tools via MCP:
|
|
|
95
99
|
| `update_brain` | AI-powered smart update across sections |
|
|
96
100
|
| `prompt_brain` | Natural language command (edit, delete, reorganize) |
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
Add the [skill reference](skill.md) to your rules file and your AI uses these tools automatically.
|
|
103
|
+
|
|
104
|
+
### 3. Back Up Your Files (Cloud Backup)
|
|
105
|
+
|
|
106
|
+
The CLI scans your workspace for agent config files and backs them up to the cloud:
|
|
107
|
+
|
|
108
|
+
| File | Tool |
|
|
109
|
+
|------|------|
|
|
110
|
+
| `CLAUDE.md` | Claude Code |
|
|
111
|
+
| `AGENTS.md` | Multi-agent |
|
|
112
|
+
| `.cursorrules` | Cursor |
|
|
113
|
+
| `.windsurfrules` | Windsurf |
|
|
114
|
+
| `copilot-instructions.md` | GitHub Copilot |
|
|
115
|
+
| `MEMORY.md` | Claude Code |
|
|
116
|
+
| `SOUL.md` | OpenClaw |
|
|
117
|
+
| `rules/*.md` | Various |
|
|
118
|
+
|
|
119
|
+
Files are encrypted at rest (AES-256-GCM), versioned, and syncable across machines.
|
|
120
|
+
|
|
121
|
+
## Documentation
|
|
122
|
+
|
|
123
|
+
| Document | Description |
|
|
124
|
+
|----------|-------------|
|
|
125
|
+
| [**skill.md**](skill.md) | AI skill file — MCP tools, rate limits, error codes |
|
|
126
|
+
| [**Add to your AI**](https://piut.com/docs#add-to-ai) | Setup guides for Claude, ChatGPT, Cursor, Copilot, and more |
|
|
127
|
+
| [**API Reference**](https://piut.com/docs#api-examples) | Code examples in cURL, Python, Node.js, Go, and Ruby |
|
|
128
|
+
| [**Cloud Backup**](https://piut.com/docs#cloud-backup) | Cloud backup setup, commands, and sync workflow |
|
|
129
|
+
| [**Rate Limits**](https://piut.com/docs#limits) | Limits by plan, error codes, and response headers |
|
|
130
|
+
|
|
131
|
+
All documentation is maintained at [piut.com/docs](https://piut.com/docs) — the interactive version with credential auto-fill and setup guides.
|
|
99
132
|
|
|
100
133
|
## Links
|
|
101
134
|
|
package/dist/cli.js
CHANGED
|
@@ -505,10 +505,1172 @@ async function removeCommand() {
|
|
|
505
505
|
console.log();
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
// src/commands/sync.ts
|
|
509
|
+
import fs8 from "fs";
|
|
510
|
+
import crypto2 from "crypto";
|
|
511
|
+
import { password as password2, confirm as confirm3, checkbox as checkbox3, select } from "@inquirer/prompts";
|
|
512
|
+
import chalk3 from "chalk";
|
|
513
|
+
|
|
514
|
+
// src/lib/scanner.ts
|
|
515
|
+
import fs6 from "fs";
|
|
516
|
+
import path6 from "path";
|
|
517
|
+
import os3 from "os";
|
|
518
|
+
var KNOWN_FILES = [
|
|
519
|
+
"CLAUDE.md",
|
|
520
|
+
".claude/MEMORY.md",
|
|
521
|
+
".claude/settings.json",
|
|
522
|
+
"AGENTS.md",
|
|
523
|
+
".cursorrules",
|
|
524
|
+
".windsurfrules",
|
|
525
|
+
".github/copilot-instructions.md",
|
|
526
|
+
".cursor/rules/*.md",
|
|
527
|
+
".cursor/rules/*.mdc",
|
|
528
|
+
".windsurf/rules/*.md",
|
|
529
|
+
".claude/rules/*.md",
|
|
530
|
+
"CONVENTIONS.md",
|
|
531
|
+
".zed/rules.md"
|
|
532
|
+
];
|
|
533
|
+
var GLOBAL_FILES = [
|
|
534
|
+
"~/.claude/MEMORY.md",
|
|
535
|
+
"~/.claude/settings.local.json",
|
|
536
|
+
"~/.openclaw/workspace/SOUL.md",
|
|
537
|
+
"~/.openclaw/workspace/MEMORY.md",
|
|
538
|
+
"~/.openclaw/workspace/IDENTITY.md",
|
|
539
|
+
"~/.config/agent/IDENTITY.md"
|
|
540
|
+
];
|
|
541
|
+
function detectInstalledTools() {
|
|
542
|
+
const installed = [];
|
|
543
|
+
for (const tool of TOOLS) {
|
|
544
|
+
const paths = resolveConfigPaths(tool.configPaths);
|
|
545
|
+
for (const configPath of paths) {
|
|
546
|
+
if (fs6.existsSync(configPath) || fs6.existsSync(path6.dirname(configPath))) {
|
|
547
|
+
installed.push({ name: tool.name, id: tool.id });
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return installed;
|
|
553
|
+
}
|
|
554
|
+
function categorizeFile(filePath) {
|
|
555
|
+
const lower = filePath.toLowerCase();
|
|
556
|
+
if (lower.includes(".claude/") || lower.includes("claude.md")) return "Claude Code";
|
|
557
|
+
if (lower.includes(".cursor/") || lower.includes(".cursorrules")) return "Cursor Rules";
|
|
558
|
+
if (lower.includes(".windsurf/") || lower.includes(".windsurfrules")) return "Windsurf Rules";
|
|
559
|
+
if (lower.includes(".github/copilot")) return "VS Code / Copilot";
|
|
560
|
+
if (lower.includes(".aws/amazonq")) return "Amazon Q";
|
|
561
|
+
if (lower.includes(".zed/")) return "Zed";
|
|
562
|
+
if (lower.includes(".openclaw/")) return "OpenClaw";
|
|
563
|
+
return "Custom";
|
|
564
|
+
}
|
|
565
|
+
function matchesGlob(filename, pattern) {
|
|
566
|
+
if (!pattern.includes("*")) return filename === pattern;
|
|
567
|
+
const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
|
|
568
|
+
return regex.test(filename);
|
|
569
|
+
}
|
|
570
|
+
function scanDirectory(dir, patterns) {
|
|
571
|
+
const found = [];
|
|
572
|
+
for (const pattern of patterns) {
|
|
573
|
+
const parts = pattern.split("/");
|
|
574
|
+
if (parts.length === 1) {
|
|
575
|
+
const filePath = path6.join(dir, pattern);
|
|
576
|
+
if (fs6.existsSync(filePath) && fs6.statSync(filePath).isFile()) {
|
|
577
|
+
found.push(filePath);
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
const lastPart = parts[parts.length - 1];
|
|
581
|
+
const dirPart = parts.slice(0, -1).join("/");
|
|
582
|
+
const fullDir = path6.join(dir, dirPart);
|
|
583
|
+
if (lastPart.includes("*")) {
|
|
584
|
+
if (fs6.existsSync(fullDir) && fs6.statSync(fullDir).isDirectory()) {
|
|
585
|
+
try {
|
|
586
|
+
const entries = fs6.readdirSync(fullDir);
|
|
587
|
+
for (const entry of entries) {
|
|
588
|
+
if (matchesGlob(entry, lastPart)) {
|
|
589
|
+
const fullPath = path6.join(fullDir, entry);
|
|
590
|
+
if (fs6.statSync(fullPath).isFile()) {
|
|
591
|
+
found.push(fullPath);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
const filePath = path6.join(dir, pattern);
|
|
600
|
+
if (fs6.existsSync(filePath) && fs6.statSync(filePath).isFile()) {
|
|
601
|
+
found.push(filePath);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return found;
|
|
607
|
+
}
|
|
608
|
+
function scanForFiles(workspaceDirs) {
|
|
609
|
+
const home2 = os3.homedir();
|
|
610
|
+
const files = [];
|
|
611
|
+
const seen = /* @__PURE__ */ new Set();
|
|
612
|
+
for (const globalPath of GLOBAL_FILES) {
|
|
613
|
+
const absPath = expandPath(globalPath);
|
|
614
|
+
if (fs6.existsSync(absPath) && fs6.statSync(absPath).isFile()) {
|
|
615
|
+
if (seen.has(absPath)) continue;
|
|
616
|
+
seen.add(absPath);
|
|
617
|
+
files.push({
|
|
618
|
+
absolutePath: absPath,
|
|
619
|
+
displayPath: globalPath,
|
|
620
|
+
sizeBytes: fs6.statSync(absPath).size,
|
|
621
|
+
category: "Global",
|
|
622
|
+
type: "global",
|
|
623
|
+
projectName: "Global"
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const dirs = workspaceDirs || [process.cwd()];
|
|
628
|
+
for (const dir of dirs) {
|
|
629
|
+
const absDir = path6.resolve(dir);
|
|
630
|
+
if (!fs6.existsSync(absDir)) continue;
|
|
631
|
+
const foundPaths = scanDirectory(absDir, KNOWN_FILES);
|
|
632
|
+
for (const filePath of foundPaths) {
|
|
633
|
+
if (seen.has(filePath)) continue;
|
|
634
|
+
seen.add(filePath);
|
|
635
|
+
const relativePath = path6.relative(absDir, filePath);
|
|
636
|
+
const projectName = path6.basename(absDir);
|
|
637
|
+
files.push({
|
|
638
|
+
absolutePath: filePath,
|
|
639
|
+
displayPath: path6.relative(home2, filePath).startsWith("..") ? filePath : "~/" + path6.relative(home2, filePath),
|
|
640
|
+
sizeBytes: fs6.statSync(filePath).size,
|
|
641
|
+
category: categorizeFile(filePath),
|
|
642
|
+
type: "project",
|
|
643
|
+
projectName
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return files;
|
|
648
|
+
}
|
|
649
|
+
function formatSize(bytes) {
|
|
650
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
651
|
+
const kb = bytes / 1024;
|
|
652
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
653
|
+
const mb = kb / 1024;
|
|
654
|
+
return `${mb.toFixed(1)} MB`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/lib/sync-api.ts
|
|
658
|
+
var API_BASE2 = process.env.PIUT_API_BASE || "https://piut.com";
|
|
659
|
+
function authHeaders(apiKey) {
|
|
660
|
+
return {
|
|
661
|
+
Authorization: `Bearer ${apiKey}`,
|
|
662
|
+
"Content-Type": "application/json"
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
async function uploadFiles(apiKey, files) {
|
|
666
|
+
const res = await fetch(`${API_BASE2}/api/sync/upload`, {
|
|
667
|
+
method: "POST",
|
|
668
|
+
headers: authHeaders(apiKey),
|
|
669
|
+
body: JSON.stringify({ files })
|
|
670
|
+
});
|
|
671
|
+
if (!res.ok) {
|
|
672
|
+
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
673
|
+
throw new Error(body.error || `Upload failed (HTTP ${res.status})`);
|
|
674
|
+
}
|
|
675
|
+
return res.json();
|
|
676
|
+
}
|
|
677
|
+
async function listFiles(apiKey) {
|
|
678
|
+
const res = await fetch(`${API_BASE2}/api/sync/files`, {
|
|
679
|
+
headers: authHeaders(apiKey)
|
|
680
|
+
});
|
|
681
|
+
if (!res.ok) {
|
|
682
|
+
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
683
|
+
throw new Error(body.error || `List failed (HTTP ${res.status})`);
|
|
684
|
+
}
|
|
685
|
+
return res.json();
|
|
686
|
+
}
|
|
687
|
+
async function pullFiles(apiKey, fileIds, deviceId) {
|
|
688
|
+
const res = await fetch(`${API_BASE2}/api/sync/pull`, {
|
|
689
|
+
method: "POST",
|
|
690
|
+
headers: authHeaders(apiKey),
|
|
691
|
+
body: JSON.stringify({ fileIds, deviceId })
|
|
692
|
+
});
|
|
693
|
+
if (!res.ok) {
|
|
694
|
+
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
695
|
+
throw new Error(body.error || `Pull failed (HTTP ${res.status})`);
|
|
696
|
+
}
|
|
697
|
+
return res.json();
|
|
698
|
+
}
|
|
699
|
+
async function listFileVersions(apiKey, fileId) {
|
|
700
|
+
const res = await fetch(`${API_BASE2}/api/sync/files/${fileId}/versions`, {
|
|
701
|
+
headers: authHeaders(apiKey)
|
|
702
|
+
});
|
|
703
|
+
if (!res.ok) {
|
|
704
|
+
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
705
|
+
throw new Error(body.error || `Version list failed (HTTP ${res.status})`);
|
|
706
|
+
}
|
|
707
|
+
return res.json();
|
|
708
|
+
}
|
|
709
|
+
async function getFileVersion(apiKey, fileId, version) {
|
|
710
|
+
const res = await fetch(`${API_BASE2}/api/sync/files/${fileId}/versions/${version}`, {
|
|
711
|
+
headers: authHeaders(apiKey)
|
|
712
|
+
});
|
|
713
|
+
if (!res.ok) {
|
|
714
|
+
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
715
|
+
throw new Error(body.error || `Version fetch failed (HTTP ${res.status})`);
|
|
716
|
+
}
|
|
717
|
+
return res.json();
|
|
718
|
+
}
|
|
719
|
+
async function resolveConflict(apiKey, fileId, resolution, localContent, deviceId, deviceName) {
|
|
720
|
+
const res = await fetch(`${API_BASE2}/api/sync/resolve`, {
|
|
721
|
+
method: "POST",
|
|
722
|
+
headers: authHeaders(apiKey),
|
|
723
|
+
body: JSON.stringify({ fileId, resolution, localContent, deviceId, deviceName })
|
|
724
|
+
});
|
|
725
|
+
if (!res.ok) {
|
|
726
|
+
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
727
|
+
throw new Error(body.error || `Resolve failed (HTTP ${res.status})`);
|
|
728
|
+
}
|
|
729
|
+
return res.json();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/lib/sync-config.ts
|
|
733
|
+
import fs7 from "fs";
|
|
734
|
+
import path7 from "path";
|
|
735
|
+
import os4 from "os";
|
|
736
|
+
import crypto from "crypto";
|
|
737
|
+
var CONFIG_DIR = path7.join(os4.homedir(), ".piut");
|
|
738
|
+
var CONFIG_FILE = path7.join(CONFIG_DIR, "config.json");
|
|
739
|
+
function defaultDeviceName() {
|
|
740
|
+
return os4.hostname() || "unknown";
|
|
741
|
+
}
|
|
742
|
+
function generateDeviceId() {
|
|
743
|
+
return `dev_${crypto.randomBytes(8).toString("hex")}`;
|
|
744
|
+
}
|
|
745
|
+
function readSyncConfig() {
|
|
746
|
+
const defaults = {
|
|
747
|
+
deviceId: generateDeviceId(),
|
|
748
|
+
deviceName: defaultDeviceName(),
|
|
749
|
+
autoDiscover: false,
|
|
750
|
+
keepBrainUpdated: false,
|
|
751
|
+
useBrain: false,
|
|
752
|
+
backedUpFiles: []
|
|
753
|
+
};
|
|
754
|
+
try {
|
|
755
|
+
const raw = fs7.readFileSync(CONFIG_FILE, "utf-8");
|
|
756
|
+
const parsed = JSON.parse(raw);
|
|
757
|
+
return { ...defaults, ...parsed };
|
|
758
|
+
} catch {
|
|
759
|
+
return defaults;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function writeSyncConfig(config) {
|
|
763
|
+
fs7.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
764
|
+
fs7.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
765
|
+
}
|
|
766
|
+
function updateSyncConfig(updates) {
|
|
767
|
+
const config = readSyncConfig();
|
|
768
|
+
const updated = { ...config, ...updates };
|
|
769
|
+
writeSyncConfig(updated);
|
|
770
|
+
return updated;
|
|
771
|
+
}
|
|
772
|
+
function getConfigFile() {
|
|
773
|
+
return CONFIG_FILE;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/lib/sensitive-guard.ts
|
|
777
|
+
var SECRET_PATTERNS = [
|
|
778
|
+
// API keys and tokens
|
|
779
|
+
/(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token|auth[_-]?token|bearer)\s*[:=]\s*['"]?[A-Za-z0-9_\-/.]{20,}/i,
|
|
780
|
+
// AWS credentials
|
|
781
|
+
/AKIA[0-9A-Z]{16}/,
|
|
782
|
+
// Private keys
|
|
783
|
+
/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
|
|
784
|
+
// Supabase service role JWTs (eyJ...)
|
|
785
|
+
/SUPABASE_SERVICE_ROLE_KEY\s*[:=]\s*['"]?eyJ/i,
|
|
786
|
+
// Generic JWT tokens assigned to variables
|
|
787
|
+
/(?:JWT|TOKEN|SECRET|PASSWORD|CREDENTIAL)\s*[:=]\s*['"]?eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/i,
|
|
788
|
+
// Connection strings
|
|
789
|
+
/(?:postgres|mysql|mongodb|redis):\/\/[^\s'"]+:[^\s'"]+@/i,
|
|
790
|
+
// Stripe keys
|
|
791
|
+
/sk_(?:live|test)_[A-Za-z0-9]{20,}/,
|
|
792
|
+
// Anthropic keys
|
|
793
|
+
/sk-ant-[A-Za-z0-9_-]{20,}/,
|
|
794
|
+
// OpenAI keys
|
|
795
|
+
/sk-[A-Za-z0-9]{40,}/,
|
|
796
|
+
// npm tokens
|
|
797
|
+
/npm_[A-Za-z0-9]{36}/,
|
|
798
|
+
// GitHub tokens
|
|
799
|
+
/gh[pousr]_[A-Za-z0-9]{36,}/,
|
|
800
|
+
// Generic password assignments
|
|
801
|
+
/(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/i
|
|
802
|
+
];
|
|
803
|
+
var BLOCKED_FILENAMES = [
|
|
804
|
+
".env",
|
|
805
|
+
".env.local",
|
|
806
|
+
".env.production",
|
|
807
|
+
".env.development",
|
|
808
|
+
".env.staging",
|
|
809
|
+
".env.test",
|
|
810
|
+
"credentials.json",
|
|
811
|
+
"service-account.json",
|
|
812
|
+
"secrets.json",
|
|
813
|
+
"secrets.yaml",
|
|
814
|
+
"secrets.yml",
|
|
815
|
+
".netrc",
|
|
816
|
+
".npmrc",
|
|
817
|
+
"id_rsa",
|
|
818
|
+
"id_ed25519",
|
|
819
|
+
"id_ecdsa"
|
|
820
|
+
];
|
|
821
|
+
function isBlockedFilename(filePath) {
|
|
822
|
+
const basename = filePath.split("/").pop() || "";
|
|
823
|
+
return BLOCKED_FILENAMES.some(
|
|
824
|
+
(blocked) => basename === blocked || basename.startsWith(".env.")
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
function scanForSecrets(content) {
|
|
828
|
+
const matches = [];
|
|
829
|
+
const lines = content.split("\n");
|
|
830
|
+
for (let i = 0; i < lines.length; i++) {
|
|
831
|
+
const line = lines[i];
|
|
832
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
833
|
+
if (pattern.test(line)) {
|
|
834
|
+
const preview = line.length > 80 ? line.slice(0, 77) + "..." : line;
|
|
835
|
+
matches.push({
|
|
836
|
+
line: i + 1,
|
|
837
|
+
preview: preview.replace(/(['"])[A-Za-z0-9_\-/.+=]{20,}(['"])/g, "$1[REDACTED]$2"),
|
|
838
|
+
pattern: pattern.source.slice(0, 40)
|
|
839
|
+
});
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return matches;
|
|
845
|
+
}
|
|
846
|
+
function guardFile(filePath, content) {
|
|
847
|
+
if (isBlockedFilename(filePath)) {
|
|
848
|
+
return { blocked: true, reason: "filename", matches: [] };
|
|
849
|
+
}
|
|
850
|
+
const matches = scanForSecrets(content);
|
|
851
|
+
if (matches.length > 0) {
|
|
852
|
+
return { blocked: true, reason: "content", matches };
|
|
853
|
+
}
|
|
854
|
+
return { blocked: false, reason: null, matches: [] };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/commands/sync.ts
|
|
858
|
+
async function syncCommand(options) {
|
|
859
|
+
if (options.install) {
|
|
860
|
+
await installFlow(options);
|
|
861
|
+
} else if (options.push) {
|
|
862
|
+
await pushFlow(options);
|
|
863
|
+
} else if (options.pull) {
|
|
864
|
+
await pullFlow(options);
|
|
865
|
+
} else if (options.watch) {
|
|
866
|
+
await watchFlow();
|
|
867
|
+
} else if (options.history) {
|
|
868
|
+
await historyFlow(options.history);
|
|
869
|
+
} else if (options.diff) {
|
|
870
|
+
await diffFlow(options.diff);
|
|
871
|
+
} else if (options.restore) {
|
|
872
|
+
await restoreFlow(options.restore);
|
|
873
|
+
} else if (options.installDaemon) {
|
|
874
|
+
await installDaemonFlow();
|
|
875
|
+
} else {
|
|
876
|
+
await statusFlow();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function guardAndFilter(files, options) {
|
|
880
|
+
const safe = [];
|
|
881
|
+
let blocked = 0;
|
|
882
|
+
for (const file of files) {
|
|
883
|
+
const content = fs8.readFileSync(file.absolutePath, "utf-8");
|
|
884
|
+
const result = guardFile(file.displayPath, content);
|
|
885
|
+
if (result.blocked) {
|
|
886
|
+
blocked++;
|
|
887
|
+
if (result.reason === "filename") {
|
|
888
|
+
console.log(chalk3.red(` BLOCKED ${file.displayPath}`) + dim(" (sensitive filename)"));
|
|
889
|
+
} else {
|
|
890
|
+
console.log(chalk3.red(` BLOCKED ${file.displayPath}`) + dim(" (contains secrets)"));
|
|
891
|
+
for (const match of result.matches.slice(0, 3)) {
|
|
892
|
+
console.log(dim(` line ${match.line}: ${match.preview}`));
|
|
893
|
+
}
|
|
894
|
+
if (result.matches.length > 3) {
|
|
895
|
+
console.log(dim(` ... and ${result.matches.length - 3} more`));
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
safe.push(file);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (blocked > 0) {
|
|
903
|
+
console.log();
|
|
904
|
+
console.log(warning(` ${blocked} file(s) blocked by sensitive file guard`));
|
|
905
|
+
console.log(dim(" These files will not be uploaded to protect your secrets."));
|
|
906
|
+
console.log();
|
|
907
|
+
}
|
|
908
|
+
return safe;
|
|
909
|
+
}
|
|
910
|
+
async function installFlow(options) {
|
|
911
|
+
banner();
|
|
912
|
+
console.log(brand.bold(" Cloud Backup Setup"));
|
|
913
|
+
console.log();
|
|
914
|
+
const config = readSyncConfig();
|
|
915
|
+
let apiKey = options.key || config.apiKey;
|
|
916
|
+
if (!apiKey) {
|
|
917
|
+
console.log(dim(" Enter your API key, or press Enter to get one at piut.com/dashboard"));
|
|
918
|
+
console.log();
|
|
919
|
+
apiKey = await password2({
|
|
920
|
+
message: "Enter your API key (or press Enter to get one)",
|
|
921
|
+
mask: "*",
|
|
922
|
+
validate: (v) => {
|
|
923
|
+
if (!v) return true;
|
|
924
|
+
return v.startsWith("pb_") || "Key must start with pb_";
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
if (!apiKey) {
|
|
928
|
+
console.log();
|
|
929
|
+
console.log(` Get an API key at: ${brand("https://piut.com/dashboard")}`);
|
|
930
|
+
console.log(dim(" Then run: npx @piut/cli sync --install"));
|
|
931
|
+
console.log();
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
console.log(dim(" Validating key..."));
|
|
936
|
+
let validationResult;
|
|
937
|
+
try {
|
|
938
|
+
validationResult = await validateKey(apiKey);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
console.log(chalk3.red(` \u2717 ${err.message}`));
|
|
941
|
+
console.log(dim(" Get a key at https://piut.com/dashboard"));
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
console.log(success(` \u2713 Authenticated as ${validationResult.displayName}`));
|
|
945
|
+
console.log();
|
|
946
|
+
updateSyncConfig({ apiKey });
|
|
947
|
+
console.log(dim(" Scanning your AI tool environments..."));
|
|
948
|
+
const tools = detectInstalledTools();
|
|
949
|
+
if (tools.length === 0) {
|
|
950
|
+
console.log(warning(" No AI tools detected."));
|
|
951
|
+
console.log(dim(" Supported: Claude Code, Claude Desktop, Cursor, Windsurf, Copilot, Amazon Q, Zed"));
|
|
952
|
+
console.log();
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
console.log(success(` \u2713 Found ${tools.length} product${tools.length === 1 ? "" : "s"} installed:`));
|
|
956
|
+
for (const tool of tools) {
|
|
957
|
+
console.log(` - ${tool.name}`);
|
|
958
|
+
}
|
|
959
|
+
console.log();
|
|
960
|
+
if (!options.yes) {
|
|
961
|
+
const proceed = await confirm3({
|
|
962
|
+
message: `Continue with all ${tools.length}?`,
|
|
963
|
+
default: true
|
|
964
|
+
});
|
|
965
|
+
if (!proceed) {
|
|
966
|
+
console.log(dim(" Setup cancelled."));
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
console.log();
|
|
971
|
+
console.log(dim(" Scanning for brain files..."));
|
|
972
|
+
console.log(dim(" (looking for: CLAUDE.md, MEMORY.md, SOUL.md, IDENTITY.md, .cursorrules, etc.)"));
|
|
973
|
+
console.log();
|
|
974
|
+
if (!options.yes) {
|
|
975
|
+
const scanPermission = await confirm3({
|
|
976
|
+
message: "Permission to scan your workspace?",
|
|
977
|
+
default: true
|
|
978
|
+
});
|
|
979
|
+
if (!scanPermission) {
|
|
980
|
+
console.log(dim(" Scan cancelled."));
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const scannedFiles = scanForFiles();
|
|
985
|
+
if (scannedFiles.length === 0) {
|
|
986
|
+
console.log(warning(" No agent config files found in the current workspace."));
|
|
987
|
+
console.log(dim(" Try running from a project directory with CLAUDE.md, .cursorrules, etc."));
|
|
988
|
+
console.log();
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const safeFiles = guardAndFilter(scannedFiles, options);
|
|
992
|
+
if (safeFiles.length === 0) {
|
|
993
|
+
console.log(warning(" All files were blocked by the sensitive file guard."));
|
|
994
|
+
console.log();
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
console.log();
|
|
998
|
+
console.log(` Found ${brand.bold(String(safeFiles.length))} safe files across your workspace:`);
|
|
999
|
+
console.log();
|
|
1000
|
+
const grouped = groupByCategory(safeFiles);
|
|
1001
|
+
const choices = [];
|
|
1002
|
+
for (const [category, files] of Object.entries(grouped)) {
|
|
1003
|
+
console.log(dim(` \u{1F4C1} ${category}`));
|
|
1004
|
+
for (const file of files) {
|
|
1005
|
+
console.log(` \u2611 ${file.displayPath}`);
|
|
1006
|
+
choices.push({
|
|
1007
|
+
name: `${file.displayPath} ${dim(`(${formatSize(file.sizeBytes)})`)}`,
|
|
1008
|
+
value: file,
|
|
1009
|
+
checked: true
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
console.log();
|
|
1013
|
+
}
|
|
1014
|
+
let selectedFiles;
|
|
1015
|
+
if (options.yes) {
|
|
1016
|
+
selectedFiles = safeFiles;
|
|
1017
|
+
} else {
|
|
1018
|
+
selectedFiles = await checkbox3({
|
|
1019
|
+
message: `Back up all ${safeFiles.length} files?`,
|
|
1020
|
+
choices
|
|
1021
|
+
});
|
|
1022
|
+
if (selectedFiles.length === 0) {
|
|
1023
|
+
console.log(dim(" No files selected."));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
await uploadScannedFiles(apiKey, selectedFiles);
|
|
1028
|
+
console.log();
|
|
1029
|
+
console.log(` View your backups: ${brand("https://piut.com/dashboard/backups")}`);
|
|
1030
|
+
console.log();
|
|
1031
|
+
if (!options.yes) {
|
|
1032
|
+
const autoBackup = await confirm3({
|
|
1033
|
+
message: "Configure auto-backup?",
|
|
1034
|
+
default: false
|
|
1035
|
+
});
|
|
1036
|
+
if (autoBackup) {
|
|
1037
|
+
updateSyncConfig({ autoDiscover: true });
|
|
1038
|
+
console.log(success(" \u2713 Auto-backup enabled."));
|
|
1039
|
+
console.log(dim(" New files in the same environments will be backed up automatically."));
|
|
1040
|
+
console.log(dim(" Configure: piut sync config"));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
console.log();
|
|
1044
|
+
}
|
|
1045
|
+
async function pushFlow(options) {
|
|
1046
|
+
banner();
|
|
1047
|
+
const config = readSyncConfig();
|
|
1048
|
+
const apiKey = options.key || config.apiKey;
|
|
1049
|
+
if (!apiKey) {
|
|
1050
|
+
console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
console.log(dim(" Scanning for changes..."));
|
|
1054
|
+
const files = scanForFiles();
|
|
1055
|
+
if (files.length === 0) {
|
|
1056
|
+
console.log(dim(" No files found to push."));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const safeFiles = guardAndFilter(files, options);
|
|
1060
|
+
if (safeFiles.length === 0) {
|
|
1061
|
+
console.log(dim(" No safe files to push."));
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (!options.preferLocal && !options.preferCloud) {
|
|
1065
|
+
try {
|
|
1066
|
+
const cloudFiles = await listFiles(apiKey);
|
|
1067
|
+
for (const localFile of safeFiles) {
|
|
1068
|
+
const localContent = fs8.readFileSync(localFile.absolutePath, "utf-8");
|
|
1069
|
+
const localHash = hashContent(localContent);
|
|
1070
|
+
const cloudFile = cloudFiles.files.find(
|
|
1071
|
+
(cf) => cf.file_path === localFile.displayPath && cf.project_name === localFile.projectName
|
|
1072
|
+
);
|
|
1073
|
+
if (cloudFile && cloudFile.content_hash !== localHash) {
|
|
1074
|
+
console.log(warning(` Conflict: ${localFile.displayPath}`));
|
|
1075
|
+
console.log(dim(` local hash: ${localHash.slice(0, 12)}...`));
|
|
1076
|
+
console.log(dim(` cloud hash: ${cloudFile.content_hash.slice(0, 12)}...`));
|
|
1077
|
+
if (!options.yes) {
|
|
1078
|
+
const resolution = await select({
|
|
1079
|
+
message: `How to resolve ${localFile.displayPath}?`,
|
|
1080
|
+
choices: [
|
|
1081
|
+
{ name: "Keep local (push local to cloud)", value: "keep-local" },
|
|
1082
|
+
{ name: "Keep cloud (skip this file)", value: "keep-cloud" }
|
|
1083
|
+
]
|
|
1084
|
+
});
|
|
1085
|
+
if (resolution === "keep-cloud") {
|
|
1086
|
+
await resolveConflict(apiKey, cloudFile.id, "keep-cloud", void 0, config.deviceId, config.deviceName);
|
|
1087
|
+
console.log(success(` \u2713 Kept cloud version of ${localFile.displayPath}`));
|
|
1088
|
+
const idx = safeFiles.indexOf(localFile);
|
|
1089
|
+
if (idx >= 0) safeFiles.splice(idx, 1);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
} catch {
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
await uploadScannedFiles(apiKey, safeFiles);
|
|
1099
|
+
console.log();
|
|
1100
|
+
}
|
|
1101
|
+
async function pullFlow(options) {
|
|
1102
|
+
banner();
|
|
1103
|
+
const config = readSyncConfig();
|
|
1104
|
+
const apiKey = options.key || config.apiKey;
|
|
1105
|
+
if (!apiKey) {
|
|
1106
|
+
console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
|
|
1107
|
+
process.exit(1);
|
|
1108
|
+
}
|
|
1109
|
+
console.log(dim(" Pulling latest versions from cloud..."));
|
|
1110
|
+
try {
|
|
1111
|
+
const result = await pullFiles(apiKey, void 0, config.deviceId);
|
|
1112
|
+
if (result.files.length === 0) {
|
|
1113
|
+
console.log(dim(" No files to pull. Everything is up to date."));
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
for (const file of result.files) {
|
|
1117
|
+
console.log(success(` \u2713 ${file.file_path}`) + dim(` (v${file.current_version})`));
|
|
1118
|
+
}
|
|
1119
|
+
console.log();
|
|
1120
|
+
console.log(success(` Pulled ${result.files.length} file(s)`));
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
console.log(chalk3.red(` \u2717 Pull failed: ${err.message}`));
|
|
1123
|
+
process.exit(1);
|
|
1124
|
+
}
|
|
1125
|
+
console.log();
|
|
1126
|
+
}
|
|
1127
|
+
async function historyFlow(filePathOrId) {
|
|
1128
|
+
banner();
|
|
1129
|
+
console.log(brand.bold(" Version History"));
|
|
1130
|
+
console.log();
|
|
1131
|
+
const config = readSyncConfig();
|
|
1132
|
+
if (!config.apiKey) {
|
|
1133
|
+
console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
|
|
1134
|
+
process.exit(1);
|
|
1135
|
+
}
|
|
1136
|
+
const fileId = await resolveFileId(config.apiKey, filePathOrId);
|
|
1137
|
+
if (!fileId) {
|
|
1138
|
+
console.log(chalk3.red(` \u2717 File not found: ${filePathOrId}`));
|
|
1139
|
+
process.exit(1);
|
|
1140
|
+
}
|
|
1141
|
+
const result = await listFileVersions(config.apiKey, fileId);
|
|
1142
|
+
console.log(` ${brand.bold(result.filePath)} (${result.projectName})`);
|
|
1143
|
+
console.log(` Current version: v${result.currentVersion}`);
|
|
1144
|
+
console.log();
|
|
1145
|
+
for (const v of result.versions) {
|
|
1146
|
+
const date = new Date(v.createdAt).toLocaleString();
|
|
1147
|
+
const size = formatSize(v.contentSize);
|
|
1148
|
+
const summary = v.changeSummary ? ` \u2014 ${v.changeSummary}` : "";
|
|
1149
|
+
const marker = v.version === result.currentVersion ? chalk3.green(" (current)") : "";
|
|
1150
|
+
console.log(` v${v.version} ${dim(date)} ${dim(size)}${summary}${marker}`);
|
|
1151
|
+
}
|
|
1152
|
+
console.log();
|
|
1153
|
+
}
|
|
1154
|
+
async function diffFlow(filePathOrId) {
|
|
1155
|
+
banner();
|
|
1156
|
+
console.log(brand.bold(" Local vs Cloud Diff"));
|
|
1157
|
+
console.log();
|
|
1158
|
+
const config = readSyncConfig();
|
|
1159
|
+
if (!config.apiKey) {
|
|
1160
|
+
console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
}
|
|
1163
|
+
const fileId = await resolveFileId(config.apiKey, filePathOrId);
|
|
1164
|
+
if (!fileId) {
|
|
1165
|
+
console.log(chalk3.red(` \u2717 File not found in cloud: ${filePathOrId}`));
|
|
1166
|
+
process.exit(1);
|
|
1167
|
+
}
|
|
1168
|
+
const versions = await listFileVersions(config.apiKey, fileId);
|
|
1169
|
+
const cloudVersion = await getFileVersion(config.apiKey, fileId, versions.currentVersion);
|
|
1170
|
+
const cloudContent = cloudVersion.content;
|
|
1171
|
+
const localPath = resolveLocalPath(filePathOrId, versions.filePath);
|
|
1172
|
+
if (!localPath || !fs8.existsSync(localPath)) {
|
|
1173
|
+
console.log(warning(` Local file not found: ${filePathOrId}`));
|
|
1174
|
+
console.log(dim(" Showing cloud content only:"));
|
|
1175
|
+
console.log();
|
|
1176
|
+
console.log(cloudContent);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
const localContent = fs8.readFileSync(localPath, "utf-8");
|
|
1180
|
+
if (localContent === cloudContent) {
|
|
1181
|
+
console.log(success(" \u2713 Local and cloud are identical"));
|
|
1182
|
+
console.log();
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
const localLines = localContent.split("\n");
|
|
1186
|
+
const cloudLines = cloudContent.split("\n");
|
|
1187
|
+
const maxLen = Math.max(localLines.length, cloudLines.length);
|
|
1188
|
+
console.log(` ${versions.filePath}`);
|
|
1189
|
+
console.log(dim(` local: ${localLines.length} lines | cloud v${versions.currentVersion}: ${cloudLines.length} lines`));
|
|
1190
|
+
console.log();
|
|
1191
|
+
let diffCount = 0;
|
|
1192
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1193
|
+
const local = localLines[i];
|
|
1194
|
+
const cloud = cloudLines[i];
|
|
1195
|
+
if (local !== cloud) {
|
|
1196
|
+
diffCount++;
|
|
1197
|
+
if (diffCount > 50) {
|
|
1198
|
+
console.log(dim(` ... and more differences (${maxLen - i} lines remaining)`));
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
if (cloud !== void 0 && local !== void 0) {
|
|
1202
|
+
console.log(chalk3.red(` - ${i + 1}: ${cloud}`));
|
|
1203
|
+
console.log(chalk3.green(` + ${i + 1}: ${local}`));
|
|
1204
|
+
} else if (cloud === void 0) {
|
|
1205
|
+
console.log(chalk3.green(` + ${i + 1}: ${local}`));
|
|
1206
|
+
} else {
|
|
1207
|
+
console.log(chalk3.red(` - ${i + 1}: ${cloud}`));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
console.log();
|
|
1212
|
+
console.log(dim(` ${diffCount} line(s) differ`));
|
|
1213
|
+
console.log();
|
|
1214
|
+
}
|
|
1215
|
+
async function restoreFlow(filePathOrId) {
|
|
1216
|
+
banner();
|
|
1217
|
+
console.log(brand.bold(" Restore from Cloud"));
|
|
1218
|
+
console.log();
|
|
1219
|
+
const config = readSyncConfig();
|
|
1220
|
+
if (!config.apiKey) {
|
|
1221
|
+
console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
|
|
1222
|
+
process.exit(1);
|
|
1223
|
+
}
|
|
1224
|
+
const fileId = await resolveFileId(config.apiKey, filePathOrId);
|
|
1225
|
+
if (!fileId) {
|
|
1226
|
+
console.log(chalk3.red(` \u2717 File not found in cloud: ${filePathOrId}`));
|
|
1227
|
+
process.exit(1);
|
|
1228
|
+
}
|
|
1229
|
+
const versionsResult = await listFileVersions(config.apiKey, fileId);
|
|
1230
|
+
console.log(` ${brand.bold(versionsResult.filePath)} (${versionsResult.projectName})`);
|
|
1231
|
+
console.log();
|
|
1232
|
+
if (versionsResult.versions.length <= 1) {
|
|
1233
|
+
console.log(dim(" Only one version available. Nothing to restore."));
|
|
1234
|
+
console.log();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const versionChoice = await select({
|
|
1238
|
+
message: "Which version to restore?",
|
|
1239
|
+
choices: versionsResult.versions.map((v) => ({
|
|
1240
|
+
name: `v${v.version} \u2014 ${new Date(v.createdAt).toLocaleString()} (${formatSize(v.contentSize)})${v.changeSummary ? ` \u2014 ${v.changeSummary}` : ""}`,
|
|
1241
|
+
value: v.version
|
|
1242
|
+
}))
|
|
1243
|
+
});
|
|
1244
|
+
const versionData = await getFileVersion(config.apiKey, fileId, versionChoice);
|
|
1245
|
+
const localPath = resolveLocalPath(filePathOrId, versionsResult.filePath);
|
|
1246
|
+
console.log();
|
|
1247
|
+
console.log(dim(` Restoring v${versionChoice} of ${versionsResult.filePath}...`));
|
|
1248
|
+
const result = await uploadFiles(config.apiKey, [{
|
|
1249
|
+
projectName: versionsResult.projectName,
|
|
1250
|
+
filePath: versionsResult.filePath,
|
|
1251
|
+
content: versionData.content,
|
|
1252
|
+
category: "project",
|
|
1253
|
+
deviceId: config.deviceId,
|
|
1254
|
+
deviceName: config.deviceName
|
|
1255
|
+
}]);
|
|
1256
|
+
if (result.uploaded > 0) {
|
|
1257
|
+
console.log(success(` \u2713 Cloud restored to v${versionChoice} content (saved as new version)`));
|
|
1258
|
+
}
|
|
1259
|
+
if (localPath) {
|
|
1260
|
+
const writeLocal = await confirm3({
|
|
1261
|
+
message: `Also write to local file ${localPath}?`,
|
|
1262
|
+
default: true
|
|
1263
|
+
});
|
|
1264
|
+
if (writeLocal) {
|
|
1265
|
+
fs8.writeFileSync(localPath, versionData.content, "utf-8");
|
|
1266
|
+
console.log(success(` \u2713 Local file updated: ${localPath}`));
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
console.log();
|
|
1270
|
+
}
|
|
1271
|
+
async function watchFlow() {
|
|
1272
|
+
banner();
|
|
1273
|
+
console.log(brand.bold(" Live Sync (Watch Mode)"));
|
|
1274
|
+
console.log();
|
|
1275
|
+
const config = readSyncConfig();
|
|
1276
|
+
if (!config.apiKey) {
|
|
1277
|
+
console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
|
|
1278
|
+
process.exit(1);
|
|
1279
|
+
}
|
|
1280
|
+
let chokidar;
|
|
1281
|
+
try {
|
|
1282
|
+
chokidar = await import("chokidar");
|
|
1283
|
+
} catch {
|
|
1284
|
+
console.log(chalk3.red(" \u2717 chokidar is required for watch mode."));
|
|
1285
|
+
console.log(dim(" Install it: npm install -g chokidar"));
|
|
1286
|
+
console.log(dim(" Or use cron-based sync: piut sync --install-daemon"));
|
|
1287
|
+
process.exit(1);
|
|
1288
|
+
}
|
|
1289
|
+
const files = scanForFiles();
|
|
1290
|
+
const safeFiles = guardAndFilter(files, { yes: true });
|
|
1291
|
+
if (safeFiles.length === 0) {
|
|
1292
|
+
console.log(dim(" No files to watch."));
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const watchPaths = safeFiles.map((f) => f.absolutePath);
|
|
1296
|
+
console.log(dim(` Watching ${watchPaths.length} file(s) for changes...`));
|
|
1297
|
+
for (const f of safeFiles) {
|
|
1298
|
+
console.log(dim(` ${f.displayPath}`));
|
|
1299
|
+
}
|
|
1300
|
+
console.log();
|
|
1301
|
+
console.log(dim(" Press Ctrl+C to stop."));
|
|
1302
|
+
console.log();
|
|
1303
|
+
const debounceMap = /* @__PURE__ */ new Map();
|
|
1304
|
+
const DEBOUNCE_MS = 2e3;
|
|
1305
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
1306
|
+
persistent: true,
|
|
1307
|
+
ignoreInitial: true,
|
|
1308
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
1309
|
+
});
|
|
1310
|
+
watcher.on("change", (changedPath) => {
|
|
1311
|
+
const existing = debounceMap.get(changedPath);
|
|
1312
|
+
if (existing) clearTimeout(existing);
|
|
1313
|
+
debounceMap.set(changedPath, setTimeout(async () => {
|
|
1314
|
+
debounceMap.delete(changedPath);
|
|
1315
|
+
const file = safeFiles.find((f) => f.absolutePath === changedPath);
|
|
1316
|
+
if (!file) return;
|
|
1317
|
+
const content = fs8.readFileSync(changedPath, "utf-8");
|
|
1318
|
+
const guardResult = guardFile(file.displayPath, content);
|
|
1319
|
+
if (guardResult.blocked) {
|
|
1320
|
+
console.log(chalk3.red(` BLOCKED ${file.displayPath}`) + dim(" (sensitive content detected)"));
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
try {
|
|
1324
|
+
const result = await uploadFiles(config.apiKey, [{
|
|
1325
|
+
projectName: file.projectName,
|
|
1326
|
+
filePath: file.displayPath,
|
|
1327
|
+
content,
|
|
1328
|
+
category: file.type,
|
|
1329
|
+
deviceId: config.deviceId,
|
|
1330
|
+
deviceName: config.deviceName
|
|
1331
|
+
}]);
|
|
1332
|
+
const uploaded = result.files.find((f) => f.status === "ok");
|
|
1333
|
+
if (uploaded) {
|
|
1334
|
+
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1335
|
+
console.log(success(` \u2713 ${file.displayPath}`) + dim(` v${uploaded.version} (${time})`));
|
|
1336
|
+
}
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
console.log(chalk3.red(` \u2717 ${file.displayPath}: ${err.message}`));
|
|
1339
|
+
}
|
|
1340
|
+
}, DEBOUNCE_MS));
|
|
1341
|
+
});
|
|
1342
|
+
await new Promise(() => {
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
async function installDaemonFlow() {
|
|
1346
|
+
banner();
|
|
1347
|
+
console.log(brand.bold(" Auto-Sync Daemon Setup"));
|
|
1348
|
+
console.log();
|
|
1349
|
+
const platform2 = process.platform;
|
|
1350
|
+
if (platform2 === "darwin") {
|
|
1351
|
+
await installMacDaemon();
|
|
1352
|
+
} else if (platform2 === "linux") {
|
|
1353
|
+
installLinuxCron();
|
|
1354
|
+
} else {
|
|
1355
|
+
console.log(dim(" Auto-sync daemon setup is available for macOS and Linux."));
|
|
1356
|
+
console.log();
|
|
1357
|
+
console.log(dim(" Manual alternative: add this to your crontab (crontab -e):"));
|
|
1358
|
+
console.log();
|
|
1359
|
+
console.log(` */30 * * * * cd ~ && npx @piut/cli sync --push --yes 2>&1 >> ~/.piut/sync.log`);
|
|
1360
|
+
console.log();
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async function installMacDaemon() {
|
|
1364
|
+
const plistName = "com.piut.auto-sync";
|
|
1365
|
+
const plistDir = `${process.env.HOME}/Library/LaunchAgents`;
|
|
1366
|
+
const plistPath = `${plistDir}/${plistName}.plist`;
|
|
1367
|
+
const logDir = `${process.env.HOME}/.piut/logs`;
|
|
1368
|
+
const npxPath = await resolveNpxPath();
|
|
1369
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1370
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1371
|
+
<plist version="1.0">
|
|
1372
|
+
<dict>
|
|
1373
|
+
<key>Label</key>
|
|
1374
|
+
<string>${plistName}</string>
|
|
1375
|
+
<key>ProgramArguments</key>
|
|
1376
|
+
<array>
|
|
1377
|
+
<string>${npxPath}</string>
|
|
1378
|
+
<string>@piut/cli</string>
|
|
1379
|
+
<string>sync</string>
|
|
1380
|
+
<string>--push</string>
|
|
1381
|
+
<string>--yes</string>
|
|
1382
|
+
</array>
|
|
1383
|
+
<key>StartInterval</key>
|
|
1384
|
+
<integer>1800</integer>
|
|
1385
|
+
<key>RunAtLoad</key>
|
|
1386
|
+
<true/>
|
|
1387
|
+
<key>WorkingDirectory</key>
|
|
1388
|
+
<string>${process.env.HOME}</string>
|
|
1389
|
+
<key>StandardOutPath</key>
|
|
1390
|
+
<string>${logDir}/sync.out.log</string>
|
|
1391
|
+
<key>StandardErrorPath</key>
|
|
1392
|
+
<string>${logDir}/sync.err.log</string>
|
|
1393
|
+
<key>EnvironmentVariables</key>
|
|
1394
|
+
<dict>
|
|
1395
|
+
<key>PATH</key>
|
|
1396
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.HOME}/.local/bin:${process.env.HOME}/.npm-global/bin</string>
|
|
1397
|
+
</dict>
|
|
1398
|
+
</dict>
|
|
1399
|
+
</plist>`;
|
|
1400
|
+
console.log(dim(" This will create a macOS LaunchAgent that runs every 30 minutes."));
|
|
1401
|
+
console.log();
|
|
1402
|
+
console.log(dim(` Plist: ${plistPath}`));
|
|
1403
|
+
console.log(dim(` Logs: ${logDir}/sync.{out,err}.log`));
|
|
1404
|
+
console.log();
|
|
1405
|
+
const proceed = await confirm3({
|
|
1406
|
+
message: "Install the auto-sync LaunchAgent?",
|
|
1407
|
+
default: true
|
|
1408
|
+
});
|
|
1409
|
+
if (!proceed) {
|
|
1410
|
+
console.log(dim(" Cancelled."));
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
fs8.mkdirSync(logDir, { recursive: true });
|
|
1414
|
+
fs8.mkdirSync(plistDir, { recursive: true });
|
|
1415
|
+
fs8.writeFileSync(plistPath, plistContent, "utf-8");
|
|
1416
|
+
console.log(success(` \u2713 Plist written: ${plistPath}`));
|
|
1417
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
1418
|
+
try {
|
|
1419
|
+
try {
|
|
1420
|
+
execSync2(`launchctl bootout gui/$(id -u) ${plistPath} 2>/dev/null`, { stdio: "ignore" });
|
|
1421
|
+
} catch {
|
|
1422
|
+
}
|
|
1423
|
+
execSync2(`launchctl bootstrap gui/$(id -u) ${plistPath}`);
|
|
1424
|
+
console.log(success(" \u2713 LaunchAgent loaded \u2014 auto-sync active!"));
|
|
1425
|
+
} catch {
|
|
1426
|
+
console.log(warning(" LaunchAgent written but could not be loaded automatically."));
|
|
1427
|
+
console.log(dim(` Load manually: launchctl bootstrap gui/$(id -u) ${plistPath}`));
|
|
1428
|
+
}
|
|
1429
|
+
console.log();
|
|
1430
|
+
console.log(dim(" To stop: launchctl bootout gui/$(id -u) com.piut.auto-sync"));
|
|
1431
|
+
console.log(dim(" To check: launchctl print gui/$(id -u)/com.piut.auto-sync"));
|
|
1432
|
+
console.log();
|
|
1433
|
+
}
|
|
1434
|
+
function installLinuxCron() {
|
|
1435
|
+
console.log(dim(" Add this line to your crontab (run: crontab -e):"));
|
|
1436
|
+
console.log();
|
|
1437
|
+
console.log(` */30 * * * * cd ~ && npx @piut/cli sync --push --yes 2>&1 >> ~/.piut/sync.log`);
|
|
1438
|
+
console.log();
|
|
1439
|
+
console.log(dim(" This will push changes every 30 minutes."));
|
|
1440
|
+
console.log();
|
|
1441
|
+
}
|
|
1442
|
+
async function statusFlow() {
|
|
1443
|
+
banner();
|
|
1444
|
+
console.log(brand.bold(" Cloud Backup Status"));
|
|
1445
|
+
console.log();
|
|
1446
|
+
const config = readSyncConfig();
|
|
1447
|
+
if (!config.apiKey) {
|
|
1448
|
+
console.log(dim(" Not configured."));
|
|
1449
|
+
console.log();
|
|
1450
|
+
console.log(` Get started: ${brand("npx @piut/cli sync --install")}`);
|
|
1451
|
+
console.log();
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
try {
|
|
1455
|
+
const result = await listFiles(config.apiKey);
|
|
1456
|
+
console.log(` Files: ${brand.bold(String(result.fileCount))} / ${result.fileLimit}`);
|
|
1457
|
+
console.log(` Storage: ${brand.bold(formatSize(result.storageUsed))} / ${formatSize(result.storageLimit)}`);
|
|
1458
|
+
console.log(` Devices: ${result.devices.length}`);
|
|
1459
|
+
console.log();
|
|
1460
|
+
if (result.files.length > 0) {
|
|
1461
|
+
console.log(dim(" Backed-up files:"));
|
|
1462
|
+
for (const file of result.files) {
|
|
1463
|
+
console.log(` ${file.file_path} ${dim(`v${file.current_version}`)}`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
console.log();
|
|
1467
|
+
console.log(dim(" Configuration:"));
|
|
1468
|
+
console.log(` Auto-discover: ${config.autoDiscover ? success("ON") : dim("OFF")}`);
|
|
1469
|
+
console.log(` Brain sync: ${config.keepBrainUpdated ? success("ON") : dim("OFF")}`);
|
|
1470
|
+
console.log(` Use brain: ${config.useBrain ? success("ON") : dim("OFF")}`);
|
|
1471
|
+
console.log();
|
|
1472
|
+
console.log(` Dashboard: ${brand("https://piut.com/dashboard/backups")}`);
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
console.log(chalk3.red(` \u2717 ${err.message}`));
|
|
1475
|
+
}
|
|
1476
|
+
console.log();
|
|
1477
|
+
}
|
|
1478
|
+
function hashContent(content) {
|
|
1479
|
+
return crypto2.createHash("sha256").update(content, "utf8").digest("hex");
|
|
1480
|
+
}
|
|
1481
|
+
async function uploadScannedFiles(apiKey, files) {
|
|
1482
|
+
console.log();
|
|
1483
|
+
console.log(dim(" Backing up files..."));
|
|
1484
|
+
const syncConfig = readSyncConfig();
|
|
1485
|
+
const payloads = files.map((file) => ({
|
|
1486
|
+
projectName: file.projectName,
|
|
1487
|
+
filePath: file.displayPath,
|
|
1488
|
+
content: fs8.readFileSync(file.absolutePath, "utf-8"),
|
|
1489
|
+
category: file.type,
|
|
1490
|
+
deviceId: syncConfig.deviceId,
|
|
1491
|
+
deviceName: syncConfig.deviceName
|
|
1492
|
+
}));
|
|
1493
|
+
try {
|
|
1494
|
+
const result = await uploadFiles(apiKey, payloads);
|
|
1495
|
+
let totalSize = 0;
|
|
1496
|
+
for (const file of result.files) {
|
|
1497
|
+
const scanned = files.find(
|
|
1498
|
+
(s) => s.displayPath === file.filePath && s.projectName === file.projectName
|
|
1499
|
+
);
|
|
1500
|
+
const size = scanned ? scanned.sizeBytes : 0;
|
|
1501
|
+
totalSize += size;
|
|
1502
|
+
if (file.status === "ok") {
|
|
1503
|
+
console.log(success(` \u2713 ${file.filePath}`) + dim(` (${formatSize(size)})`));
|
|
1504
|
+
} else {
|
|
1505
|
+
console.log(chalk3.red(` \u2717 ${file.filePath}: ${file.status}`));
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
console.log();
|
|
1509
|
+
if (result.uploaded > 0) {
|
|
1510
|
+
console.log(success(` \u2713 All files backed up successfully!`));
|
|
1511
|
+
console.log();
|
|
1512
|
+
console.log(dim(" \u{1F4CA} Backup complete:"));
|
|
1513
|
+
console.log(dim(` ${result.uploaded} files | ${formatSize(totalSize)} total`));
|
|
1514
|
+
}
|
|
1515
|
+
if (result.errors > 0) {
|
|
1516
|
+
console.log(warning(` ${result.errors} file(s) failed to upload.`));
|
|
1517
|
+
}
|
|
1518
|
+
const backedUpPaths = result.files.filter((f) => f.status === "ok").map((f) => f.filePath);
|
|
1519
|
+
updateSyncConfig({ backedUpFiles: backedUpPaths });
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
console.log(chalk3.red(` \u2717 Upload failed: ${err.message}`));
|
|
1522
|
+
process.exit(1);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
async function resolveFileId(apiKey, pathOrId) {
|
|
1526
|
+
if (/^[0-9a-f]{8}-/.test(pathOrId)) return pathOrId;
|
|
1527
|
+
const result = await listFiles(apiKey);
|
|
1528
|
+
const match = result.files.find(
|
|
1529
|
+
(f) => f.file_path === pathOrId || f.file_path.endsWith(pathOrId) || f.file_path.includes(pathOrId)
|
|
1530
|
+
);
|
|
1531
|
+
return match?.id || null;
|
|
1532
|
+
}
|
|
1533
|
+
function resolveLocalPath(input, cloudPath) {
|
|
1534
|
+
if (fs8.existsSync(input)) return input;
|
|
1535
|
+
const home2 = process.env.HOME || "";
|
|
1536
|
+
if (cloudPath.startsWith("~/")) {
|
|
1537
|
+
const expanded = cloudPath.replace("~", home2);
|
|
1538
|
+
if (fs8.existsSync(expanded)) return expanded;
|
|
1539
|
+
}
|
|
1540
|
+
const cwdPath = `${process.cwd()}/${cloudPath}`;
|
|
1541
|
+
if (fs8.existsSync(cwdPath)) return cwdPath;
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
async function resolveNpxPath() {
|
|
1545
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
1546
|
+
try {
|
|
1547
|
+
return execSync2("which npx", { encoding: "utf-8" }).trim();
|
|
1548
|
+
} catch {
|
|
1549
|
+
return "/usr/local/bin/npx";
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
function groupByCategory(files) {
|
|
1553
|
+
const groups = {};
|
|
1554
|
+
for (const file of files) {
|
|
1555
|
+
if (!groups[file.category]) groups[file.category] = [];
|
|
1556
|
+
groups[file.category].push(file);
|
|
1557
|
+
}
|
|
1558
|
+
const sorted = {};
|
|
1559
|
+
if (groups["Global"]) {
|
|
1560
|
+
sorted["Global"] = groups["Global"];
|
|
1561
|
+
delete groups["Global"];
|
|
1562
|
+
}
|
|
1563
|
+
for (const key of Object.keys(groups).sort()) {
|
|
1564
|
+
sorted[key] = groups[key];
|
|
1565
|
+
}
|
|
1566
|
+
return sorted;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/commands/sync-config.ts
|
|
1570
|
+
import chalk4 from "chalk";
|
|
1571
|
+
async function syncConfigCommand(options) {
|
|
1572
|
+
banner();
|
|
1573
|
+
if (options.autoDiscover !== void 0) {
|
|
1574
|
+
const value = parseBool(options.autoDiscover);
|
|
1575
|
+
if (value === null) {
|
|
1576
|
+
console.log(chalk4.red(" \u2717 Invalid value. Use: on, off, true, false"));
|
|
1577
|
+
process.exit(1);
|
|
1578
|
+
}
|
|
1579
|
+
updateSyncConfig({ autoDiscover: value });
|
|
1580
|
+
console.log(success(` \u2713 Auto-discover: ${value ? "ON" : "OFF"}`));
|
|
1581
|
+
console.log();
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (options.keepBrainUpdated !== void 0) {
|
|
1585
|
+
const value = parseBool(options.keepBrainUpdated);
|
|
1586
|
+
if (value === null) {
|
|
1587
|
+
console.log(chalk4.red(" \u2717 Invalid value. Use: on, off, true, false"));
|
|
1588
|
+
process.exit(1);
|
|
1589
|
+
}
|
|
1590
|
+
updateSyncConfig({ keepBrainUpdated: value });
|
|
1591
|
+
console.log(success(` \u2713 Keep brain updated: ${value ? "ON" : "OFF"}`));
|
|
1592
|
+
console.log();
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
if (options.useBrain !== void 0) {
|
|
1596
|
+
const value = parseBool(options.useBrain);
|
|
1597
|
+
if (value === null) {
|
|
1598
|
+
console.log(chalk4.red(" \u2717 Invalid value. Use: on, off, true, false"));
|
|
1599
|
+
process.exit(1);
|
|
1600
|
+
}
|
|
1601
|
+
updateSyncConfig({ useBrain: value });
|
|
1602
|
+
console.log(success(` \u2713 Use brain: ${value ? "ON" : "OFF"}`));
|
|
1603
|
+
console.log();
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (options.show || options.files) {
|
|
1607
|
+
showConfig();
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
showConfigMenu();
|
|
1611
|
+
}
|
|
1612
|
+
function showConfig() {
|
|
1613
|
+
const config = readSyncConfig();
|
|
1614
|
+
console.log(brand.bold(" Current Configuration"));
|
|
1615
|
+
console.log();
|
|
1616
|
+
console.log(` Config file: ${dim(getConfigFile())}`);
|
|
1617
|
+
console.log(` Device ID: ${dim(config.deviceId)}`);
|
|
1618
|
+
console.log(` Device name: ${dim(config.deviceName)}`);
|
|
1619
|
+
console.log(` API key: ${config.apiKey ? success("configured") : dim("not set")}`);
|
|
1620
|
+
console.log();
|
|
1621
|
+
console.log(dim(" Features:"));
|
|
1622
|
+
console.log(` Auto-discover: ${config.autoDiscover ? success("ON") : dim("OFF")}`);
|
|
1623
|
+
console.log(` Keep brain updated: ${config.keepBrainUpdated ? success("ON") : dim("OFF")}`);
|
|
1624
|
+
console.log(` Use brain: ${config.useBrain ? success("ON") : dim("OFF")}`);
|
|
1625
|
+
console.log();
|
|
1626
|
+
if (config.backedUpFiles.length > 0) {
|
|
1627
|
+
console.log(dim(" Backed-up files:"));
|
|
1628
|
+
for (const f of config.backedUpFiles) {
|
|
1629
|
+
console.log(` ${f}`);
|
|
1630
|
+
}
|
|
1631
|
+
} else {
|
|
1632
|
+
console.log(dim(" No files backed up yet."));
|
|
1633
|
+
}
|
|
1634
|
+
console.log();
|
|
1635
|
+
}
|
|
1636
|
+
function showConfigMenu() {
|
|
1637
|
+
const config = readSyncConfig();
|
|
1638
|
+
console.log(brand.bold(" Configuration Options"));
|
|
1639
|
+
console.log(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1640
|
+
console.log();
|
|
1641
|
+
console.log(` 1. ${chalk4.white("Change which files are backed up")}`);
|
|
1642
|
+
console.log(` Current: ${config.backedUpFiles.length} files selected`);
|
|
1643
|
+
console.log(dim(` Command: piut sync config --files`));
|
|
1644
|
+
console.log();
|
|
1645
|
+
console.log(` 2. ${chalk4.white("Auto-backup new files in same environments")}`);
|
|
1646
|
+
console.log(` Current: ${config.autoDiscover ? success("ON") : dim("OFF")}`);
|
|
1647
|
+
console.log(dim(` Command: piut sync config --auto-discover [on|off]`));
|
|
1648
|
+
console.log();
|
|
1649
|
+
console.log(` 3. ${chalk4.white("Use skill to keep brain up to date")}`);
|
|
1650
|
+
console.log(` Current: ${config.keepBrainUpdated ? success("ON") : dim("OFF")}`);
|
|
1651
|
+
console.log(dim(` Command: piut sync config --keep-brain-updated [on|off]`));
|
|
1652
|
+
console.log();
|
|
1653
|
+
console.log(` 4. ${chalk4.white("Reference centralized brain")}`);
|
|
1654
|
+
console.log(` Current: ${config.useBrain ? success("ON") : dim("OFF")}`);
|
|
1655
|
+
console.log(dim(` Command: piut sync config --use-brain [on|off]`));
|
|
1656
|
+
console.log();
|
|
1657
|
+
console.log(` 5. ${chalk4.white("View current configuration")}`);
|
|
1658
|
+
console.log(dim(` Command: piut sync config --show`));
|
|
1659
|
+
console.log();
|
|
1660
|
+
}
|
|
1661
|
+
function parseBool(value) {
|
|
1662
|
+
const lower = value.toLowerCase();
|
|
1663
|
+
if (["on", "true", "1", "yes"].includes(lower)) return true;
|
|
1664
|
+
if (["off", "false", "0", "no"].includes(lower)) return false;
|
|
1665
|
+
return null;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
508
1668
|
// src/cli.ts
|
|
509
1669
|
var program = new Command();
|
|
510
|
-
program.name("piut").description("
|
|
1670
|
+
program.name("piut").description("Automatic backup + hosting of all your agent configs across every machine. Version history. Restore any time. One command to set up.").version("1.1.0");
|
|
511
1671
|
program.command("setup", { isDefault: true }).description("Auto-detect and configure AI tools").option("-k, --key <key>", "API key (prompts interactively if not provided)").option("-t, --tool <id>", "Configure a single tool (claude-code, cursor, windsurf, etc.)").option("-y, --yes", "Skip interactive prompts (auto-select all detected tools)").option("--project", "Prefer project-local config files").option("--skip-skill", "Skip skill.md file placement").action(setupCommand);
|
|
512
1672
|
program.command("status").description("Show which AI tools are configured with p\u0131ut").action(statusCommand);
|
|
513
1673
|
program.command("remove").description("Remove p\u0131ut configuration from AI tools").action(removeCommand);
|
|
1674
|
+
var sync = program.command("sync").description("Back up and sync your agent config files to the cloud").option("--install", "Run the guided backup setup flow").option("--push", "Push local changes to cloud").option("--pull", "Pull latest versions from cloud").option("--watch", "Watch files for changes and auto-push (live sync)").option("--history <file>", "Show version history for a file").option("--diff <file>", "Show diff between local and cloud version").option("--restore <file>", "Restore a file from a previous version").option("--prefer-local", "Resolve conflicts by keeping local version").option("--prefer-cloud", "Resolve conflicts by keeping cloud version").option("--install-daemon", "Set up auto-sync via cron/launchd").option("-k, --key <key>", "API key").option("-y, --yes", "Skip interactive prompts").action(syncCommand);
|
|
1675
|
+
sync.command("config").description("Configure cloud backup settings").option("--files", "Change which files are backed up").option("--auto-discover <value>", "Auto-backup new files (on/off)").option("--keep-brain-updated <value>", "Keep brain updated from backups (on/off)").option("--use-brain <value>", "Reference centralized brain (on/off)").option("--show", "View current configuration").action(syncConfigCommand);
|
|
514
1676
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piut/cli",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Automatic backup + hosting of all your agent configs across every machine. Version history. Restore any time. One command to set up.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"piut": "dist/cli.js"
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
"claude",
|
|
26
26
|
"cursor",
|
|
27
27
|
"copilot",
|
|
28
|
-
"windsurf"
|
|
28
|
+
"windsurf",
|
|
29
|
+
"backup",
|
|
30
|
+
"sync",
|
|
31
|
+
"cloud",
|
|
32
|
+
"agent-config"
|
|
29
33
|
],
|
|
30
34
|
"author": "M-Flat Inc",
|
|
31
35
|
"license": "MIT",
|
|
@@ -37,6 +41,7 @@
|
|
|
37
41
|
"dependencies": {
|
|
38
42
|
"@inquirer/prompts": "^7.0.0",
|
|
39
43
|
"chalk": "^5.4.0",
|
|
44
|
+
"chokidar": "^4.0.3",
|
|
40
45
|
"commander": "^13.0.0"
|
|
41
46
|
},
|
|
42
47
|
"devDependencies": {
|