@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.
- package/dist/context/user-context-singleton.js +19 -4
- package/dist/data/persistent-vars.d.ts +8 -0
- package/dist/data/persistent-vars.js +56 -11
- package/dist/data/tool-access-approvals.d.ts +2 -2
- package/dist/package-installer/package-creator.d.ts +16 -4
- package/dist/package-installer/package-creator.js +67 -10
- package/dist/package-installer/package-creator.test.js +83 -7
- package/dist/package-installer/types.d.ts +33 -0
- package/dist/rpc-types.d.ts +2 -1
- package/package.json +1 -1
|
@@ -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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
12
|
-
*
|
|
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<
|
|
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
|
|
38
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
|
213
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
475
|
-
expect(result).toBe("
|
|
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
|
}
|
package/dist/rpc-types.d.ts
CHANGED
|
@@ -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>;
|