cicy-desktop 2.1.55 → 2.1.56

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.55",
3
+ "version": "2.1.56",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -26,7 +26,12 @@ try { __t = require("../i18n").t; } catch { __t = null; }
26
26
  const unnamedName = () => { try { return (__t && __t("localTeams.unnamed")) || "Unnamed"; } catch { return "Unnamed"; } };
27
27
  const log = require("electron-log");
28
28
 
29
+ // global.json is now only read for the local sidecar's api_token (auto-fill).
30
+ // The TEAM LIST lives in its own file: ~/cicy-ai/db/teams.json — decoupled from
31
+ // global.json (which cicy-code + the helper self-register into, which used to
32
+ // leak "Unnamed" ghosts into the team list). Shape: a flat { "<id>": {node} } map.
29
33
  const GLOBAL_JSON = path.join(os.homedir(), "cicy-ai", "global.json");
34
+ const TEAMS_JSON = path.join(os.homedir(), "cicy-ai", "db", "teams.json");
30
35
  const HEALTH_TIMEOUT_MS = 1500;
31
36
  const CACHE_MS = 4000; // small dedupe so rapid renderer polls don't fan-out
32
37
 
@@ -44,6 +49,50 @@ function readGlobal() {
44
49
  }
45
50
  }
46
51
 
