@wuyaos/pi-sync 1.1.0 → 1.2.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/PROMO.md +37 -73
- package/README.md +141 -132
- package/README.zh-CN.md +141 -132
- package/extensions/sync/archive.ts +86 -121
- package/extensions/sync/config.ts +57 -27
- package/extensions/sync/i18n.ts +203 -0
- package/extensions/sync/index.ts +20 -43
- package/extensions/sync/menus.ts +472 -171
- package/extensions/sync/restore.ts +108 -102
- package/extensions/sync/webdav.ts +33 -12
- package/package.json +13 -5
- package/pi-bootstrap.ps1 +33 -74
- package/docs/sync-menu.png +0 -0
- package/extensions/sync/session-sync.ts +0 -231
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { ensureDir as ensureSharedDir, readJsonSafe, writeJsonAtomic } from "../_shared/json-io";
|
|
6
|
+
import { normalizeLanguage, type SyncLanguage } from "./i18n";
|
|
6
7
|
|
|
7
8
|
export const AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
|
|
8
9
|
export const AGENT_SKILLS_DIR = path.join(os.homedir(), ".agents", "skills");
|
|
@@ -10,8 +11,11 @@ export const SESSIONS_DIR = path.join(AGENT_DIR, "sessions");
|
|
|
10
11
|
export const SYNC_CONFIG_DIR = path.join(AGENT_DIR, "config");
|
|
11
12
|
export const SYNC_CONFIG_PATH = path.join(SYNC_CONFIG_DIR, "sync.json");
|
|
12
13
|
export const LEGACY_SYNC_CONFIG_PATH = path.join(AGENT_DIR, "sync_config.json");
|
|
13
|
-
export const
|
|
14
|
-
|
|
14
|
+
export const DEFAULT_PI_EXCLUDE_PATHS = ["npm", "git", "sessions", "state", "tmp", "webui-rpc-supervisor", "vstack"] as const;
|
|
15
|
+
const LEGACY_DEFAULT_PI_EXCLUDE_PATH_SETS = [
|
|
16
|
+
["npm", "git", "sessions", "state", "vstack"],
|
|
17
|
+
["npm", "git", "sessions", "state", "tmp", "vstack"],
|
|
18
|
+
] as const;
|
|
15
19
|
|
|
16
20
|
export type ManifestFile = { archive: string; source: string };
|
|
17
21
|
|
|
@@ -19,17 +23,13 @@ export interface SyncConfig {
|
|
|
19
23
|
webdavUrl: string;
|
|
20
24
|
webdavUser: string;
|
|
21
25
|
webdavPass: string;
|
|
26
|
+
language: SyncLanguage;
|
|
22
27
|
backupProviders: boolean;
|
|
23
|
-
backupSkills: boolean;
|
|
24
|
-
backupExtensions: boolean;
|
|
25
28
|
backupSessions: boolean;
|
|
26
|
-
sessionProjects: string[];
|
|
27
|
-
liveSessionBackup: boolean;
|
|
28
|
-
liveBackupDebounceMs: number;
|
|
29
|
-
syncIntervalTurns: number;
|
|
30
|
-
syncSessionOnExit: boolean;
|
|
31
|
-
backupMemory: boolean;
|
|
32
29
|
backupAgentSkills: boolean;
|
|
30
|
+
piExcludePaths: string[];
|
|
31
|
+
backupOnExit: boolean;
|
|
32
|
+
sessionProjects: string[];
|
|
33
33
|
sessionProjectMode: "whitelist" | "blacklist";
|
|
34
34
|
maxBackups: number;
|
|
35
35
|
}
|
|
@@ -38,46 +38,76 @@ export function ensureDir(dir: string): void {
|
|
|
38
38
|
ensureSharedDir(dir);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function normalizePiExcludePaths(value: unknown): string[] {
|
|
42
|
+
const source = Array.isArray(value) ? value : DEFAULT_PI_EXCLUDE_PATHS;
|
|
43
|
+
const normalized = new Set<string>();
|
|
44
|
+
for (const raw of source) {
|
|
45
|
+
const candidate = String(raw).trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
46
|
+
if (!candidate || candidate.startsWith("/") || /^[a-zA-Z]:\//.test(candidate)) continue;
|
|
47
|
+
if (candidate.split("/").includes("..")) continue;
|
|
48
|
+
normalized.add(candidate);
|
|
49
|
+
}
|
|
50
|
+
return [...normalized];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isLegacyDefaultPiExcludePaths(value: unknown): boolean {
|
|
54
|
+
if (!Array.isArray(value)) return false;
|
|
55
|
+
const normalized = normalizePiExcludePaths(value);
|
|
56
|
+
return LEGACY_DEFAULT_PI_EXCLUDE_PATH_SETS.some((legacy) => (
|
|
57
|
+
normalized.length === legacy.length && legacy.every((entry) => normalized.includes(entry))
|
|
58
|
+
));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defaultLanguage(): SyncLanguage {
|
|
62
|
+
const settings = readJsonSafe<{ piSwitch?: { language?: unknown } }>(path.join(AGENT_DIR, "settings.json"), {});
|
|
63
|
+
return normalizeLanguage(settings.piSwitch?.language);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** In-memory config cache, invalidated on saveConfig. Avoids per-hook disk reads. */
|
|
67
|
+
let configCache: SyncConfig | null = null;
|
|
68
|
+
|
|
41
69
|
export function loadConfig(): SyncConfig {
|
|
70
|
+
if (configCache) return configCache;
|
|
42
71
|
if (!fs.existsSync(SYNC_CONFIG_PATH) && fs.existsSync(LEGACY_SYNC_CONFIG_PATH)) {
|
|
43
72
|
ensureDir(SYNC_CONFIG_DIR);
|
|
44
|
-
try {
|
|
73
|
+
try {
|
|
74
|
+
fs.copyFileSync(LEGACY_SYNC_CONFIG_PATH, SYNC_CONFIG_PATH);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn(`[pi-sync] Failed to migrate legacy sync config: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
45
78
|
}
|
|
46
79
|
const data = readJsonSafe<Partial<SyncConfig>>(SYNC_CONFIG_PATH, {});
|
|
47
80
|
const normalizeList = (value: unknown): string[] => Array.isArray(value) ? value.map(String).filter(Boolean) : [];
|
|
48
|
-
|
|
81
|
+
const result: SyncConfig = {
|
|
49
82
|
webdavUrl: data.webdavUrl || "",
|
|
50
83
|
webdavUser: data.webdavUser || "",
|
|
51
84
|
webdavPass: data.webdavPass || "",
|
|
85
|
+
language: data.language ? normalizeLanguage(data.language) : defaultLanguage(),
|
|
52
86
|
backupProviders: data.backupProviders !== false,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
87
|
+
backupSessions: data.backupSessions !== false,
|
|
88
|
+
backupAgentSkills: data.backupAgentSkills === true,
|
|
89
|
+
piExcludePaths: isLegacyDefaultPiExcludePaths(data.piExcludePaths)
|
|
90
|
+
? [...DEFAULT_PI_EXCLUDE_PATHS]
|
|
91
|
+
: normalizePiExcludePaths(data.piExcludePaths),
|
|
92
|
+
backupOnExit: data.backupOnExit !== false,
|
|
56
93
|
sessionProjects: normalizeList(data.sessionProjects),
|
|
57
|
-
|
|
58
|
-
liveBackupDebounceMs: typeof data.liveBackupDebounceMs === "number" && data.liveBackupDebounceMs > 0 ? data.liveBackupDebounceMs : 3000,
|
|
59
|
-
syncIntervalTurns: typeof data.syncIntervalTurns === "number" && data.syncIntervalTurns >= 0 ? Math.floor(data.syncIntervalTurns) : 0,
|
|
60
|
-
syncSessionOnExit: data.syncSessionOnExit !== false,
|
|
61
|
-
backupMemory: data.backupMemory !== false,
|
|
62
|
-
backupAgentSkills: data.backupAgentSkills !== false,
|
|
63
|
-
sessionProjectMode: data.sessionProjectMode === "blacklist" ? "blacklist" : "whitelist",
|
|
94
|
+
sessionProjectMode: data.sessionProjectMode === "whitelist" ? "whitelist" : "blacklist",
|
|
64
95
|
maxBackups: typeof data.maxBackups === "number" && data.maxBackups >= 0 ? Math.floor(data.maxBackups) : 10,
|
|
65
96
|
};
|
|
97
|
+
configCache = result;
|
|
98
|
+
return result;
|
|
66
99
|
}
|
|
67
100
|
|
|
68
101
|
export function saveConfig(config: SyncConfig, ctx?: Pick<ExtensionContext, "ui">): void {
|
|
69
102
|
ensureDir(path.dirname(SYNC_CONFIG_PATH));
|
|
70
103
|
writeJsonAtomic(SYNC_CONFIG_PATH, config, { backup: true });
|
|
104
|
+
configCache = config;
|
|
71
105
|
if (ctx) refreshFooterStatusFromConfig(ctx, config);
|
|
72
106
|
}
|
|
73
107
|
|
|
74
108
|
/** Refresh footer status from an already-loaded config (avoids re-reading the file). */
|
|
75
109
|
export function refreshFooterStatusFromConfig(ctx: Pick<ExtensionContext, "ui">, config: SyncConfig): void {
|
|
76
|
-
|
|
77
|
-
if (config.liveSessionBackup) parts.push("⚡");
|
|
78
|
-
if (config.syncIntervalTurns > 0) parts.push(`🔄${config.syncIntervalTurns}`);
|
|
79
|
-
if (config.syncSessionOnExit) parts.push("📤");
|
|
80
|
-
ctx.ui.setStatus("pi-sync", parts.length ? `sync:${parts.join("")}` : undefined);
|
|
110
|
+
ctx.ui.setStatus("pi-sync", config.backupOnExit ? "backup:exit" : undefined);
|
|
81
111
|
}
|
|
82
112
|
|
|
83
113
|
export function isProjectAllowed(projectDir: string | undefined, config: SyncConfig): boolean {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export type SyncLanguage = "en" | "zh";
|
|
2
|
+
|
|
3
|
+
const EN = {
|
|
4
|
+
on: "ON",
|
|
5
|
+
off: "OFF",
|
|
6
|
+
enabled: "enabled",
|
|
7
|
+
disabled: "disabled",
|
|
8
|
+
keepAll: "keep all",
|
|
9
|
+
cancel: "❌ Cancel",
|
|
10
|
+
back: "x Back",
|
|
11
|
+
save: "s Save",
|
|
12
|
+
|
|
13
|
+
menuTitle: "Pi WebDAV Backup",
|
|
14
|
+
uploadAllBackups: "⬆️ Back Up All",
|
|
15
|
+
restoreAllBackups: "⬇️ Restore All (latest)",
|
|
16
|
+
uploadPiBackup: "☁️ Upload Pi Backup",
|
|
17
|
+
uploadSkillsBackup: "📦 Upload Skills Backup",
|
|
18
|
+
uploadSessionsArchive: "🗂️ Upload Sessions Archive",
|
|
19
|
+
restorePiBackup: "📥 Restore Pi Backup",
|
|
20
|
+
restoreSkillsBackup: "📥 Restore Skills Backup",
|
|
21
|
+
restoreSessionsArchive: "📥 Restore Sessions Archive",
|
|
22
|
+
configureSettings: "⚙️ Configure Backup Settings",
|
|
23
|
+
switchLanguage: "🌐 Language: English → 中文",
|
|
24
|
+
|
|
25
|
+
configureTitle: "Configure Backup Settings",
|
|
26
|
+
webdavUrl: "WebDAV URL: {value}",
|
|
27
|
+
webdavUsername: "WebDAV Username: {value}",
|
|
28
|
+
webdavPassword: "WebDAV Password/Token: {value}",
|
|
29
|
+
piBackup: "Pi Backup: {value}",
|
|
30
|
+
skillsBackup: "Shared Skills Backup: {value}",
|
|
31
|
+
sessionsBackup: "Sessions Backup: {value}",
|
|
32
|
+
backupOnExit: "Archive Current Project On Exit: {value}",
|
|
33
|
+
piExcludePaths: "Pi Backup Exclusions: {value}",
|
|
34
|
+
sessionProjects: "Session Projects: {value}",
|
|
35
|
+
sessionProjectMode: "Session Project Mode: {value}",
|
|
36
|
+
maxCloudBackups: "Max Cloud Backups: {value}",
|
|
37
|
+
language: "Language: English",
|
|
38
|
+
notSet: "(not set)",
|
|
39
|
+
passwordSet: "(set)",
|
|
40
|
+
none: "(none)",
|
|
41
|
+
piKind: "Pi",
|
|
42
|
+
skillsKind: "Skills",
|
|
43
|
+
sessionsKind: "Sessions",
|
|
44
|
+
|
|
45
|
+
setupRequired: "WebDAV is not configured. Please set it up now.",
|
|
46
|
+
promptWebdavUrl: "WebDAV URL:",
|
|
47
|
+
promptWebdavUsername: "WebDAV username:",
|
|
48
|
+
promptWebdavPassword: "WebDAV token (or $ENV_VAR):",
|
|
49
|
+
promptExcludePaths: "Excluded paths (comma separated, relative to ~/.pi/agent):",
|
|
50
|
+
promptMaxBackups: "Maximum backups (0 = keep all):",
|
|
51
|
+
configSaved: "Backup configuration updated.",
|
|
52
|
+
|
|
53
|
+
backupDisabled: "{kind} backup is disabled.",
|
|
54
|
+
backupUploaded: "Uploaded {filename}\n{contents}{pruned}",
|
|
55
|
+
backupFailed: "{kind} backup failed: {error}",
|
|
56
|
+
prunedCount: "\nPruned: {count}",
|
|
57
|
+
noAllowedSessionProjects: "No allowed local session projects found.",
|
|
58
|
+
archiveSessionProject: "Archive session project",
|
|
59
|
+
sessionArchiveUploaded: "Uploaded {filename}\n{contents}",
|
|
60
|
+
sessionArchiveFailed: "Session archive failed: {error}",
|
|
61
|
+
|
|
62
|
+
noArchivesFound: "No {kind} archives found.",
|
|
63
|
+
restoreArchive: "Restore {kind} archive",
|
|
64
|
+
replaceSharedSkillsTitle: "Replace shared skills?",
|
|
65
|
+
replaceSharedSkillsBody: "The current ~/.agents/skills will be moved to a timestamped backup.",
|
|
66
|
+
restoreCompleted: "Restored {kind}:\n{contents}",
|
|
67
|
+
restoreFailed: "{kind} restore failed: {error}",
|
|
68
|
+
restoreSessionProject: "Restore session archive: project",
|
|
69
|
+
selectSessionArchive: "Select session archive",
|
|
70
|
+
sessionRestoreCompleted: "Session archive restored:\n{contents}",
|
|
71
|
+
sessionRestoreFailed: "Session restore failed: {error}",
|
|
72
|
+
noRemoteSessionProjects: "No remote session archives found.",
|
|
73
|
+
confirmPiRestoreTitle: "Restore Pi backup?",
|
|
74
|
+
confirmPiRestoreBody: "Entries: {count}\n{plan}",
|
|
75
|
+
confirmRestoreAllTitle: "Restore all latest backups?",
|
|
76
|
+
confirmRestoreAllBody: "This restores Pi, enabled Skills, and the latest archive for every allowed session project.",
|
|
77
|
+
allBackupCompleted: "All backups completed:\n{results}",
|
|
78
|
+
allRestoreCompleted: "Restore all completed:\n{results}",
|
|
79
|
+
reloadRuntimeTitle: "Reload Runtime?",
|
|
80
|
+
reloadRuntimeBody: "Reload Pi to apply the restored data?",
|
|
81
|
+
|
|
82
|
+
projectModeWhitelist: "whitelist",
|
|
83
|
+
projectModeBlacklist: "blacklist",
|
|
84
|
+
projectSelectionWhitelist: "only selected projects",
|
|
85
|
+
projectSelectionBlacklist: "all except selected projects",
|
|
86
|
+
projectSelectionAll: "all projects",
|
|
87
|
+
selectSessionProjects: "Select Session Projects [{mode}]",
|
|
88
|
+
switchProjectMode: "m Switch Mode: {mode}",
|
|
89
|
+
selectAllProjects: "a Select All",
|
|
90
|
+
resetProjects: "r Reset",
|
|
91
|
+
|
|
92
|
+
exitBackupFailed: "Exit session backup failed: {error}",
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
export type TranslationKey = keyof typeof EN;
|
|
96
|
+
type TranslationParams = Record<string, string | number | boolean>;
|
|
97
|
+
|
|
98
|
+
const ZH: Record<TranslationKey, string> = {
|
|
99
|
+
on: "开启",
|
|
100
|
+
off: "关闭",
|
|
101
|
+
enabled: "已启用",
|
|
102
|
+
disabled: "已禁用",
|
|
103
|
+
keepAll: "全部保留",
|
|
104
|
+
cancel: "❌ 取消",
|
|
105
|
+
back: "x 返回",
|
|
106
|
+
save: "s 保存",
|
|
107
|
+
|
|
108
|
+
menuTitle: "Pi WebDAV 备份",
|
|
109
|
+
uploadAllBackups: "⬆️ 全部备份",
|
|
110
|
+
restoreAllBackups: "⬇️ 全部恢复(最新)",
|
|
111
|
+
uploadPiBackup: "☁️ 上传 Pi 备份",
|
|
112
|
+
uploadSkillsBackup: "📦 上传 Skills 备份",
|
|
113
|
+
uploadSessionsArchive: "🗂️ 上传会话归档",
|
|
114
|
+
restorePiBackup: "📥 恢复 Pi 备份",
|
|
115
|
+
restoreSkillsBackup: "📥 恢复 Skills 备份",
|
|
116
|
+
restoreSessionsArchive: "📥 恢复会话归档",
|
|
117
|
+
configureSettings: "⚙️ 配置备份设置",
|
|
118
|
+
switchLanguage: "🌐 语言:中文 → English",
|
|
119
|
+
|
|
120
|
+
configureTitle: "配置备份设置",
|
|
121
|
+
webdavUrl: "WebDAV 地址:{value}",
|
|
122
|
+
webdavUsername: "WebDAV 用户名:{value}",
|
|
123
|
+
webdavPassword: "WebDAV 密码/令牌:{value}",
|
|
124
|
+
piBackup: "Pi 备份:{value}",
|
|
125
|
+
skillsBackup: "共享 Skills 备份:{value}",
|
|
126
|
+
sessionsBackup: "会话备份:{value}",
|
|
127
|
+
backupOnExit: "退出时归档当前项目:{value}",
|
|
128
|
+
piExcludePaths: "Pi 备份排除项:{value}",
|
|
129
|
+
sessionProjects: "会话项目:{value}",
|
|
130
|
+
sessionProjectMode: "会话项目模式:{value}",
|
|
131
|
+
maxCloudBackups: "云端最大备份数:{value}",
|
|
132
|
+
language: "语言:中文",
|
|
133
|
+
notSet: "(未设置)",
|
|
134
|
+
passwordSet: "(已设置)",
|
|
135
|
+
none: "(无)",
|
|
136
|
+
piKind: "Pi",
|
|
137
|
+
skillsKind: "Skills",
|
|
138
|
+
sessionsKind: "会话",
|
|
139
|
+
|
|
140
|
+
setupRequired: "尚未配置 WebDAV,请先完成设置。",
|
|
141
|
+
promptWebdavUrl: "WebDAV 地址:",
|
|
142
|
+
promptWebdavUsername: "WebDAV 用户名:",
|
|
143
|
+
promptWebdavPassword: "WebDAV 令牌(或 $ENV_VAR):",
|
|
144
|
+
promptExcludePaths: "排除路径(以逗号分隔,相对于 ~/.pi/agent):",
|
|
145
|
+
promptMaxBackups: "最大备份数(0 = 全部保留):",
|
|
146
|
+
configSaved: "备份配置已更新。",
|
|
147
|
+
|
|
148
|
+
backupDisabled: "{kind} 备份已禁用。",
|
|
149
|
+
backupUploaded: "已上传 {filename}\n{contents}{pruned}",
|
|
150
|
+
backupFailed: "{kind} 备份失败:{error}",
|
|
151
|
+
prunedCount: "\n已清理旧备份:{count} 个",
|
|
152
|
+
noAllowedSessionProjects: "没有允许备份的本地会话项目。",
|
|
153
|
+
archiveSessionProject: "选择要归档的会话项目",
|
|
154
|
+
sessionArchiveUploaded: "已上传 {filename}\n{contents}",
|
|
155
|
+
sessionArchiveFailed: "会话归档失败:{error}",
|
|
156
|
+
|
|
157
|
+
noArchivesFound: "未找到 {kind} 归档。",
|
|
158
|
+
restoreArchive: "恢复 {kind} 归档",
|
|
159
|
+
replaceSharedSkillsTitle: "替换共享 Skills?",
|
|
160
|
+
replaceSharedSkillsBody: "当前 ~/.agents/skills 将移动到带时间戳的备份目录。",
|
|
161
|
+
restoreCompleted: "已恢复 {kind}:\n{contents}",
|
|
162
|
+
restoreFailed: "{kind} 恢复失败:{error}",
|
|
163
|
+
restoreSessionProject: "选择要恢复的会话项目",
|
|
164
|
+
selectSessionArchive: "选择会话归档",
|
|
165
|
+
sessionRestoreCompleted: "会话归档已恢复:\n{contents}",
|
|
166
|
+
sessionRestoreFailed: "会话恢复失败:{error}",
|
|
167
|
+
noRemoteSessionProjects: "未找到远端会话归档。",
|
|
168
|
+
confirmPiRestoreTitle: "恢复 Pi 备份?",
|
|
169
|
+
confirmPiRestoreBody: "归档条目:{count}\n{plan}",
|
|
170
|
+
confirmRestoreAllTitle: "恢复全部最新备份?",
|
|
171
|
+
confirmRestoreAllBody: "将恢复 Pi、已启用的 Skills,以及每个允许会话项目的最新归档。",
|
|
172
|
+
allBackupCompleted: "全部备份完成:\n{results}",
|
|
173
|
+
allRestoreCompleted: "全部恢复完成:\n{results}",
|
|
174
|
+
reloadRuntimeTitle: "重新加载运行时?",
|
|
175
|
+
reloadRuntimeBody: "现在重新加载 Pi 以应用恢复的数据?",
|
|
176
|
+
|
|
177
|
+
projectModeWhitelist: "白名单",
|
|
178
|
+
projectModeBlacklist: "黑名单",
|
|
179
|
+
projectSelectionWhitelist: "仅所选项目",
|
|
180
|
+
projectSelectionBlacklist: "除所选项目外全部",
|
|
181
|
+
projectSelectionAll: "全部项目",
|
|
182
|
+
selectSessionProjects: "选择会话项目[{mode}]",
|
|
183
|
+
switchProjectMode: "m 切换模式:{mode}",
|
|
184
|
+
selectAllProjects: "a 全选",
|
|
185
|
+
resetProjects: "r 重置",
|
|
186
|
+
|
|
187
|
+
exitBackupFailed: "退出时会话备份失败:{error}",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const STRINGS: Record<SyncLanguage, Record<TranslationKey, string>> = { en: EN, zh: ZH };
|
|
191
|
+
|
|
192
|
+
export function normalizeLanguage(value: unknown): SyncLanguage {
|
|
193
|
+
return typeof value === "string" && value.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function t(language: SyncLanguage, key: TranslationKey, params?: TranslationParams): string {
|
|
197
|
+
let message = STRINGS[language][key];
|
|
198
|
+
if (!params) return message;
|
|
199
|
+
for (const [name, value] of Object.entries(params)) {
|
|
200
|
+
message = message.split(`{${name}}`).join(String(value));
|
|
201
|
+
}
|
|
202
|
+
return message;
|
|
203
|
+
}
|
package/extensions/sync/index.ts
CHANGED
|
@@ -1,58 +1,35 @@
|
|
|
1
1
|
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
|
|
3
|
-
import { loadConfig, refreshFooterStatusFromConfig } from "./config";
|
|
4
|
-
import { registerSyncCommand } from "./menus";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
setSessionContext,
|
|
12
|
-
uploadCurrentSession,
|
|
13
|
-
} from "./session-sync";
|
|
4
|
+
import { isProjectAllowed, loadConfig, refreshFooterStatusFromConfig } from "./config";
|
|
5
|
+
import { registerSyncCommand, uploadSessionProjectArchive } from "./menus";
|
|
6
|
+
|
|
7
|
+
export function projectDirFromSessionDir(sessionDir: string | undefined): string | undefined {
|
|
8
|
+
if (!sessionDir) return undefined;
|
|
9
|
+
const projectDir = path.basename(sessionDir);
|
|
10
|
+
return projectDir.startsWith("--") && projectDir.endsWith("--") ? projectDir : undefined;
|
|
11
|
+
}
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
|
-
* pi-sync
|
|
14
|
+
* Archive-only pi-sync entrypoint.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* Runtime work is intentionally limited to one cached config read at startup
|
|
17
|
+
* and one current-project archive on shutdown. There are no per-turn hooks,
|
|
18
|
+
* timers, live uploads, or interval counters.
|
|
21
19
|
*/
|
|
22
|
-
export default function (pi: ExtensionAPI): void {
|
|
23
|
-
// Capture the active session file/project dir for live backup and interval sync.
|
|
20
|
+
export default function registerSyncExtension(pi: ExtensionAPI): void {
|
|
24
21
|
pi.on("session_start", async (_event, ctx) => {
|
|
25
|
-
setSessionContext(ctx.sessionManager.getSessionFile() ?? undefined, ctx.sessionManager.getSessionDir());
|
|
26
|
-
refreshFooterStatusFromConfig(ctx, loadConfig());
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Refresh the session path (compaction/fork may change it) and schedule a debounced live backup.
|
|
30
|
-
pi.on("agent_settled", async (_event, ctx) => {
|
|
31
|
-
refreshSessionFile(ctx.sessionManager.getSessionFile() ?? undefined);
|
|
32
|
-
const config = loadConfig();
|
|
33
|
-
if (config.liveSessionBackup && currentProjectIsAllowed(config)) {
|
|
34
|
-
scheduleLiveBackup(ctx);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Every-N-turns interval sync (0 = off). Only counts turns for allowed projects.
|
|
39
|
-
pi.on("turn_end", async (_event, ctx) => {
|
|
40
22
|
const config = loadConfig();
|
|
41
|
-
|
|
42
|
-
if (!currentProjectIsAllowed(config)) return;
|
|
43
|
-
refreshSessionFile(ctx.sessionManager.getSessionFile() ?? undefined);
|
|
44
|
-
if (incrementTurnAndShouldSync(config.syncIntervalTurns)) {
|
|
45
|
-
await uploadCurrentSession(ctx, true).catch(() => { /* silent */ });
|
|
46
|
-
}
|
|
23
|
+
refreshFooterStatusFromConfig(ctx, config);
|
|
47
24
|
});
|
|
48
25
|
|
|
49
|
-
// Flush on exit: upload if sync-on-exit or live backup is enabled.
|
|
50
26
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
51
|
-
clearLiveBackupTimer();
|
|
52
27
|
const config = loadConfig();
|
|
53
|
-
if (config.
|
|
54
|
-
|
|
55
|
-
|
|
28
|
+
if (!config.backupOnExit || !config.backupSessions) return;
|
|
29
|
+
if (!config.webdavUrl || !config.webdavUser || !config.webdavPass) return;
|
|
30
|
+
const projectDir = projectDirFromSessionDir(ctx.sessionManager.getSessionDir());
|
|
31
|
+
if (!projectDir || !isProjectAllowed(projectDir, config)) return;
|
|
32
|
+
await uploadSessionProjectArchive(ctx, config, projectDir, true);
|
|
56
33
|
});
|
|
57
34
|
|
|
58
35
|
registerSyncCommand(pi);
|