@peers-app/peers-sdk 0.23.8 → 0.23.9

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.
@@ -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.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"