52
+ // The team map, from teams.json. One-time migration: if teams.json is absent
53
+ // but the legacy global.json.cicyDesktopNodes still has teams, seed teams.json
54
+ // from it (global.json is left untouched). Returns a { "<id>": {node} } map.
55
+ function readNodes() {
56
+ try {
57
+ const raw = fs.readFileSync(TEAMS_JSON, "utf8");
58
+ const parsed = JSON.parse(raw);
59
+ if (parsed && typeof parsed === "object") return parsed;
60
+ } catch (e) {
61
+ if (e && e.code !== "ENOENT") log.info(`[local-teams] teams.json read failed: ${e.message}`);
62
+ }
63
+ const legacy = (readGlobal()?.cicyDesktopNodes) || {};
64
+ if (Object.keys(legacy).length) {
65
+ try {
66
+ fs.mkdirSync(path.dirname(TEAMS_JSON), { recursive: true });
67
+ fs.writeFileSync(TEAMS_JSON, JSON.stringify(legacy, null, 2), { mode: 0o600 });
68
+ log.info(`[local-teams] migrated ${Object.keys(legacy).length} team(s) global.json → teams.json`);
69
+ } catch (e) { log.info(`[local-teams] teams.json migrate failed: ${e.message}`); }
70
+ return legacy;
71
+ }
72
+ return {};
73
+ }
74
+
75
+ // Atomic read-modify-write of teams.json. The updater gets the node map and
76
+ // returns the next map. Seeds from legacy global.json on first write too.
77
+ async function writeNodes(updater) {
78
+ let nodes = {};
79
+ try {
80
+ const raw = await fs.promises.readFile(TEAMS_JSON, "utf8");
81
+ nodes = JSON.parse(raw);
82
+ if (!nodes || typeof nodes !== "object") nodes = {};
83
+ } catch (e) {
84
+ if (e.code !== "ENOENT") throw e;
85
+ nodes = (readGlobal()?.cicyDesktopNodes) || {}; // seed from legacy
86
+ }
87
+ const next = updater(nodes) || nodes;
88
+ const tmp = `${TEAMS_JSON}.tmp.${process.pid}.${Date.now()}`;
89
+ await fs.promises.mkdir(path.dirname(TEAMS_JSON), { recursive: true });
90
+ await fs.promises.writeFile(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
91
+ await fs.promises.rename(tmp, TEAMS_JSON);
92
+ _cacheUntil = 0; // invalidate list() cache
93
+ return next;
94
+ }
95
+
47
96
  function probeHealth(baseUrl, token) {
48
97
  return new Promise((resolve) => {
49
98
  let parsed;
@@ -93,8 +142,7 @@ function classify(health) {
93
142
 
94
143
  async function list({ refresh = false } = {}) {
95
144
  if (!refresh && _cache && Date.now() < _cacheUntil) return _cache;
96
- const g = readGlobal();
97
- const nodes = (g && g.cicyDesktopNodes) || {};
145
+ const nodes = readNodes();
98
146
  const slugs = Object.keys(nodes);
99
147
  const teams = await Promise.all(slugs.map(async (slug) => {
100
148
  const node = nodes[slug] || {};
@@ -133,8 +181,7 @@ async function list({ refresh = false } = {}) {
133
181
  // the SPA of every desktop tool, which was the regression in the
134
182
  // previous implementation.
135
183
  function openTeam(id) {
136
- const g = readGlobal();
137
- const node = g?.cicyDesktopNodes?.[id];
184
+ const node = readNodes()[id];
138
185
  if (!node) return { ok: false, error: "team not found" };
139
186
  const baseUrl = (node.base_url || "").replace(/\/$/, "");
140
187
  if (!baseUrl) return { ok: false, error: "no base_url" };
@@ -179,8 +226,7 @@ function stripVolatile(u) {
179
226
  // 刷新 action). Matches the window the same way openTeam reuses one — by
180
227
  // origin+pathname. No-op-with-error if no window is open for the team.
181
228
  function reloadTeam(id) {
182
- const g = readGlobal();
183
- const node = g?.cicyDesktopNodes?.[id];
229
+ const node = readNodes()[id];
184
230
  if (!node) return { ok: false, error: "team not found" };
185
231
  const baseUrl = (node.base_url || "").replace(/\/$/, "");
186
232
  if (!baseUrl) return { ok: false, error: "no base_url" };
@@ -222,23 +268,6 @@ function slugifyId(input) {
222
268
  .slice(0, 40);
223
269
  }
224
270
 
225
- async function writeGlobal(updater) {
226
- let parsed = {};
227
- try {
228
- const raw = await fsp.readFile(GLOBAL_JSON, "utf8");
229
- parsed = JSON.parse(raw);
230
- if (!parsed || typeof parsed !== "object") parsed = {};
231
- } catch (e) {
232
- if (e.code !== "ENOENT") throw e;
233
- }
234
- const next = updater(parsed);
235
- const tmp = `${GLOBAL_JSON}.tmp.${process.pid}.${Date.now()}`;
236
- await fsp.mkdir(path.dirname(GLOBAL_JSON), { recursive: true });
237
- await fsp.writeFile(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
238
- await fsp.rename(tmp, GLOBAL_JSON);
239
- _cacheUntil = 0; // invalidate list() cache
240
- return next;
241
- }
242
271
 
243
272
  // Dedupe key for a team: host:port only. The same cicy-code node is the same
244
273
  // node across platforms/protocols, so protocol, path and token never affect
@@ -286,8 +315,7 @@ async function addTeam(spec) {
286
315
  // This is what the user sees in the helper flow — "rerun the installer
287
316
  // on the same port" should rotate the token in place, not pile on a
288
317
  // second card.
289
- const g = readGlobal();
290
- const existing = g?.cicyDesktopNodes || {};
318
+ const existing = readNodes();
291
319
  let existingId = null;
292
320
  for (const [k, v] of Object.entries(existing)) {
293
321
  if (normaliseUrl(v?.base_url || "") === baseUrlKey) { existingId = k; break; }
@@ -318,10 +346,9 @@ async function addTeam(spec) {
318
346
  // Drop undefined keys so we never overwrite existing fields with null.
319
347
  Object.keys(patch).forEach(k => patch[k] === undefined && delete patch[k]);
320
348
 
321
- await writeGlobal((gNext) => {
322
- if (!gNext.cicyDesktopNodes || typeof gNext.cicyDesktopNodes !== "object") gNext.cicyDesktopNodes = {};
323
- const prev = gNext.cicyDesktopNodes[id] || {};
324
- gNext.cicyDesktopNodes[id] = {
349
+ await writeNodes((nodes) => {
350
+ const prev = nodes[id] || {};
351
+ nodes[id] = {
325
352
  ...prev,
326
353
  ...patch,
327
354
  // Upsert by base_url: an EXISTING team keeps its (possibly user-renamed)
@@ -332,10 +359,10 @@ async function addTeam(spec) {
332
359
  added_at: prev.added_at || now,
333
360
  updated_at: now,
334
361
  };
335
- return gNext;
362
+ return nodes;
336
363
  });
337
364
  log.info(`[local-teams] ${existingId ? "upsert" : "add"} ${id} → ${baseUrl} (source=${patch.install_source || "n/a"})`);
338
- const next = (readGlobal()?.cicyDesktopNodes || {})[id] || {};
365
+ const next = readNodes()[id] || {};
339
366
  return { ok: true, id, upserted: !!existingId, team: { id, ...next, port } };
340
367
  }
341
368
 
@@ -370,9 +397,8 @@ async function updateTeam(id, patch) {
370
397
 
371
398
  // If base_url is changing, enforce dedupe — refuse to merge two teams.
372
399
  if (filtered.base_url) {
373
- const g = readGlobal();
374
400
  const nextKey = normaliseUrl(filtered.base_url);
375
- for (const [k, v] of Object.entries(g?.cicyDesktopNodes || {})) {
401
+ for (const [k, v] of Object.entries(readNodes())) {
376
402
  if (k === id) continue;
377
403
  if (normaliseUrl(v?.base_url || "") === nextKey) {
378
404
  return { ok: false, error: `another team (id=${k}) already uses that base_url` };
@@ -381,20 +407,16 @@ async function updateTeam(id, patch) {
381
407
  }
382
408
 
383
409
  let existed = false;
384
- await writeGlobal((g) => {
385
- if (g.cicyDesktopNodes && Object.prototype.hasOwnProperty.call(g.cicyDesktopNodes, id)) {
410
+ await writeNodes((nodes) => {
411
+ if (Object.prototype.hasOwnProperty.call(nodes, id)) {
386
412
  existed = true;
387
- g.cicyDesktopNodes[id] = {
388
- ...g.cicyDesktopNodes[id],
389
- ...filtered,
390
- updated_at: new Date().toISOString(),
391
- };
413
+ nodes[id] = { ...nodes[id], ...filtered, updated_at: new Date().toISOString() };
392
414
  }
393
- return g;
415
+ return nodes;
394
416
  });
395
417
  if (!existed) return { ok: false, error: "team not found" };
396
418
  log.info(`[local-teams] update ${id} → ${Object.keys(filtered).join(",")}`);
397
- const next = (readGlobal()?.cicyDesktopNodes || {})[id] || {};
419
+ const next = readNodes()[id] || {};
398
420
  let port = null;
399
421
  try { port = parseInt(new URL(next.base_url || "").port, 10) || null; } catch {}
400
422
  return { ok: true, id, team: { id, ...next, port } };
@@ -403,12 +425,12 @@ async function updateTeam(id, patch) {
403
425
  async function removeTeam(id) {
404
426
  if (!id) return { ok: false, error: "id required" };
405
427
  let existed = false;
406
- await writeGlobal((g) => {
407
- if (g.cicyDesktopNodes && Object.prototype.hasOwnProperty.call(g.cicyDesktopNodes, id)) {
428
+ await writeNodes((nodes) => {
429
+ if (Object.prototype.hasOwnProperty.call(nodes, id)) {
408
430
  existed = true;
409
- delete g.cicyDesktopNodes[id];
431
+ delete nodes[id];
410
432
  }
411
- return g;
433
+ return nodes;
412
434
  });
413
435
  log.info(`[local-teams] remove ${id} (existed=${existed})`);
414
436
  return { ok: true, removed: existed };
@@ -651,8 +673,7 @@ async function upgradeDocker(node) {
651
673
 
652
674
  async function upgradeTeam(id) {
653
675
  if (!id) return { ok: false, error: "id required" };
654
- const g = readGlobal();
655
- const node = g?.cicyDesktopNodes?.[id];
676
+ const node = readNodes()[id];
656
677
  if (!node) return { ok: false, error: "team not found" };
657
678
  const src = String(node.install_source || "").toLowerCase();
658
679
  const isDocker = src.includes("docker") || (!!node.container_name && !node.install_path);