@gfgghgftgffg/amox-pi-sync 0.1.1 → 0.1.2

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,13 +5,13 @@
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.1`, standalone TypeScript entrypoint `index.ts` and `core.ts`; no runtime dependency on the original suite.
8
+ - Local package: `@gfgghgftgffg/amox-pi-sync@0.1.2`, 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, 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.
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 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. Recompute styles when rendering, keep RPC/native status text plain, and do not store styled strings in settings. Keep large diagnostic results in the action notification rather than copying JSON dumps into status details.
15
15
 
16
16
  Safety requirements: private SSH remote, no working-directory mutation, no JSONL merging, preserve local-only files, back up replacements and never overwrite active sessions. 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
 
package/README.md CHANGED
@@ -26,14 +26,14 @@ pi update --extensions
26
26
  | Command | Purpose |
27
27
  | --- | --- |
28
28
  | `/sync-setup git@host:owner/private-repo.git [interval]` | Configure an SSH repository and optional pull interval in minutes (no transfer) |
29
- | `/sync-push` | Upload selected raw agent data |
29
+ | `/sync-push` | Push selected raw agent data |
30
30
  | `/sync-pull` | Pull selected raw agent data |
31
31
  | `/sync-settings` | Open settings and maintenance actions |
32
32
  | `/sync-status` | View status and actions; also works without Starline |
33
33
 
34
34
  The settings menu contains **View Status**, **Sync Mode** (`auto`, `manual`, `off`), **Sync Paths** (include/exclude safe relative agent-data paths), **Backups** (create/list/restore), **Diagnostics** (doctor/diff/log), and **Cleanup** (Preview, Run, Delete Selected Backup, Retention). `sessions` and `missions` are the default paths. Manual push and pull commands remain available in both `manual` and `off` modes.
35
35
 
36
- Manual/off startup performs no network access. Auto startup computes a local fingerprint, starts timers and launches a background pull without awaiting network completion. `/sync-setup` only saves configuration and stops timers; reload to activate auto mode if enabled. With Starline installed, use its clickable status capsule to open details and actions: check status, upload, pull and settings. The status key remains `pi-sync` for existing Starline placement; the pill bridge takes precedence over legacy `extensionStatuses.commands`, with no Starline configuration changes required. There is no three-line widget. `/sync-status`, pill clicks, settings, submenus and confirmations share centered Starline bubbles above the footer; text entry still uses Pi input dialogs. Without Starline, the native UI remains available.
36
+ Manual/off startup performs no network access. Auto startup computes a local fingerprint, starts timers and launches a background pull without awaiting network completion. `/sync-setup` only saves configuration and stops timers; reload to activate auto mode if enabled. With Starline installed, use its clickable status capsule to open details and actions: check status, push, pull and settings. The status key remains `pi-sync` for existing Starline placement; the pill bridge takes precedence over legacy `extensionStatuses.commands`, with no Starline configuration changes required. There is no three-line widget. `/sync-status`, pill clicks, settings, submenus and confirmations share centered Starline bubbles above the footer; text entry still uses Pi input dialogs. Labels and values use Pi theme colours, while bold action rows keep Starline’s selection contrast. State shows Manual, Auto or Off directly; active operations use Push/Pull, with the configured mode shown separately. Without Starline, the native UI remains available and status output stays plain text.
37
37
 
38
38
  ## Data and safety
39
39
 
package/README.zh-CN.md CHANGED
@@ -26,14 +26,14 @@ pi update --extensions
26
26
  | 命令 | 用途 |
27
27
  | --- | --- |
28
28
  | `/sync-setup git@host:owner/private-repo.git [interval]` | 配置 SSH 仓库及可选拉取间隔(分钟),不传输数据 |
29
- | `/sync-push` | 上传选中的原始 agent 数据 |
30
- | `/sync-pull` | 拉取选中的原始 agent 数据 |
29
+ | `/sync-push` | Push 选中的原始 agent 数据 |
30
+ | `/sync-pull` | Pull 选中的原始 agent 数据 |
31
31
  | `/sync-settings` | 打开设置与维护操作 |
32
32
  | `/sync-status` | 查看状态和操作;未安装 Starline 时也可使用 |
33
33
 
34
34
  设置菜单保留 **View Status**、**Sync Mode**(`auto`、`manual`、`off`)、**Sync Paths**(包含/排除任意安全的 agent 数据相对路径)、**Backups**(创建/列表/恢复)、**Diagnostics**(doctor/diff/log)和 **Cleanup**(Preview、Run、Delete Selected Backup、Retention)。默认路径为 `sessions` 和 `missions`。`manual` 和 `off` 模式仍可手动上传、拉取。
35
35
 
36
- manual/off 启动不访问网络。auto 启动先计算本地 fingerprint,再启动定时器并发起后台拉取,不等待网络完成。`/sync-setup` 只保存配置并停止定时器;已启用 auto 时需 reload 激活。安装 Starline 时,优先点击状态胶囊打开详情,执行状态检查、上传、拉取或设置。状态 key 保留为 `pi-sync`,兼容原 Starline placement;pill bridge 优先于旧 `extensionStatuses.commands`,无需修改 Starline 配置。没有三行 widget。`/sync-status`、点击胶囊、设置和子菜单统一使用底栏上方居中的 Starline 气泡,确认操作也使用相同样式;文本输入仍使用 Pi 输入框。未安装 Starline 时回退原生界面。
36
+ manual/off 启动不访问网络。auto 启动先计算本地 fingerprint,再启动定时器并发起后台拉取,不等待网络完成。`/sync-setup` 只保存配置并停止定时器;已启用 auto 时需 reload 激活。安装 Starline 时,优先点击状态胶囊打开详情,执行 Check status、Push、Pull 或 Settings。状态 key 保留为 `pi-sync`,兼容原 Starline placement;pill bridge 优先于旧 `extensionStatuses.commands`,无需修改 Starline 配置。没有三行 widget。`/sync-status`、点击胶囊、设置和子菜单统一使用底栏上方居中的 Starline 气泡,确认操作也使用相同样式;文本输入仍使用 Pi 输入框。标签、数值和异常使用 Pi 主题色区分,可选操作使用粗体并保留 Starline 自带的选中对比度。State 直接显示 Manual、Auto 或 Off;执行中显示 Push/Pull,另列当前模式,不追加 idle。未安装 Starline 时回退原生界面,状态输出保留纯文本。
37
37
 
38
38
  ## 数据与安全
39
39
 
package/index.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  // Derived from pi-sync-suite 0.1.1, Copyright (c) 2026 Murat (MIT).
2
2
  // Upstream: https://github.com/MuratAitov/pi-sync
3
+ import { stripVTControlCharacters } from "node:util";
3
4
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
5
  import { SyncService, createConfig, defaultPaths, loadConfig, saveConfig, validatePath, type Config } from "./core.ts";
5
6
 
6
- type BubbleLine = string | { text: string; onSelect: () => void };
7
+ type BubbleLine = string | { text: string; onSelect: () => void; current?: boolean };
8
+ type Tone = "text" | "muted" | "dim" | "accent" | "success" | "warning" | "error";
9
+ type DetailRow = { label: string; value: string; tone?: Tone };
10
+ type Choice = string | { value: string; label: string };
11
+ type SelectDetails = { description?: string; rows?: () => DetailRow[]; current?: string };
7
12
  type BubbleContent = { title: string; body: BubbleLine[]; hint: string };
8
13
  type BubbleHandle = { refresh(): void; close(): void; onClose?(callback: () => void): void };
9
14
  type PillAPI = {
@@ -13,8 +18,12 @@ type PillAPI = {
13
18
  };
14
19
  const STATUS_KEY = "pi-sync";
15
20
  const pillAPI = () => (globalThis as Record<symbol, unknown>)[Symbol.for("pi-starline.pill-api.v1")] as PillAPI | undefined;
16
- const message = (error: unknown) => error instanceof Error ? error.message : String(error);
17
- const displayTime = (value?: string) => value ? new Date(value).toLocaleString() : "never";
21
+ const bubbleAPI = (context: ExtensionContext) => context.mode === "rpc" ? undefined : pillAPI();
22
+ const message = (error: unknown) => terminalText(error instanceof Error ? error.message : String(error));
23
+ const displayTime = (value?: string) => value ? new Date(value).toLocaleString() : "Never";
24
+ const modeLabel = (mode: Config["mode"]) => ({ manual: "Manual", auto: "Auto", off: "Off" })[mode];
25
+ const terminalText = (value: string) => stripVTControlCharacters(value).replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
26
+ const menuHint = "↑↓ Enter Esc";
18
27
 
19
28
  export default function amoxPiSync(pi: ExtensionAPI): void {
20
29
  let ctx: ExtensionContext | undefined;
@@ -34,8 +43,10 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
34
43
  let watching = false;
35
44
  let generation = 0;
36
45
 
46
+ const stateLabel = () => busy ?? (error ? "Error" : config ? modeLabel(config.mode) : "Not configured");
47
+ const stateTone = (): Tone => error && !busy ? "error" : busy ? "accent" : config?.mode === "off" ? "muted" : config ? "accent" : "warning";
37
48
  const publish = () => {
38
- ctx?.ui.setStatus(STATUS_KEY, `Sync: ${busy ?? (error ? "error" : config?.mode ?? "setup")}`);
49
+ ctx?.ui.setStatus(STATUS_KEY, `Sync: ${stateLabel()}`);
39
50
  bubble?.refresh();
40
51
  };
41
52
 
@@ -62,7 +73,7 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
62
73
  return false;
63
74
  }
64
75
  if (busy) {
65
- if (!quiet) ctx.ui.notify(`Sync is already ${busy}.`, "warning");
76
+ if (!quiet) ctx.ui.notify(`Sync is busy: ${busy}.`, "warning");
66
77
  return false;
67
78
  }
68
79
  const active = service;
@@ -75,16 +86,17 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
75
86
  error = undefined;
76
87
  publish();
77
88
  try {
78
- const uploadedFingerprint = label === "uploading" ? await active.fingerprint() : undefined;
89
+ const pushedFingerprint = label === "Push" ? await active.fingerprint() : undefined;
79
90
  const result = await action(active);
80
- notice = typeof result === "string" ? result : `${label} completed`;
81
- // Mark only the pre-upload snapshot as observed. Changes made while Git
91
+ notice = `${label} completed`;
92
+ // Mark only the pre-push snapshot as observed. Changes made while Git
82
93
  // runs must remain detectable by the next watcher pass.
83
- if (uploadedFingerprint !== undefined) fingerprint = uploadedFingerprint;
84
- if (!quiet) context.ui.notify(notice, "info");
94
+ if (pushedFingerprint !== undefined) fingerprint = pushedFingerprint;
95
+ if (!quiet) context.ui.notify(typeof result === "string" ? terminalText(result) : notice, "info");
85
96
  return true;
86
97
  } catch (failure) {
87
98
  error = message(failure);
99
+ notice = `${label} failed`;
88
100
  context.ui.notify(`amox-pi-sync ${label}: ${error}`, "error");
89
101
  return false;
90
102
  } finally {
@@ -110,7 +122,7 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
110
122
  }
111
123
  if (token !== generation) return;
112
124
  pullTimer = setInterval(() => {
113
- if (!busy) void run("downloading", sync => sync.pull(), true);
125
+ if (!busy) void run("Pull", sync => sync.pull(), true);
114
126
  }, config.pullIntervalMinutes * 60_000);
115
127
  watchTimer = setInterval(() => {
116
128
  if (busy || watching) return;
@@ -119,7 +131,7 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
119
131
  if (token !== generation || next === fingerprint || busy) return;
120
132
  if (debounce) clearTimeout(debounce);
121
133
  debounce = setTimeout(() => {
122
- if (token === generation && !busy) void run("uploading", sync => sync.push(), true);
134
+ if (token === generation && !busy) void run("Push", sync => sync.push(), true);
123
135
  }, config!.pushDebounceMs);
124
136
  }).catch(failure => {
125
137
  const text = message(failure);
@@ -129,42 +141,72 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
129
141
  }).finally(() => { watching = false; });
130
142
  }, config.watchIntervalMs);
131
143
  // Do not await network during session startup.
132
- void run("downloading", sync => sync.pull(), true);
144
+ void run("Pull", sync => sync.pull(), true);
133
145
  }
134
146
 
135
- function detailLines(): string[] {
136
- if (!config || !service) return ["Not configured", "Use /sync-setup <private-ssh-repository>", ...(error ? [`Error: ${error}`] : [])];
147
+ function overviewRows(): DetailRow[] {
148
+ if (!config || !service) return [
149
+ { label: "State", value: stateLabel(), tone: stateTone() },
150
+ { label: "Setup", value: "/sync-setup <private-ssh-repository>", tone: "accent" },
151
+ ...(error ? [{ label: "Error", value: error, tone: "error" as const }] : []),
152
+ ];
153
+ return [
154
+ { label: "State", value: stateLabel(), tone: stateTone() },
155
+ ...(busy || error ? [{ label: "Mode", value: modeLabel(config.mode), tone: "muted" as const }] : []),
156
+ { label: "Paths", value: config.paths.join(", ") || "None" },
157
+ { label: "Remote", value: config.repoUrl },
158
+ ];
159
+ }
160
+
161
+ function detailRows(): DetailRow[] {
162
+ const rows = overviewRows();
163
+ if (!config || !service) return rows;
137
164
  const status = service.status;
165
+ const local = status.localChanges?.trim();
166
+ const remoteChecked = Boolean(status.checkedAt) && status.remoteChanges !== undefined;
138
167
  return [
139
- `State: ${busy ?? (error ? "error" : `idle (${config.mode})`)}`,
140
- `Objects: ${config.paths.join(", ") || "none"}`,
141
- `Remote: ${config.repoUrl}`,
142
- `Last upload: ${displayTime(config.lastPushAt)}`,
143
- `Last pull: ${displayTime(config.lastPullAt)}`,
144
- `Local: ${status.localChanges?.trim() || (status.checkedAt ? "no pending changes at last check" : "not checked")}`,
145
- `Remote updates: ${status.remoteChanges === undefined ? "not checked" : status.remoteChanges}`,
146
- `Checked: ${displayTime(status.checkedAt)}`,
147
- ...(config.lastResult ? [`Last sync result: ${config.lastResult}`] : []),
148
- ...(notice ? [`Last action: ${notice}`] : []),
149
- ...(error ? [`Error: ${error}`] : []),
168
+ ...rows,
169
+ { label: "Last push", value: displayTime(config.lastPushAt), tone: config.lastPushAt ? "text" : "dim" },
170
+ { label: "Last pull", value: displayTime(config.lastPullAt), tone: config.lastPullAt ? "text" : "dim" },
171
+ { label: "Last check", value: displayTime(status.checkedAt), tone: status.checkedAt ? "text" : "dim" },
172
+ { label: "Local changes", value: local || (status.checkedAt ? "None at last check" : "Not checked"), tone: local ? "warning" : status.checkedAt ? "success" : "dim" },
173
+ { label: "Remote updates", value: remoteChecked ? String(status.remoteChanges) : "Not checked", tone: remoteChecked ? status.remoteChanges ? "warning" : "success" : "dim" },
174
+ ...(config.lastResult ? [{ label: "Last sync", value: config.lastResult }] : []),
175
+ ...(notice ? [{ label: "Last action", value: notice }] : []),
176
+ ...(error ? [{ label: "Error", value: error, tone: "error" as const }] : []),
150
177
  ];
151
178
  }
152
179
 
180
+ function detailLines(): string[] {
181
+ return detailRows().map(row => `${row.label}: ${terminalText(row.value)}`);
182
+ }
183
+
184
+ function renderRows(context: ExtensionContext, rows: DetailRow[]): string[] {
185
+ const theme = context.ui.theme;
186
+ return rows.map(row => theme.fg("muted", `${row.label.padEnd(14)} `)
187
+ + theme.fg(row.tone ?? "text", row.label === "State" ? theme.bold(terminalText(row.value)) : terminalText(row.value)));
188
+ }
189
+
190
+ // Targets inherit Starline's foreground so a selected row remains legible on
191
+ // its pill-coloured background. Bold adds hierarchy without overriding it.
192
+ const actionText = (context: ExtensionContext, text: string) => context.ui.theme.bold(terminalText(text));
193
+
153
194
  function content(context: ExtensionContext): BubbleContent {
154
- const body: BubbleLine[] = detailLines();
195
+ const body: BubbleLine[] = renderRows(context, detailRows());
155
196
  if (config && !busy) {
156
- body.push(
157
- { text: "Check status", onSelect: () => { void run("checking", sync => sync.check()); } },
158
- { text: "Upload", onSelect: () => { void run("uploading", sync => sync.push()); } },
159
- { text: "Pull", onSelect: () => { void run("downloading", sync => sync.pull()); } },
197
+ body.push("", context.ui.theme.fg("muted", "Actions"),
198
+ { text: actionText(context, "Check status"), onSelect: () => { void run("Check", sync => sync.check()); } },
199
+ { text: actionText(context, "Push"), onSelect: () => { void run("Push", sync => sync.push()); } },
200
+ { text: actionText(context, "Pull"), onSelect: () => { void run("Pull", sync => sync.pull()); } },
160
201
  );
161
- }
162
- if (!busy) body.push({ text: "Settings", onSelect: () => settings(context).catch(report) });
163
- return { title: "Amox Pi Sync", body, hint: "Last-check values are not a live guarantee · Esc to close" };
202
+ } else if (!busy) body.push("");
203
+ if (!busy) body.push({ text: actionText(context, "Settings"), onSelect: () => settings(context).catch(report) });
204
+ body.push("", context.ui.theme.fg("dim", "Values reflect the last check."));
205
+ return { title: "Amox Pi Sync", body, hint: menuHint };
164
206
  }
165
207
 
166
208
  function showStatus(context: ExtensionContext): void {
167
- const api = pillAPI();
209
+ const api = bubbleAPI(context);
168
210
  if (api?.version === 1 && context.hasUI) {
169
211
  // All command, pill and menu views share Starline's unanchored placement.
170
212
  bubble = api.showBubble(context, () => content(context));
@@ -173,18 +215,26 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
173
215
  }
174
216
  }
175
217
 
176
- async function select(context: ExtensionContext, title: string, options: string[], description?: string): Promise<string | undefined> {
177
- const api = pillAPI();
178
- if (api?.version !== 1 || !context.hasUI) return context.ui.select(title, options);
218
+ async function select(context: ExtensionContext, title: string, options: Choice[], details: SelectDetails = {}): Promise<string | undefined> {
219
+ const choices = options.map(option => typeof option === "string" ? { value: option, label: option } : option);
220
+ const nativeSelect = async () => {
221
+ const heading = details.description ? `${title}\n${terminalText(details.description)}` : title;
222
+ const label = await context.ui.select(heading, choices.map(choice => terminalText(choice.label)));
223
+ return choices.find(choice => terminalText(choice.label) === label)?.value;
224
+ };
225
+ const api = bubbleAPI(context);
226
+ if (api?.version !== 1 || !context.hasUI) return nativeSelect();
179
227
  return new Promise(resolve => {
180
228
  let selected = false;
181
229
  const handle = api.showBubble(context, () => ({
182
230
  title,
183
231
  body: [
184
- ...(description ? [description] : []),
185
- ...options.map(text => ({ text, onSelect: () => { selected = true; resolve(text); } })),
232
+ ...(details.rows ? [...renderRows(context, details.rows()), ""] : []),
233
+ ...(details.description ? [context.ui.theme.fg("muted", terminalText(details.description)), ""] : []),
234
+ ...choices.map(choice => ({ text: actionText(context, choice.label), current: choice.value === details.current,
235
+ onSelect: () => { selected = true; resolve(choice.value); } })),
186
236
  ],
187
- hint: "↑/↓ · Enter to select · Esc to close",
237
+ hint: menuHint,
188
238
  }));
189
239
  bubble = handle;
190
240
  if (handle?.onClose) {
@@ -193,14 +243,14 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
193
243
  handle.onClose(() => queueMicrotask(() => { if (!selected) resolve(undefined); }));
194
244
  } else {
195
245
  handle?.close();
196
- void context.ui.select(title, options).then(resolve);
246
+ void nativeSelect().then(resolve);
197
247
  }
198
248
  });
199
249
  }
200
250
 
201
251
  async function confirm(context: ExtensionContext, title: string, description: string): Promise<boolean> {
202
- if (pillAPI()?.version !== 1 || !context.hasUI) return context.ui.confirm(title, description);
203
- return await select(context, title, ["Cancel", "Confirm"], description) === "Confirm";
252
+ if (bubbleAPI(context)?.version !== 1 || !context.hasUI) return context.ui.confirm(title, description);
253
+ return await select(context, title, ["Cancel", "Confirm"], { description, current: "Cancel" }) === "Confirm";
204
254
  }
205
255
 
206
256
  const report = (failure: unknown) => {
@@ -210,7 +260,7 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
210
260
  };
211
261
 
212
262
  async function persist(next: Config): Promise<void> {
213
- if (busy) throw new Error(`Sync is already ${busy}; retry the settings change after it finishes.`);
263
+ if (busy) throw new Error(`Sync is busy: ${busy}; retry the settings change after it finishes.`);
214
264
  await saveConfig(next, paths);
215
265
  config = next;
216
266
  makeService();
@@ -219,17 +269,21 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
219
269
  }
220
270
 
221
271
  async function settings(context: ExtensionContext): Promise<void> {
222
- if (busy) { context.ui.notify(`Sync is already ${busy}.`, "warning"); return; }
272
+ if (busy) { context.ui.notify(`Sync is busy: ${busy}.`, "warning"); return; }
223
273
  for (;;) {
224
- const choice = await select(context, "Amox Pi Sync", [
225
- "View Status", `Sync Mode [${config?.mode ?? "not configured"}]`,
226
- `Sync Paths [${config?.paths.length ?? 0}]`, "Backups", "Cleanup", "Diagnostics", "Back",
227
- ]);
274
+ const choice = await select(context, "Sync settings", [
275
+ { value: "status", label: "Status" },
276
+ { value: "mode", label: `Mode ${config ? modeLabel(config.mode) : "Not configured"}` },
277
+ { value: "paths", label: `Paths ${config?.paths.length ?? 0}` },
278
+ "Backups", "Cleanup", "Diagnostics", "Back",
279
+ ], { rows: overviewRows });
228
280
  if (!choice || choice === "Back") return;
229
- if (choice === "View Status") { showStatus(context); return; }
281
+ if (choice === "status") { showStatus(context); return; }
230
282
  if (!config || !service) { context.ui.notify("Run /sync-setup first.", "warning"); continue; }
231
- if (choice.startsWith("Sync Mode")) {
232
- const selected = await select(context, "Sync mode", ["Manual", "Auto", "Off", "Back"]);
283
+ if (choice === "mode") {
284
+ const selected = await select(context, "Sync mode", ["Manual", "Auto", "Off", "Back"], {
285
+ current: modeLabel(config.mode), description: "Auto runs in the background. Manual and Off still allow Push and Pull.",
286
+ });
233
287
  if (!selected || selected === "Back") continue;
234
288
  const next = structuredClone(config);
235
289
  if (selected === "Auto") {
@@ -242,7 +296,7 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
242
296
  }
243
297
  next.mode = selected.toLowerCase() as Config["mode"];
244
298
  await persist(next);
245
- } else if (choice.startsWith("Sync Paths")) {
299
+ } else if (choice === "paths") {
246
300
  const selected = await select(context, "Sync paths", [
247
301
  ...config.paths.map(item => `Exclude ${item}`), "Include path", "Back",
248
302
  ]);
@@ -256,38 +310,38 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
256
310
  } else next.paths = next.paths.filter(item => `Exclude ${item}` !== selected);
257
311
  await persist(next);
258
312
  } else if (choice === "Backups") {
259
- const selected = await select(context, "Backups (local; no automatic cleanup)", ["Create", "List", "Restore", "Back"]);
260
- if (selected === "Create") await run("backing up", async sync => {
313
+ const selected = await select(context, "Backups", ["Create", "List", "Restore", "Back"], { description: "Local backups. Cleanup is manual; nothing is deleted automatically." });
314
+ if (selected === "Create") await run("Create backup", async sync => {
261
315
  const backup = await sync.backup(); return JSON.stringify(backup, null, 2);
262
316
  });
263
- if (selected === "List") await run("listing backups", async sync => JSON.stringify(await sync.listBackups(), null, 2));
317
+ if (selected === "List") await run("List backups", async sync => JSON.stringify(await sync.listBackups(), null, 2));
264
318
  if (selected === "Restore") {
265
319
  const backups = await service.listBackups();
266
320
  if (!backups.length) { context.ui.notify("No backups.", "info"); continue; }
267
321
  const id = await context.ui.input("Backup ID", "latest");
268
322
  if (!id) continue;
269
323
  if (!await confirm(context, "Restore backup?", "Matching managed files can be overwritten. A pre-restore backup will be created. The active session cannot be replaced.")) continue;
270
- await run("restoring", sync => sync.restore(id));
324
+ await run("Restore backup", sync => sync.restore(id));
271
325
  }
272
326
  } else if (choice === "Cleanup") {
273
- const selected = await select(context, "Cleanup (complete local backups only)", ["Preview", "Run", "Delete Selected Backup", "Retention", "Back"]);
274
- if (selected === "Preview" || selected === "Run") {
327
+ const selected = await select(context, "Backup cleanup", ["Preview", "Delete eligible backups", "Delete backup", "Retention", "Back"], { description: "Only complete local backups can be deleted. Sessions and missions are never cleanup targets." });
328
+ if (selected === "Preview" || selected === "Delete eligible backups") {
275
329
  let candidates: Awaited<ReturnType<SyncService["planCleanup"]>> = [];
276
- const planned = await run("planning cleanup", async sync => {
330
+ const planned = await run("Preview cleanup", async sync => {
277
331
  candidates = await sync.planCleanup();
278
332
  return candidates.length ? JSON.stringify(candidates, null, 2) : "No backup cleanup candidates.";
279
333
  });
280
- if (!planned || selected !== "Run" || !candidates.length) continue;
334
+ if (!planned || selected !== "Delete eligible backups" || !candidates.length) continue;
281
335
  if (await confirm(context, "Delete complete backups?", `Delete ${candidates.length} previewed backup(s)? Sessions, missions and the sync repository will not be deleted.`)) {
282
- await run("cleaning backups", async sync => `Deleted ${await sync.applyCleanup(candidates.map(item => item.id))} backup(s).`);
336
+ await run("Delete backups", async sync => `Deleted ${await sync.applyCleanup(candidates.map(item => item.id))} backup(s).`);
283
337
  }
284
- } else if (selected === "Delete Selected Backup") {
338
+ } else if (selected === "Delete backup") {
285
339
  const backups = await service.listBackups();
286
340
  if (!backups.length) { context.ui.notify("No backups.", "info"); continue; }
287
- const id = await select(context, "Select complete backup to delete", [...backups.map(item => item.id), "Back"]);
341
+ const id = await select(context, "Choose backup to delete", [...backups.map(item => item.id), "Back"]);
288
342
  if (!id || id === "Back") continue;
289
343
  if (await confirm(context, "Delete backup?", `Permanently delete local backup ${id}?`)) {
290
- await run("cleaning backups", async sync => `Deleted ${await sync.applyCleanup([id])} backup.`);
344
+ await run("Delete backups", async sync => `Deleted ${await sync.applyCleanup([id])} backup.`);
291
345
  }
292
346
  } else if (selected === "Retention") {
293
347
  const keep = await context.ui.input("Keep newest complete backups (0 or more)", String(config.retention?.keepBackups ?? 20));
@@ -303,10 +357,10 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
303
357
  context.ui.notify("Retention saved. Cleanup remains manual; no files deleted.", "info");
304
358
  }
305
359
  } else if (choice === "Diagnostics") {
306
- const selected = await select(context, "Diagnostics", ["Doctor", "Diff / Check", "Log", "Back"]);
307
- if (selected === "Doctor") await run("diagnosing", async sync => JSON.stringify(await sync.doctor(), null, 2));
308
- if (selected === "Diff / Check") await run("checking", async sync => { await sync.check(); return detailLines().join("\n"); });
309
- if (selected === "Log") await run("reading log", sync => sync.log());
360
+ const selected = await select(context, "Diagnostics", ["Doctor", "Check status", "View log", "Back"]);
361
+ if (selected === "Doctor") await run("Doctor", async sync => JSON.stringify(await sync.doctor(), null, 2));
362
+ if (selected === "Check status") await run("Check", async sync => { await sync.check(); return detailLines().join("\n"); });
363
+ if (selected === "View log") await run("View log", sync => sync.log());
310
364
  }
311
365
  }
312
366
  }
@@ -326,7 +380,7 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
326
380
  context.ui.setWidget(STATUS_KEY, undefined);
327
381
  publish();
328
382
  unregister?.();
329
- const api = pillAPI();
383
+ const api = bubbleAPI(context);
330
384
  unregister = api?.version === 1 ? api.register(STATUS_KEY, ({ ctx: current }) => showStatus(current)) : undefined;
331
385
  await startTimers();
332
386
  });
@@ -351,17 +405,17 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
351
405
  handler: async (_args, context) => { await settings(context).catch(report); },
352
406
  });
353
407
  pi.registerCommand("sync-push", {
354
- description: "Upload the managed raw data snapshot",
355
- handler: async () => { await run("uploading", sync => sync.push()); },
408
+ description: "Push the managed raw data snapshot",
409
+ handler: async () => { await run("Push", sync => sync.push()); },
356
410
  });
357
411
  pi.registerCommand("sync-pull", {
358
412
  description: "Pull raw data, backing up files before replacement",
359
- handler: async () => { await run("downloading", sync => sync.pull()); },
413
+ handler: async () => { await run("Pull", sync => sync.pull()); },
360
414
  });
361
415
  pi.registerCommand("sync-setup", {
362
416
  description: "Configure a private SSH repository: /sync-setup <ssh-url> [pull-minutes]",
363
417
  handler: async (args, context) => {
364
- if (busy) { context.ui.notify(`Sync is already ${busy}.`, "warning"); return; }
418
+ if (busy) { context.ui.notify(`Sync is busy: ${busy}.`, "warning"); return; }
365
419
  try {
366
420
  const [url, interval, ...extra] = args.trim().split(/\s+/);
367
421
  if (!url || extra.length) throw new Error("Usage: /sync-setup <private-ssh-url> [pull-minutes]");
@@ -375,7 +429,7 @@ export default function amoxPiSync(pi: ExtensionAPI): void {
375
429
  next.paths = [...config.paths];
376
430
  next.mode = config.mode;
377
431
  }
378
- if (busy) throw new Error(`Sync is already ${busy}; retry setup after it finishes.`);
432
+ if (busy) throw new Error(`Sync is busy: ${busy}; retry setup after it finishes.`);
379
433
  stopTimers();
380
434
  await saveConfig(next, paths);
381
435
  config = next;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gfgghgftgffg/amox-pi-sync",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",