@gfgghgftgffg/amox-pi-sync 0.1.7 → 0.1.8

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/MAINTENANCE.md CHANGED
@@ -5,15 +5,15 @@
5
5
  - Upstream: [MuratAitov/pi-sync](https://github.com/MuratAitov/pi-sync), npm `pi-sync-suite@0.1.1`.
6
6
  - Source ref: `npm:pi-sync-suite@0.1.1` (published source baseline, not the repository's moving HEAD).
7
7
  - Upstream MIT notice is preserved verbatim in `LICENSE`: Copyright (c) 2026 Murat.
8
- - Local package: `@gfgghgftgffg/amox-pi-sync@0.1.7`, standalone TypeScript entrypoint `index.ts` and `core.ts`; no runtime dependency on the original suite.
8
+ - Local package: `@gfgghgftgffg/amox-pi-sync@0.1.8`, standalone TypeScript entrypoint `index.ts` and `core.ts`; no runtime dependency on the original suite.
9
9
 
10
10
  ## Scope
11
11
 
12
12
  Keep raw agent-data push/pull, safe relative include/exclude paths (sessions + missions by default), manual/background automatic sync, deletion propagation from a per-device `syncedPaths` baseline, orphan subagent pruning, backups/restoration and diagnostics. No configuration sync/settings merging, Markdown/JSON exports, Environment Tools or Local Packages. Cleanup is retained only for manually deleting local backup directories (Preview, Run, Delete Selected Backup, Retention); never sessions/missions, with no automatic deletion or archive restoration. Default cleanup candidates are complete backups older than 180 days or beyond the newest 20; counting individual files is not the retention unit. No three-line widget: optional Starline capsule/details, with `/sync-status` fallback.
13
13
 
14
- Settings are stored in `amox-pi-sync.json`; legacy `pi-sync-suite.json` is read only as a migration fallback until new settings are saved. Backups use `amox-pi-sync-backups` and require manual retention management via Cleanup. Manual/off startup must not access the network; auto startup fingerprints locally then starts timers and a non-awaited background pull. Setup only saves configuration and stops timers, requiring reload to activate auto mode. The automatic loop must not block the UI. Migration uses an independent `amox-pi-sync-repo`, not the old snapshot directory. The `pi-sync` status key preserves existing Starline placement; the pill bridge takes priority over legacy command mapping. `off` disables automation, not explicit commands. Presentation separates labels and values with a colon; the path count is explicitly marked configured. Starline action rows have a leading bullet, while native Pi selectors retain their own indicators. Presentation uses semantic Pi theme colours for labels, values and errors, and bold targets without fixed foreground/background colours so Starline owns selection contrast. Keep Manual/Auto/Off and Push/Pull terminology consistent across capsule, details, menus and command feedback; never append idle to the mode. Anchor bubbles to the clicked Starline cell, including submenus and confirmations; commands reuse the last clicked anchor or center before one is known. Forward pill colours by default and offer semantic theme colour overrides through bubbleColor; do not override selected-row text contrast. Colour changes must not start timers or network work. Without Starline use native Pi UI and hide the Starline colour option. Recompute theme styles when rendering, keep RPC/native status text plain, and do not store styled strings in settings. On Windows prefer native OpenSSH when present for non-ASCII home paths; retain batch mode, timeout and host-key verification. Keep large diagnostic results in the action notification rather than copying JSON dumps into status details.
14
+ Settings are stored in `amox-pi-sync.json`; legacy `pi-sync-suite.json` is read only as a migration fallback until new settings are saved. Backups use `amox-pi-sync-backups` and require manual retention management via Cleanup. Pull/restore backups contain only the local files being replaced or deleted plus a `files` manifest; new-file-only pulls create no backup. Manual Create backup remains full. Manual/off startup must not access the network; auto startup fingerprints locally then starts timers and a non-awaited background pull. Setup only saves configuration and stops timers, requiring reload to activate auto mode. The automatic loop must not block the UI. Migration uses an independent `amox-pi-sync-repo`, not the old snapshot directory. The `pi-sync` status key preserves existing Starline placement; the pill bridge takes priority over legacy command mapping. `off` disables automation, not explicit commands. Presentation separates labels and values with a colon; the path count is explicitly marked configured. Starline action rows have a leading bullet, while native Pi selectors retain their own indicators. Presentation uses semantic Pi theme colours for labels, values and errors, and bold targets without fixed foreground/background colours so Starline owns selection contrast. Keep Manual/Auto/Off and Push/Pull terminology consistent across capsule, details, menus and command feedback; never append idle to the mode. Anchor bubbles to the clicked Starline cell, including submenus and confirmations; commands reuse the last clicked anchor or center before one is known. Forward pill colours by default and offer semantic theme colour overrides through bubbleColor; do not override selected-row text contrast. Colour changes must not start timers or network work. Without Starline use native Pi UI and hide the Starline colour option. Recompute theme styles when rendering, keep RPC/native status text plain, and do not store styled strings in settings. On Windows prefer native OpenSSH when present for non-ASCII home paths; retain batch mode, timeout and host-key verification. Keep large diagnostic results in the action notification rather than copying JSON dumps into status details.
15
15
 
16
- Safety requirements: private SSH remote, no working-directory mutation, no semantic JSONL merging, preserve files that were never synced, back up replacements/deletions and never overwrite or delete active sessions. Deletion propagation must use `syncedPaths`: Push removes only previously synced paths missing locally, Pull removes only baseline paths absent from the new snapshot, and Pull preserves locally missing baseline paths as pending push deletions. A missing baseline is seeded at session start from files present both locally and in an existing snapshot; local-only and unpulled remote-only paths stay outside it. Pre-upgrade deletions are not inferred automatically. Prune orphan sessions scans `sessions/**/*.jsonl`, compares parent basenames case-insensitively, accepts `type#8hex` names, iterates parent candidates to a fixed point, skips files modified within 10 minutes and the active session, refuses dirty repositories, commits snapshot removals before deleting local files, and never deletes symlinks or non-files. Before-push pruning runs outside `run("Push")`, is Off by default, and never blocks a push on failure. Network transfers use a 10-minute timeout and local Git commands 60 seconds; a terminated or timed-out process must never be recorded as success, even when an executor reports code 0. Pull fast-forwards or performs a clean file-level three-way merge; conflicts abort and apply nothing. A rejected non-fast-forward push fetches, integrates without rebase or force, and retries once; files integrated during push are applied on the next pull. Git commands force `core.autocrlf=false` and `core.eol=lf`, with a repository-local `* -text` attribute; do not rewrite raw bytes or existing mixed-EOL history automatically. Safe-path validation must exclude configuration and implementation data even when users select arbitrary relative data paths. Do not claim encryption, conflict-free multi-device merging or automatic secret removal.
16
+ Safety requirements: private SSH remote, no working-directory mutation, no semantic JSONL merging, preserve files that were never synced, back up only the local files being replaced/deleted (new-file-only changes need no backup) and never overwrite or delete active sessions. Deletion propagation must use `syncedPaths`: Push removes only previously synced paths missing locally, Pull removes only baseline paths absent from the new snapshot, and Pull preserves locally missing baseline paths as pending push deletions. A missing baseline is seeded at session start from files present both locally and in an existing snapshot; local-only and unpulled remote-only paths stay outside it. Pre-upgrade deletions are not inferred automatically. Prune orphan sessions scans `sessions/**/*.jsonl`, compares parent basenames case-insensitively, accepts `type#8hex` names, iterates parent candidates to a fixed point, skips files modified within 10 minutes and the active session, refuses dirty repositories, commits snapshot removals before deleting local files, and never deletes symlinks or non-files. Before-push pruning runs outside `run("Push")`, is Off by default, and never blocks a push on failure. Network transfers use a 10-minute timeout and local Git commands 60 seconds; a terminated or timed-out process must never be recorded as success, even when an executor reports code 0. Pull fast-forwards or performs a clean file-level three-way merge; conflicts abort and apply nothing. A rejected non-fast-forward push fetches, integrates without rebase or force, and retries once; files integrated during push are applied on the next pull. Git commands force `core.autocrlf=false` and `core.eol=lf`, with a repository-local `* -text` attribute; do not rewrite raw bytes or existing mixed-EOL history automatically. Safe-path validation must exclude configuration and implementation data even when users select arbitrary relative data paths. Do not claim encryption, conflict-free multi-device merging or automatic secret removal.
17
17
 
18
18
  ## 功能取舍与维护边界
19
19
 
package/README.md CHANGED
@@ -47,7 +47,7 @@ Manual/off startup performs no network access. Auto startup computes a local fin
47
47
  - **Prune orphan sessions** (action or `/sync-prune`) scans `sessions/**/*.jsonl` for subagent sessions whose parent is missing or itself pruned. It compares parent basenames case-insensitively, accepts names matching `type#8hex`, skips files modified in the last 10 minutes and the active session, shows count/size/first names, commits snapshot removals first, then deletes local files. Staged or uncommitted repository changes abort without deleting local files. `--yes` skips confirmation. **Prune before push** runs it before manual, command and automatic pushes; a prune failure warns but does not block the push.
48
48
  - Network Git operations (`clone`, `fetch`, `pull`, `push`) use a 10-minute timeout; local Git operations use 60 seconds. A process terminated by timeout or cancellation is a failure and is never recorded as completed, even if Pi reports exit code 0. Pull fast-forwards when possible; otherwise it performs a clean three-way merge in the snapshot repository. Conflicts abort the merge and apply no files. A rejected non-fast-forward push fetches, integrates remote history without rebase or force, and retries once; files integrated during push are applied on the next pull.
49
49
  - Git commands force `core.autocrlf=false` and `core.eol=lf`, and the snapshot repository uses a local `* -text` attribute so Git does not convert raw bytes. Existing history with mixed CRLF/LF is not rewritten automatically; normalizing it requires an explicit manual migration and repack. On Windows, SSH prefers native OpenSSH (`System32/OpenSSH/ssh.exe`) when available to support non-ASCII user directories; otherwise it uses `ssh`. Batch mode, connection timeout and host-key verification remain enabled, overriding a custom `core.sshCommand`.
50
- - Backups use `amox-pi-sync-backups` under the agent-data directory. Cleanup only deletes local backups after manual preview/confirmation, using complete backup directories rather than individual files. The default limits are 20 complete backups and a maximum age of 180 days; backups older than this or beyond the newest 20 become cleanup candidates. There is no automatic deletion; sessions and missions are never cleanup targets.
50
+ - Backups use `amox-pi-sync-backups` under the agent-data directory. Pull and restore back up only the local files being replaced or deleted, and record them in the backup manifest; a pull that only adds new files creates no backup. Manual **Create backup** remains a full managed-data snapshot. Cleanup only deletes local backups after manual preview/confirmation, using complete backup directories rather than individual files. The default limits are 20 complete backups and a maximum age of 180 days; older or excess backups become cleanup candidates. There is no automatic deletion; sessions and missions are never cleanup targets.
51
51
  - Settings use `amox-pi-sync.json`. If absent, legacy `pi-sync-suite.json` can be read for migration. The old file is not changed before new settings are saved; saving writes the new file. Migration uses a separate `amox-pi-sync-repo` directory rather than reusing the old snapshot.
52
52
 
53
53
  Configuration sync/settings merging, Markdown/JSON archives, Environment Tools and Local Packages from the original suite are not included. Deployment tools such as `amox-agent-bootstrap` own environment setup and machine-specific configuration; Pi's native `/export` handles readable exports. Raw data selection, backups and manual backup cleanup remain part of sync. See [feature decisions](MAINTENANCE.md#功能取舍与维护边界) for the reasons and maintenance boundaries. This is a selectively maintained fork, not a runtime wrapper or a full replacement of the upstream suite. Daily reviews track published upstream changes, port worthwhile retained behavior, skip removed features and ask the owner through ntfy when uncertain; removed modules are not restored automatically.
package/README.zh-CN.md CHANGED
@@ -47,7 +47,7 @@ manual/off 启动不访问网络。auto 启动先计算本地 fingerprint,再
47
47
  - **Prune orphan sessions**(操作项或 `/sync-prune`)扫描 `sessions/**/*.jsonl`,找出父会话不存在或父会话本身也被清理的子代理会话。父文件按 basename 不区分大小写比对,名字匹配 `type#8hex`;最近 10 分钟内修改过的文件和当前活跃会话会跳过;预览显示条数、体积和前几条名称;先提交快照删除,再删本地文件。仓库有暂存或未提交修改时拒绝执行,不删本地文件。`--yes` 跳过确认。**Prune before push** 会在手动、命令和自动 Push 前执行;清理失败只警告,不阻断 Push。
48
48
  - 网络 Git 操作(`clone`、`fetch`、`pull`、`push`)超时为 10 分钟,本地 Git 操作为 60 秒。被超时或取消终止的进程一律视为失败,即使 Pi 返回 exit code 0,也不会写入完成时间。能快进时 Pull 直接快进,否则在快照仓库中执行干净的三方合并;冲突会中止合并且不应用任何文件。Push 被非快进拒绝时会 fetch、在不 rebase、不 force 的前提下整合远端历史,并重试一次;Push 整合的文件在下一次 Pull 时应用。
49
49
  - Git 操作强制 `core.autocrlf=false`、`core.eol=lf`,快照仓库使用本地 `* -text` 属性,避免 Git 转换原始字节。已有 CRLF/LF 混合历史不会被自动改写;统一换行需要显式的手动迁移和 repack。Windows 优先使用原生 OpenSSH(`System32/OpenSSH/ssh.exe`),支持中文用户目录;不存在时及其他平台使用 `ssh`。保留非交互模式、连接超时及主机密钥验证,不复用自定义 `core.sshCommand`。
50
- - 备份位于 agent 数据目录下的 `amox-pi-sync-backups`。Cleanup 仅在手动预览/确认后删除本地备份,按完整备份目录计数,不按单个文件计数。默认数量上限 20 份、最长 180 天;超龄或超出最新 20 份的备份纳入清理候选,不自动删除,不清理 sessions 或 missions。
50
+ - 备份位于 agent 数据目录下的 `amox-pi-sync-backups`。Pull 和恢复只备份本次会被替换或删除的本地文件,并在清单中记录;只新增文件的 Pull 不创建备份。手动 **Create backup** 仍是完整纳管数据快照。Cleanup 仅在手动预览/确认后删除本地备份,按完整备份目录计数,不按单个文件计数。默认数量上限 20 份、最长 180 天;超龄或超量的备份纳入清理候选,不自动删除,不清理 sessions 或 missions。
51
51
  - 新设置文件为 `amox-pi-sync.json`;不存在时可读取旧 `pi-sync-suite.json` 迁移。保存新设置前不改旧文件,保存时写入新文件。迁移使用独立的 `amox-pi-sync-repo` 目录,不复用旧快照。
52
52
 
53
53
  不包含原版的配置同步/settings 合并、Markdown/JSON 归档、Environment Tools 和 Local Packages。环境部署和机器专属配置交给 `amox-agent-bootstrap` 等部署流程,阅读版导出使用 Pi 原生 `/export`;同步器保留原始数据选择、备份恢复和手动备份清理。各项原因与后续维护边界见[功能取舍](MAINTENANCE.md#功能取舍与维护边界)。这是选择性跟进上游的独立 fork,不是调用原版运行时的包装,也不是原版全功能替代品。每日审查上游正式发布,仅移植保留范围内有价值的改动;舍弃功能跳过,不能判断时由 ntfy 请维护者决定,不自动恢复原版模块。
package/core.ts CHANGED
@@ -39,7 +39,7 @@ export type Config = {
39
39
  export type Paths = { agentDir: string; configFile: string; backupDir: string };
40
40
  export type GitExecutor = (args: string[], cwd: string, timeoutMs?: number) => Promise<{ stdout: string; stderr: string; code: number; killed?: boolean }>;
41
41
  export type Status = { busy?: string; error?: string; localChanges?: string; remoteChanges?: number; checkedAt?: string };
42
- export type Backup = { id: string; createdAt: string; reason: string; includedPaths: string[] };
42
+ export type Backup = { id: string; createdAt: string; reason: string; includedPaths: string[]; files?: string[] };
43
43
  export type BackupSummary = Backup;
44
44
  export type SyncResult = { changed: boolean; message: string; files: string[]; backupId?: string };
45
45
  export type SessionEntry = { path: string; parentSession?: string; name?: string; bytes?: number; mtimeMs?: number };
@@ -428,13 +428,19 @@ export class SyncService {
428
428
  finally { await fs.rm(tmp, { force: true }); }
429
429
  }
430
430
  }
431
- private async makeBackup(reason: string): Promise<Backup> {
432
- const files = await this.collect(this.paths.agentDir);
431
+ private async makeBackup(reason: string, selected?: Files): Promise<Backup> {
432
+ const files = selected ?? await this.collect(this.paths.agentDir);
433
433
  const id = `${new Date().toISOString().replaceAll(/[:.]/g, "-")}-${randomUUID()}`;
434
434
  const root = path.join(this.paths.backupDir, id); await noLinks(root);
435
435
  await fs.mkdir(path.join(root, "files"), { recursive: true });
436
436
  await this.write(path.join(root, "files"), files);
437
- const manifest: Backup = { id, createdAt: new Date().toISOString(), reason, includedPaths: [...this.config.paths] };
437
+ const includedPaths = selected
438
+ ? [...new Set([...files.keys()].map(item => item.split("/")[0]!))].sort()
439
+ : [...this.config.paths];
440
+ const manifest: Backup = {
441
+ id, createdAt: new Date().toISOString(), reason, includedPaths,
442
+ ...(selected ? { files: [...files.keys()].sort() } : {}),
443
+ };
438
444
  await fs.writeFile(path.join(root, "manifest.json"), JSON.stringify(manifest, null, 2), { flag: "wx", mode: 0o600 });
439
445
  return manifest;
440
446
  }
@@ -450,6 +456,8 @@ export class SyncService {
450
456
  const manifest = JSON.parse(await fs.readFile(file, "utf8")) as Backup;
451
457
  if (manifest.id !== entry.name || !Array.isArray(manifest.includedPaths) || !Number.isFinite(Date.parse(manifest.createdAt)) || typeof manifest.reason !== "string") throw new Error(`Invalid backup manifest: ${entry.name}`);
452
458
  manifest.includedPaths.forEach(validatePath);
459
+ if (manifest.files !== undefined && (!Array.isArray(manifest.files) || manifest.files.some(item => typeof item !== "string"))) throw new Error(`Invalid backup file list: ${entry.name}`);
460
+ manifest.files?.forEach(validatePath);
453
461
  const filesDir = path.join(this.paths.backupDir, entry.name, "files"); await noLinks(filesDir);
454
462
  if (!(await stat(filesDir))?.isDirectory()) throw new Error(`Incomplete backup: ${entry.name}`);
455
463
  result.push(manifest);
@@ -457,7 +465,8 @@ export class SyncService {
457
465
  return result.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
458
466
  }
459
467
  private async apply(files: Files, reason: string, deletions: string[] = [], preserve: Set<string> = new Set()): Promise<SyncResult> {
460
- const changes: Files = new Map(); const replaced: string[] = []; const removed: string[] = [];
468
+ const changes: Files = new Map(); const backupFiles: Files = new Map();
469
+ const replaced: string[] = []; const removed: string[] = [];
461
470
  let active = this.activeSessionFile ? path.resolve(this.paths.agentDir, this.activeSessionFile) : undefined;
462
471
  if (active) {
463
472
  try { active = await fs.realpath(active); }
@@ -473,7 +482,8 @@ export class SyncService {
473
482
  const local = info ? await fs.readFile(target) : undefined;
474
483
  if (local?.equals(data)) continue;
475
484
  if (sameActive(target)) throw new Error(`Active session differs: ${relative}. Switch to a new session or close it before pull/restore; no files were applied.`);
476
- changes.set(relative, data); if (local) replaced.push(relative);
485
+ changes.set(relative, data);
486
+ if (local) { replaced.push(relative); backupFiles.set(relative, local); }
477
487
  }
478
488
  for (const relative of deletions) {
479
489
  validatePath(relative);
@@ -482,17 +492,21 @@ export class SyncService {
482
492
  if (!info) continue;
483
493
  if (!info.isFile()) throw new Error(`File/directory conflict at ${relative}; move the local entry aside and retry`);
484
494
  if (sameActive(target)) throw new Error(`Active session cannot be deleted: ${relative}. Switch to a new session before pull/restore; no files were applied.`);
495
+ backupFiles.set(relative, await fs.readFile(target));
485
496
  removed.push(relative);
486
497
  }
487
498
  const preserved = preserve.size ? `; preserved ${preserve.size} local deletion(s) pending push` : "";
488
499
  if (!changes.size && !removed.length) return { changed: false, message: `Already up to date; no files applied or backup created${preserved}`, files: [] };
489
- const backup = await this.makeBackup(reason);
500
+ const backup = backupFiles.size ? await this.makeBackup(reason, backupFiles) : undefined;
490
501
  await this.write(this.paths.agentDir, changes);
491
502
  for (const relative of removed) await fs.rm(path.join(this.paths.agentDir, relative), { force: true });
492
503
  const deleted = removed.length ? `; deleted ${removed.length} remote-deleted file(s)` : "";
493
- const message = `Applied ${changes.size} file(s)${deleted}; replaced ${replaced.length} differing local file(s) from remote/backup, preserved local-only files${preserved}. Recovery backup: ${backup.id}${replaced.length ? `; replaced: ${replaced.join(", ")}` : ""}`;
504
+ const recovery = backup
505
+ ? `Recovery backup: ${backup.id}`
506
+ : "No recovery backup was needed (only new files were added)";
507
+ const message = `Applied ${changes.size} file(s)${deleted}; replaced ${replaced.length} differing local file(s) from remote/backup, preserved local-only files${preserved}. ${recovery}${replaced.length ? `; replaced: ${replaced.join(", ")}` : ""}`;
494
508
  this.config.lastResult = message;
495
- return { changed: changes.size > 0 || removed.length > 0, message, files: [...changes.keys()], backupId: backup.id };
509
+ return { changed: changes.size > 0 || removed.length > 0, message, files: [...changes.keys()], backupId: backup?.id };
496
510
  }
497
511
  async planPruneSessions(): Promise<PrunePlan> {
498
512
  return selectOrphanSessions(await this.scanSessionEntries(), { activePath: this.activeSessionRelative() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gfgghgftgffg/amox-pi-sync",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "Independent Pi agent-data sync fork with SSH Git transport, backups and optional Starline status.",
6
6
  "license": "MIT",