@peers-app/peers-sdk 0.23.8 → 0.23.10

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.
@@ -135,12 +135,27 @@ function getAllTables() {
135
135
  return tableMap;
136
136
  }
137
137
  if (!rpc_types_1.isClient) {
138
+ /**
139
+ * Resolve a table for a renderer RPC call.
140
+ *
141
+ * Prefers a full (code/package-registered) instance. When no package on this
142
+ * device has registered `tableName`, falls back to a synced
143
+ * `TableDefinitions` record so a renderer whose UI bundle is ahead of the
144
+ * package the host loaded can still reach the table's data.
145
+ */
138
146
  async function getTable(dataContextId, tableName) {
139
147
  const userContext = await getUserContext();
140
- const table = userContext
141
- .getDataContext(dataContextId || userContext.userId)
142
- .tableContainer.getTable(tableName);
143
- return table;
148
+ const tableContainer = userContext.getDataContext(dataContextId || userContext.userId).tableContainer;
149
+ try {
150
+ return tableContainer.getTable(tableName);
151
+ }
152
+ catch (err) {
153
+ const fallback = await tableContainer.getFallbackTable(tableName);
154
+ if (fallback) {
155
+ return fallback;
156
+ }
157
+ throw err;
158
+ }
144
159
  }
145
160
  // TODO: add check that client has permission to make this call
146
161
  rpc_types_1.rpcServerCalls.tableMethodCall = async (dataContextId, tableName, methodName, ...args) => {
@@ -123,12 +123,20 @@ export declare function groupVar<T = string>(name: string, opts?: Omit<IPersiste
123
123
  /**
124
124
  * A persistent variable that is stored only on this device, in the user's personal db.
125
125
  * It can have a different value for each group on the device but is not synced to other devices.
126
+ *
127
+ * The record always lives in the personal db. `opts.dataContext` (or, when omitted, the
128
+ * current default group) only selects which group's value is addressed by suffixing the
129
+ * stored name with that data context id, so server-side callers that pin a group and
130
+ * renderer callers that follow the default group share one row.
126
131
  */
127
132
  export declare function groupDeviceVar<T = string>(name: string, opts: Omit<IPersistentVarOptionsWithDefault<T>, "scope">): PersistentVar<T>;
128
133
  export declare function groupDeviceVar<T = string>(name: string, opts?: Omit<IPersistentVarOptionsWithoutDefault<T>, "scope">): PersistentVar<T | undefined>;
129
134
  /**
130
135
  * A persistent variable that is stored in the user's personal db and synced across all their devices in the group.
131
136
  * It can have a different value for each user in the group and a different value for each group the user is in.
137
+ *
138
+ * As with {@link groupDeviceVar}, the record always lives in the personal db; `opts.dataContext`
139
+ * only selects the group whose value is addressed.
132
140
  */
133
141
  export declare function groupUserVar<T = string>(name: string, opts: Omit<IPersistentVarOptionsWithDefault<T>, "scope">): PersistentVar<T>;
134
142
  export declare function groupUserVar<T = string>(name: string, opts?: Omit<IPersistentVarOptionsWithoutDefault<T>, "scope">): PersistentVar<T | undefined>;
@@ -277,26 +277,71 @@ function persistentVarFactory(name, opts) {
277
277
  void (async () => {
278
278
  try {
279
279
  const userContext = opts?.userContext || (await (0, user_context_singleton_1.getUserContext)());
280
- function getDataContext() {
281
- if (opts?.dataContext) {
282
- return opts.dataContext;
283
- }
280
+ /**
281
+ * The data context whose `PersistentVars` table stores this variable.
282
+ *
283
+ * Only `group` scope lives in a group DB. Every other scope lives in the
284
+ * user's personal DB; for `groupDevice` / `groupUser` the route context
285
+ * (`opts.dataContext` or the current default group) only disambiguates
286
+ * the stored name (see {@link getVarNameInDb}). Storing the record in
287
+ * the personal DB regardless of whether a caller pinned a `dataContext`
288
+ * keeps server-side callers (which pin) and renderer callers (which
289
+ * don't) reading and writing the same row.
290
+ */
291
+ function getStorageDataContext() {
284
292
  if (scope === "group") {
285
- return userContext.defaultDataContext();
293
+ return opts?.dataContext ?? userContext.defaultDataContext();
294
+ }
295
+ return userContext.userDataContext;
296
+ }
297
+ /**
298
+ * Before storage was unified in the personal DB, a `groupDevice` /
299
+ * `groupUser` var created with an explicit group `dataContext` was
300
+ * written to that group's DB. Return that legacy location so an
301
+ * existing value can be migrated forward, or `undefined` when there is
302
+ * no distinct legacy location for this variable.
303
+ */
304
+ function getLegacyStorageDataContext() {
305
+ if (scope !== "groupDevice" && scope !== "groupUser") {
306
+ return undefined;
286
307
  }
287
- else {
288
- return userContext.userDataContext;
308
+ const pinned = opts?.dataContext;
309
+ if (!pinned || pinned.dataContextId === userContext.userDataContext.dataContextId) {
310
+ return undefined;
289
311
  }
312
+ return pinned;
290
313
  }
291
314
  function getVarNameInDb() {
292
315
  const dc = opts?.dataContext || userContext.defaultDataContext();
293
316
  return (0, persistent_var_identity_1.persistentVarStorageName)(name, scope, dc?.dataContextId);
294
317
  }
318
+ /**
319
+ * Copy a record left in a group DB by the pre-unification storage rule
320
+ * into the personal DB. Secrets are skipped because their ciphertext is
321
+ * bound to the group key and cannot be moved verbatim. The legacy row is
322
+ * left in place: older builds on other devices may still read it, and
323
+ * the personal copy is authoritative from here on.
324
+ */
325
+ async function migrateLegacyRec(table, name) {
326
+ const legacyContext = getLegacyStorageDataContext();
327
+ if (!legacyContext) {
328
+ return undefined;
329
+ }
330
+ const legacyRec = await PersistentVars(legacyContext).findOne({ name });
331
+ if (!legacyRec || legacyRec.scope !== scope || legacyRec.isSecret) {
332
+ return undefined;
333
+ }
334
+ const { persistentVarId: _legacyId, ...rest } = legacyRec;
335
+ return table.save({ ...rest, persistentVarId: "" }, { weakInsert: true });
336
+ }
295
337
  async function loadRecFromDb() {
296
- const dc = getDataContext();
338
+ const dc = getStorageDataContext();
297
339
  const table = PersistentVars(dc);
298
340
  const name = getVarNameInDb();
299
341
  let dbRec = await table.findOne({ name });
342
+ if (!dbRec) {
343
+ dbRec = await migrateLegacyRec(table, name);
344
+ }
300
345
  if (dbRec) {
301
346
  if (dbRec.scope !== scope) {
302
347
  console.warn(`${name}: deleting old persistent var record because scopes don't match. If this is unexpected there could be two different persistent variables using the same name`);
@@ -340,7 +385,7 @@ function persistentVarFactory(name, opts) {
340
385
  return;
341
386
  }
342
387
  rec.value.value = value;
343
- const dc = getDataContext();
388
+ const dc = getStorageDataContext();
344
389
  const table = PersistentVars(dc);
345
390
  try {
346
391
  rec = await table.save(rec, { weakInsert: true });
@@ -362,7 +407,7 @@ function persistentVarFactory(name, opts) {
362
407
  }
363
408
  }
364
409
  deleteVarImpl = async () => {
365
- const dc = getDataContext();
410
+ const dc = getStorageDataContext();
366
411
  const table = PersistentVars(dc);
367
412
  if (rec?.persistentVarId) {
368
413
  await table.delete(rec);
@@ -374,7 +419,7 @@ function persistentVarFactory(name, opts) {
374
419
  };
375
420
  // subscribe to db changes
376
421
  userContext.subscribeToDataChangedAcrossAllGroups(exports.persistentVarsMetaData.name, async (evt) => {
377
- const dc = getDataContext();
422
+ const dc = getStorageDataContext();
378
423
  if (dc && evt.dataContext && evt.dataContext.dataContextId !== dc.dataContextId) {
379
424
  return;
380
425
  }
@@ -50,8 +50,8 @@ export declare const toolAccessApprovalSchema: z.ZodObject<{
50
50
  userId: string;
51
51
  expiresAt: Date;
52
52
  toolId: string;
53
- createdAt: Date;
54
53
  dataContextId: string;
54
+ createdAt: Date;
55
55
  toolAccessApprovalId: string;
56
56
  toolName: string;
57
57
  argsHash: string;
@@ -106,8 +106,8 @@ export declare function ToolAccessApprovals(dataContext?: DataContext): import("
106
106
  userId: string;
107
107
  expiresAt: Date;
108
108
  toolId: string;
109
- createdAt: Date;
110
109
  dataContextId: string;
110
+ createdAt: Date;
111
111
  toolAccessApprovalId: string;
112
112
  toolName: string;
113
113
  argsHash: string;
@@ -1,17 +1,29 @@
1
1
  import type { DataContext } from "../context/data-context";
2
- import type { ICreatePackageOpts, ICreatePackageResult, IPackageInstallerDeps } from "./types";
2
+ import type { IAddOrUpdatePackageResult, ICreatePackageOpts, ICreatePackageResult, IPackageInstallerDeps, ITemplateCacheResult, TemplateCacheRefreshFailure } from "./types";
3
+ /**
4
+ * Normalize `addOrUpdatePackage` RPC results.
5
+ * Older hosts return a bare packageId string; current hosts return
6
+ * {@link IAddOrUpdatePackageResult}.
7
+ */
8
+ export declare function unwrapAddOrUpdatePackageResult(result: string | IAddOrUpdatePackageResult): IAddOrUpdatePackageResult;
3
9
  /**
4
10
  * Convert a human-readable package name to a filesystem-safe folder slug.
5
11
  * "My Widget" → "my-widget", "camelCase Thing" → "camel-case-thing"
6
12
  */
7
13
  export declare function nameToFolderSlug(name: string): string;
14
+ /**
15
+ * Classify a failed `git pull` of the template cache as a network outage versus
16
+ * a local/git problem (dirty tree, auth, merge conflict).
17
+ */
18
+ export declare function classifyTemplatePullFailure(error: unknown): TemplateCacheRefreshFailure;
8
19
  /**
9
20
  * Ensure the package template is available locally.
10
21
  * On first call, clones the template repo into a dot-prefixed cache directory.
11
- * On subsequent calls, attempts a `git pull` to refresh (continues silently if offline).
12
- * @returns The path to the cached template directory.
22
+ * On later calls, attempts `git pull`. A failed pull does not block create;
23
+ * the existing cache is used and a warning is returned so the host can show it.
24
+ * @returns Cache path and an optional stale-template warning.
13
25
  */
14
- export declare function ensureTemplateCache(deps: IPackageInstallerDeps, templateRepo: string): Promise<string>;
26
+ export declare function ensureTemplateCache(deps: IPackageInstallerDeps, templateRepo: string): Promise<ITemplateCacheResult>;
15
27
  /**
16
28
  * Resolve (or create) the Ed25519 publish public key for a package.
17
29
  *
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.unwrapAddOrUpdatePackageResult = unwrapAddOrUpdatePackageResult;
3
4
  exports.nameToFolderSlug = nameToFolderSlug;
5
+ exports.classifyTemplatePullFailure = classifyTemplatePullFailure;
4
6
  exports.ensureTemplateCache = ensureTemplateCache;
5
7
  exports.ensurePackageSigningKey = ensurePackageSigningKey;
6
8
  exports.importExistingPackage = importExistingPackage;
@@ -22,6 +24,17 @@ function patchTemplateTokens(contents, values) {
22
24
  .replaceAll("<app-screen-id>", values.appScreenId)
23
25
  .replaceAll("<contract-id>", values.contractId);
24
26
  }
27
+ /**
28
+ * Normalize `addOrUpdatePackage` RPC results.
29
+ * Older hosts return a bare packageId string; current hosts return
30
+ * {@link IAddOrUpdatePackageResult}.
31
+ */
32
+ function unwrapAddOrUpdatePackageResult(result) {
33
+ if (typeof result === "string") {
34
+ return { packageId: result };
35
+ }
36
+ return result;
37
+ }
25
38
  /**
26
39
  * Convert a human-readable package name to a filesystem-safe folder slug.
27
40
  * "My Widget" → "my-widget", "camelCase Thing" → "camel-case-thing"
@@ -31,26 +44,67 @@ function nameToFolderSlug(name) {
31
44
  slug = slug.replace(/\s+/g, " ").trim();
32
45
  return (0, utils_1.camelCaseToHyphens)(slug);
33
46
  }
47
+ const OFFLINE_PULL_PATTERN = /unable to access|could not resolve host|failed to connect|connection (refused|timed out|reset)|network is unreachable|enotfound|etimedout|eai_again|offline|ssl connect error|could not fetch/i;
48
+ /**
49
+ * Classify a failed `git pull` of the template cache as a network outage versus
50
+ * a local/git problem (dirty tree, auth, merge conflict).
51
+ */
52
+ function classifyTemplatePullFailure(error) {
53
+ const text = execErrorText(error);
54
+ return OFFLINE_PULL_PATTERN.test(text) ? "offline" : "could-not-refresh";
55
+ }
56
+ function execErrorText(error) {
57
+ if (!(error instanceof Error)) {
58
+ return String(error);
59
+ }
60
+ const extra = "stderr" in error && typeof error.stderr === "string" ? error.stderr : "";
61
+ return [error.message, extra].filter(Boolean).join("\n");
62
+ }
63
+ function firstUsefulErrorLine(text) {
64
+ const line = text
65
+ .split(/\r?\n/)
66
+ .map((entry) => entry.trim())
67
+ .find((entry) => entry.length > 0);
68
+ if (!line) {
69
+ return "";
70
+ }
71
+ return line.length > 240 ? `${line.slice(0, 237)}...` : line;
72
+ }
73
+ function templateCacheWarning(failure, error) {
74
+ const detail = firstUsefulErrorLine(execErrorText(error));
75
+ const detailSuffix = detail ? ` ${detail}` : "";
76
+ if (failure === "offline") {
77
+ return ("This package was created from a cached template because the template repository could not be reached (offline or network error). It may not match the latest isolate package scaffold." +
78
+ detailSuffix);
79
+ }
80
+ return ("This package was created from a cached template because git pull could not update ~/peers/packages/.peers-package-template. It may be a stale version. Local changes in that cache directory are the usual cause." +
81
+ detailSuffix);
82
+ }
34
83
  /**
35
84
  * Ensure the package template is available locally.
36
85
  * On first call, clones the template repo into a dot-prefixed cache directory.
37
- * On subsequent calls, attempts a `git pull` to refresh (continues silently if offline).
38
- * @returns The path to the cached template directory.
86
+ * On later calls, attempts `git pull`. A failed pull does not block create;
87
+ * the existing cache is used and a warning is returned so the host can show it.
88
+ * @returns Cache path and an optional stale-template warning.
39
89
  */
40
90
  async function ensureTemplateCache(deps, templateRepo) {
41
91
  const cachePath = deps.resolvePath(deps.packagesRootDir, TEMPLATE_CACHE_DIR);
42
92
  if (await deps.fs.exists(cachePath)) {
43
93
  try {
44
94
  await deps.shell.exec("git pull", { cwd: cachePath });
95
+ return { cachePath };
45
96
  }
46
- catch {
47
- // Offline or remote unreachable — continue with stale cache
97
+ catch (error) {
98
+ const refreshFailure = classifyTemplatePullFailure(error);
99
+ return {
100
+ cachePath,
101
+ refreshFailure,
102
+ warning: templateCacheWarning(refreshFailure, error),
103
+ };
48
104
  }
49
105
  }
50
- else {
51
- await deps.shell.exec(`git clone ${templateRepo} ${cachePath}`);
52
- }
53
- return cachePath;
106
+ await deps.shell.exec(`git clone ${templateRepo} ${cachePath}`);
107
+ return { cachePath };
54
108
  }
55
109
  /**
56
110
  * Resolve (or create) the Ed25519 publish public key for a package.
@@ -202,6 +256,7 @@ async function createPackage(dataContext, deps, opts) {
202
256
  const contractId = (0, utils_1.newid)();
203
257
  const appScreenId = (0, utils_1.newid)();
204
258
  const templateRepo = opts.templateRepo ?? DEFAULT_TEMPLATE_REPO;
259
+ let templateWarning;
205
260
  // Get template contents into location
206
261
  if (opts.templateRepo && opts.templateRepo !== DEFAULT_TEMPLATE_REPO) {
207
262
  // Custom template — clone directly (no caching)
@@ -209,8 +264,9 @@ async function createPackage(dataContext, deps, opts) {
209
264
  }
210
265
  else {
211
266
  // Default template — use local cache for speed and offline support
212
- const cachePath = await ensureTemplateCache(deps, templateRepo);
213
- await deps.shell.exec(`cp -r ${cachePath}/. ${location}`);
267
+ const templateCache = await ensureTemplateCache(deps, templateRepo);
268
+ templateWarning = templateCache.warning;
269
+ await deps.shell.exec(`cp -r ${templateCache.cachePath}/. ${location}`);
214
270
  }
215
271
  // Patch package.json
216
272
  const packageJsonPath = deps.resolvePath(location, "package.json");
@@ -273,5 +329,6 @@ async function createPackage(dataContext, deps, opts) {
273
329
  packageId,
274
330
  localPath: location,
275
331
  packageVersion: result.packageVersion,
332
+ templateWarning,
276
333
  };
277
334
  }
@@ -83,6 +83,22 @@ beforeEach(() => {
83
83
  publicBoxKey: "test-box-key",
84
84
  });
85
85
  });
86
+ describe("unwrapAddOrUpdatePackageResult", () => {
87
+ it("accepts a legacy packageId string", () => {
88
+ expect((0, package_creator_1.unwrapAddOrUpdatePackageResult)("00mnkoyo7mizav6tseubjzr3a")).toEqual({
89
+ packageId: "00mnkoyo7mizav6tseubjzr3a",
90
+ });
91
+ });
92
+ it("passes through a structured result including a template warning", () => {
93
+ expect((0, package_creator_1.unwrapAddOrUpdatePackageResult)({
94
+ packageId: "00mnkoyo7mizav6tseubjzr3a",
95
+ warning: "stale template",
96
+ })).toEqual({
97
+ packageId: "00mnkoyo7mizav6tseubjzr3a",
98
+ warning: "stale template",
99
+ });
100
+ });
101
+ });
86
102
  describe("nameToFolderSlug", () => {
87
103
  it("converts a simple name", () => {
88
104
  expect((0, package_creator_1.nameToFolderSlug)("My Widget")).toBe("my-widget");
@@ -201,6 +217,37 @@ describe("createPackage", () => {
201
217
  });
202
218
  expect(result.localPath).toBe("/packages/my-widget");
203
219
  expect(result.packageVersion).toBe(mockPv);
220
+ expect(result.templateWarning).toBeUndefined();
221
+ });
222
+ it("returns a stale-template warning when the cached template cannot be refreshed", async () => {
223
+ const shellExec = jest.fn(async (cmd) => {
224
+ if (cmd === "git pull") {
225
+ throw new Error("error: Your local changes to the following files would be overwritten by merge:\n\t.gitignore");
226
+ }
227
+ return { stdout: "", stderr: "" };
228
+ });
229
+ const deps = makeMockDeps({}, shellExec);
230
+ const dc = mockDataContext();
231
+ deps.fs.exists.mockImplementation(async (path) => {
232
+ if (path === "/packages/desk")
233
+ return false;
234
+ if (path === "/packages/.peers-package-template")
235
+ return true;
236
+ return false;
237
+ });
238
+ deps.fs.readJson.mockResolvedValue({ name: "template", peers: {} });
239
+ const pvarSetter = jest.fn();
240
+ mockGroupDeviceVar.mockReturnValue(pvarSetter);
241
+ mockInstallPackageFromBundles.mockResolvedValue({
242
+ packageVersion: { packageVersionId: "pv" },
243
+ loaded: undefined,
244
+ unchanged: false,
245
+ });
246
+ const result = await (0, package_creator_1.createPackage)(dc, deps, { name: "Desk" });
247
+ expect(result.templateWarning).toMatch(/stale version/i);
248
+ expect(result.templateWarning).toMatch(/local changes/i);
249
+ expect(result.localPath).toBe("/packages/desk");
250
+ expect(deps.shell.exec).toHaveBeenCalledWith("cp -r /packages/.peers-package-template/. /packages/desk");
204
251
  });
205
252
  it("registers metadata, path, and signing key when importing an existing package directory", async () => {
206
253
  const deps = makeMockDeps({});
@@ -447,14 +494,16 @@ describe("ensureTemplateCache", () => {
447
494
  const deps = makeMockDeps({});
448
495
  deps.fs.exists.mockResolvedValue(false);
449
496
  const result = await (0, package_creator_1.ensureTemplateCache)(deps, "https://github.com/peers-app/peers-package-template.git");
450
- expect(result).toBe("/packages/.peers-package-template");
497
+ expect(result.cachePath).toBe("/packages/.peers-package-template");
498
+ expect(result.warning).toBeUndefined();
451
499
  expect(deps.shell.exec).toHaveBeenCalledWith("git clone https://github.com/peers-app/peers-package-template.git /packages/.peers-package-template");
452
500
  });
453
501
  it("pulls latest when cache directory already exists", async () => {
454
502
  const deps = makeMockDeps({});
455
503
  deps.fs.exists.mockResolvedValue(true);
456
504
  const result = await (0, package_creator_1.ensureTemplateCache)(deps, "https://github.com/peers-app/peers-package-template.git");
457
- expect(result).toBe("/packages/.peers-package-template");
505
+ expect(result.cachePath).toBe("/packages/.peers-package-template");
506
+ expect(result.warning).toBeUndefined();
458
507
  expect(deps.shell.exec).toHaveBeenCalledWith("git pull", {
459
508
  cwd: "/packages/.peers-package-template",
460
509
  });
@@ -462,16 +511,43 @@ describe("ensureTemplateCache", () => {
462
511
  const execCalls = deps.shell.exec.mock.calls.map((c) => c[0]);
463
512
  expect(execCalls).not.toContain(expect.stringContaining("git clone"));
464
513
  });
465
- it("continues silently when pull fails (offline)", async () => {
514
+ it("returns an offline warning when pull cannot reach the remote", async () => {
515
+ const shellExec = jest.fn(async (cmd) => {
516
+ if (cmd === "git pull") {
517
+ throw new Error("fatal: unable to access 'https://github.com/peers-app/peers-package-template.git'");
518
+ }
519
+ return { stdout: "", stderr: "" };
520
+ });
521
+ const deps = makeMockDeps({}, shellExec);
522
+ deps.fs.exists.mockResolvedValue(true);
523
+ const result = await (0, package_creator_1.ensureTemplateCache)(deps, "https://github.com/peers-app/peers-package-template.git");
524
+ expect(result.cachePath).toBe("/packages/.peers-package-template");
525
+ expect(result.refreshFailure).toBe("offline");
526
+ expect(result.warning).toMatch(/could not be reached/i);
527
+ });
528
+ it("returns a could-not-refresh warning when pull fails for local git reasons", async () => {
466
529
  const shellExec = jest.fn(async (cmd) => {
467
- if (cmd === "git pull")
468
- throw new Error("fatal: unable to access remote");
530
+ if (cmd === "git pull") {
531
+ throw new Error("error: Your local changes to the following files would be overwritten by merge:\n\t.gitignore");
532
+ }
469
533
  return { stdout: "", stderr: "" };
470
534
  });
471
535
  const deps = makeMockDeps({}, shellExec);
472
536
  deps.fs.exists.mockResolvedValue(true);
473
537
  const result = await (0, package_creator_1.ensureTemplateCache)(deps, "https://github.com/peers-app/peers-package-template.git");
474
- // Should not throw, returns cache path
475
- expect(result).toBe("/packages/.peers-package-template");
538
+ expect(result.cachePath).toBe("/packages/.peers-package-template");
539
+ expect(result.refreshFailure).toBe("could-not-refresh");
540
+ expect(result.warning).toMatch(/stale version/i);
541
+ expect(result.warning).toMatch(/overwritten by merge/i);
542
+ });
543
+ });
544
+ describe("classifyTemplatePullFailure", () => {
545
+ it("treats unreachable remotes as offline", () => {
546
+ expect((0, package_creator_1.classifyTemplatePullFailure)(new Error("fatal: unable to access remote"))).toBe("offline");
547
+ expect((0, package_creator_1.classifyTemplatePullFailure)(new Error("Could not resolve host: github.com"))).toBe("offline");
548
+ });
549
+ it("treats dirty trees and auth problems as could-not-refresh", () => {
550
+ expect((0, package_creator_1.classifyTemplatePullFailure)(new Error("Your local changes to the following files would be overwritten by merge"))).toBe("could-not-refresh");
551
+ expect((0, package_creator_1.classifyTemplatePullFailure)(new Error("Permission denied (publickey)"))).toBe("could-not-refresh");
476
552
  });
477
553
  });
@@ -84,11 +84,40 @@ export interface ICreatePackageOpts {
84
84
  */
85
85
  personalContext?: import("../context/data-context").DataContext;
86
86
  }
87
+ /** Why a `git pull` of the cached package template did not succeed. */
88
+ export type TemplateCacheRefreshFailure = "offline" | "could-not-refresh";
89
+ /** Local template cache path plus a non-fatal refresh warning when present. */
90
+ export interface ITemplateCacheResult {
91
+ /** Directory copied from when scaffolding a new package. */
92
+ cachePath: string;
93
+ /** Set when `git pull` failed and the existing cache was used anyway. */
94
+ refreshFailure?: TemplateCacheRefreshFailure;
95
+ /** User-facing explanation when {@link refreshFailure} is set. */
96
+ warning?: string;
97
+ }
87
98
  /** Result of {@link createPackage}. */
88
99
  export interface ICreatePackageResult {
100
+ /** New or imported package ID. */
89
101
  packageId: string;
102
+ /** On-disk package directory. */
90
103
  localPath: string;
104
+ /** Installed local/dev package version record. */
91
105
  packageVersion: IPackageVersion;
106
+ /**
107
+ * Present when the default template cache could not be refreshed and a
108
+ * possibly stale copy was used. Creation still succeeded.
109
+ */
110
+ templateWarning?: string;
111
+ }
112
+ /** Host/RPC result for adding, creating, or updating a package. */
113
+ export interface IAddOrUpdatePackageResult {
114
+ /** Package that was created, imported, cloned, or updated. */
115
+ packageId: string;
116
+ /**
117
+ * Non-fatal notice after a successful local create (for example a stale
118
+ * template cache). Undefined on update, clone, and import of an existing directory.
119
+ */
120
+ warning?: string;
92
121
  }
93
122
  /** Options for {@link clonePackage}. */
94
123
  export interface IClonePackageOpts {
@@ -103,8 +132,12 @@ export interface IClonePackageOpts {
103
132
  }
104
133
  /** Result of {@link clonePackage}. */
105
134
  export interface IClonePackageResult {
135
+ /** Imported package ID from the cloned repo. */
106
136
  packageId: string;
137
+ /** On-disk clone directory. */
107
138
  localPath: string;
139
+ /** Git URL that was cloned. */
108
140
  remoteRepo: string;
141
+ /** Installed local/dev package version record. */
109
142
  packageVersion: IPackageVersion;
110
143
  }
@@ -2,6 +2,7 @@ import type { IDeviceDatabaseQueryResult, IDeviceOperationStatus, IDeviceToolRun
2
2
  import type { ITool } from "./data/tools";
3
3
  import type { DevicePairingCeremonyAction, DevicePairingStartDestinationInput, DevicePairingStartSourceResult, DevicePairingState } from "./device-pairing";
4
4
  import type { IConsoleLog } from "./logging/console-logs.table";
5
+ import type { IAddOrUpdatePackageResult } from "./package-installer/types";
5
6
  import type { IAppNav } from "./types/app-nav";
6
7
  /** Named event payload sent across the frontend RPC bridge. */
7
8
  export interface IEventData<T = any> {
@@ -61,7 +62,7 @@ export declare const rpcServerCalls: {
61
62
  packageLocation?: string;
62
63
  update?: boolean;
63
64
  linkRemote?: boolean;
64
- }) => Promise<string>;
65
+ }) => Promise<string | IAddOrUpdatePackageResult>;
65
66
  seedBundledPeersCore: (dataContextId?: string) => Promise<boolean>;
66
67
  setUserIdAndSecretKey: (userId: string, secretKey: string) => Promise<void>;
67
68
  getUserId: () => Promise<string | undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.23.8",
3
+ "version": "0.23.10",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"