@peers-app/peers-sdk 0.18.8 → 0.19.6
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/README.md +74 -1
- package/dist/data/files/file-read-stream.js +7 -0
- package/dist/data/files/file.types.d.ts +6 -0
- package/dist/data/files/file.types.js +18 -0
- package/dist/data/files/files.test.js +50 -7
- package/dist/data/package-version-resolver.d.ts +13 -5
- package/dist/data/package-version-resolver.js +64 -6
- package/dist/data/package-version-resolver.test.d.ts +0 -4
- package/dist/data/package-version-resolver.test.js +127 -5
- package/dist/data/package-versions.d.ts +3 -0
- package/dist/data/package-versions.js +5 -0
- package/dist/data/packages.d.ts +6 -29
- package/dist/data/packages.js +8 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/package-installer/index.d.ts +10 -0
- package/dist/package-installer/index.js +26 -0
- package/dist/package-installer/package-author-signing.d.ts +54 -0
- package/dist/package-installer/package-author-signing.js +82 -0
- package/dist/package-installer/package-author-signing.test.d.ts +1 -0
- package/dist/package-installer/package-author-signing.test.js +189 -0
- package/dist/package-installer/package-cloner.d.ts +16 -0
- package/dist/package-installer/package-cloner.js +115 -0
- package/dist/package-installer/package-cloner.test.d.ts +1 -0
- package/dist/package-installer/package-cloner.test.js +276 -0
- package/dist/package-installer/package-creator.d.ts +22 -0
- package/dist/package-installer/package-creator.js +154 -0
- package/dist/package-installer/package-creator.test.d.ts +1 -0
- package/dist/package-installer/package-creator.test.js +354 -0
- package/dist/package-installer/package-installer.d.ts +32 -0
- package/dist/package-installer/package-installer.js +247 -0
- package/dist/package-installer/package-installer.test.d.ts +1 -0
- package/dist/package-installer/package-installer.test.js +666 -0
- package/dist/package-installer/package-propagation.d.ts +29 -0
- package/dist/package-installer/package-propagation.js +364 -0
- package/dist/package-installer/package-propagation.test.d.ts +1 -0
- package/dist/package-installer/package-propagation.test.js +1145 -0
- package/dist/package-installer/package-publisher.d.ts +55 -0
- package/dist/package-installer/package-publisher.js +71 -0
- package/dist/package-installer/package-publisher.test.d.ts +1 -0
- package/dist/package-installer/package-publisher.test.js +142 -0
- package/dist/package-installer/package-remote-checker.d.ts +54 -0
- package/dist/package-installer/package-remote-checker.js +194 -0
- package/dist/package-installer/package-remote-checker.test.d.ts +1 -0
- package/dist/package-installer/package-remote-checker.test.js +269 -0
- package/dist/package-installer/package-seed-installer.d.ts +45 -0
- package/dist/package-installer/package-seed-installer.js +108 -0
- package/dist/package-installer/package-seed-installer.test.d.ts +1 -0
- package/dist/package-installer/package-seed-installer.test.js +123 -0
- package/dist/package-installer/package-tarball.d.ts +35 -0
- package/dist/package-installer/package-tarball.js +57 -0
- package/dist/package-installer/package-tarball.test.d.ts +1 -0
- package/dist/package-installer/package-tarball.test.js +75 -0
- package/dist/package-installer/types.d.ts +110 -0
- package/dist/package-installer/types.js +2 -0
- package/dist/rpc-types.d.ts +14 -0
- package/dist/rpc-types.js +6 -0
- package/dist/system-ids.d.ts +1 -0
- package/dist/system-ids.js +2 -1
- package/package.json +3 -2
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const package_cloner_1 = require("./package-cloner");
|
|
4
|
+
// --- Mocks for SDK internals ---
|
|
5
|
+
const mockSignAndSavePkg = jest.fn();
|
|
6
|
+
jest.mock("../data/packages", () => ({
|
|
7
|
+
Packages: () => ({
|
|
8
|
+
signAndSave: mockSignAndSavePkg,
|
|
9
|
+
}),
|
|
10
|
+
}));
|
|
11
|
+
const mockGroupDeviceVar = jest.fn();
|
|
12
|
+
jest.mock("../data/persistent-vars", () => ({
|
|
13
|
+
groupDeviceVar: (...args) => mockGroupDeviceVar(...args),
|
|
14
|
+
}));
|
|
15
|
+
const mockInstallPackageFromBundles = jest.fn();
|
|
16
|
+
const mockGetPackageInfo = jest.fn();
|
|
17
|
+
jest.mock("./package-installer", () => ({
|
|
18
|
+
installPackageFromBundles: (...args) => mockInstallPackageFromBundles(...args),
|
|
19
|
+
getPackageInfo: (...args) => mockGetPackageInfo(...args),
|
|
20
|
+
}));
|
|
21
|
+
// --- Test utilities ---
|
|
22
|
+
function makeMockDeps(shellExec) {
|
|
23
|
+
return {
|
|
24
|
+
fs: {
|
|
25
|
+
readFile: jest.fn(async () => ""),
|
|
26
|
+
writeFile: jest.fn(async () => { }),
|
|
27
|
+
exists: jest.fn(async () => false),
|
|
28
|
+
readJson: jest.fn(async () => ({})),
|
|
29
|
+
},
|
|
30
|
+
shell: {
|
|
31
|
+
exec: shellExec ?? jest.fn(async () => ({ stdout: "", stderr: "" })),
|
|
32
|
+
},
|
|
33
|
+
resolvePath: (...segments) => segments.join("/"),
|
|
34
|
+
packagesRootDir: "/packages",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function mockDataContext() {
|
|
38
|
+
return {
|
|
39
|
+
packageLoader: {
|
|
40
|
+
loadPackage: jest.fn().mockResolvedValue(undefined),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// --- Tests ---
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
jest.clearAllMocks();
|
|
47
|
+
});
|
|
48
|
+
describe("deriveRepoSlug", () => {
|
|
49
|
+
it("extracts slug from https URL with .git suffix", () => {
|
|
50
|
+
expect((0, package_cloner_1.deriveRepoSlug)("https://github.com/peers-app/groceries.git")).toBe("groceries");
|
|
51
|
+
});
|
|
52
|
+
it("extracts slug from https URL without .git suffix", () => {
|
|
53
|
+
expect((0, package_cloner_1.deriveRepoSlug)("https://github.com/user/my-pkg")).toBe("my-pkg");
|
|
54
|
+
});
|
|
55
|
+
it("handles SSH-style URLs", () => {
|
|
56
|
+
expect((0, package_cloner_1.deriveRepoSlug)("git@github.com:user/some-repo.git")).toBe("some-repo");
|
|
57
|
+
});
|
|
58
|
+
it("returns empty string for empty URL", () => {
|
|
59
|
+
expect((0, package_cloner_1.deriveRepoSlug)("")).toBe("");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("clonePackage", () => {
|
|
63
|
+
it("throws when git is not installed", async () => {
|
|
64
|
+
const shellExec = jest.fn(async (cmd) => {
|
|
65
|
+
if (cmd === "git --version")
|
|
66
|
+
throw new Error("not found");
|
|
67
|
+
return { stdout: "", stderr: "" };
|
|
68
|
+
});
|
|
69
|
+
const deps = makeMockDeps(shellExec);
|
|
70
|
+
const dc = mockDataContext();
|
|
71
|
+
await expect((0, package_cloner_1.clonePackage)(dc, deps, { remoteRepo: "https://github.com/peers-app/test.git" })).rejects.toThrow("git is not installed");
|
|
72
|
+
});
|
|
73
|
+
it("throws when npm is not installed", async () => {
|
|
74
|
+
const shellExec = jest.fn(async (cmd) => {
|
|
75
|
+
if (cmd === "npm --version")
|
|
76
|
+
throw new Error("not found");
|
|
77
|
+
return { stdout: "", stderr: "" };
|
|
78
|
+
});
|
|
79
|
+
const deps = makeMockDeps(shellExec);
|
|
80
|
+
const dc = mockDataContext();
|
|
81
|
+
await expect((0, package_cloner_1.clonePackage)(dc, deps, { remoteRepo: "https://github.com/peers-app/test.git" })).rejects.toThrow("npm is not installed");
|
|
82
|
+
});
|
|
83
|
+
it("clones, installs, builds, and registers a package with valid packageId", async () => {
|
|
84
|
+
const deps = makeMockDeps();
|
|
85
|
+
const dc = mockDataContext();
|
|
86
|
+
// Directory doesn't exist
|
|
87
|
+
deps.fs.exists.mockResolvedValue(false);
|
|
88
|
+
// After clone, getPackageInfo returns a valid package
|
|
89
|
+
mockGetPackageInfo.mockResolvedValue({
|
|
90
|
+
packageId: "00mnkoyo7mizav6tseubjzr3a",
|
|
91
|
+
name: "groceries",
|
|
92
|
+
description: "Shopping list app",
|
|
93
|
+
});
|
|
94
|
+
const pvarSetter = jest.fn();
|
|
95
|
+
mockGroupDeviceVar.mockReturnValue(pvarSetter);
|
|
96
|
+
const mockPv = { packageVersionId: "pv001" };
|
|
97
|
+
mockInstallPackageFromBundles.mockResolvedValue({
|
|
98
|
+
packageVersion: mockPv,
|
|
99
|
+
loaded: undefined,
|
|
100
|
+
unchanged: false,
|
|
101
|
+
});
|
|
102
|
+
const result = await (0, package_cloner_1.clonePackage)(dc, deps, {
|
|
103
|
+
remoteRepo: "https://github.com/peers-app/groceries.git",
|
|
104
|
+
});
|
|
105
|
+
// Verify clone
|
|
106
|
+
expect(deps.shell.exec).toHaveBeenCalledWith("git clone https://github.com/peers-app/groceries.git /packages/groceries");
|
|
107
|
+
// Verify install + build
|
|
108
|
+
expect(deps.shell.exec).toHaveBeenCalledWith("npm install", { cwd: "/packages/groceries" });
|
|
109
|
+
expect(deps.shell.exec).toHaveBeenCalledWith("npm run build", { cwd: "/packages/groceries" });
|
|
110
|
+
// Verify package record was created (peers-app org = not disabled)
|
|
111
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({
|
|
112
|
+
packageId: "00mnkoyo7mizav6tseubjzr3a",
|
|
113
|
+
name: "groceries",
|
|
114
|
+
description: "Shopping list app",
|
|
115
|
+
disabled: false,
|
|
116
|
+
remoteRepo: "https://github.com/peers-app/groceries.git",
|
|
117
|
+
}), { restoreIfDeleted: true, saveAsSnapshot: true });
|
|
118
|
+
// Verify device var
|
|
119
|
+
expect(pvarSetter).toHaveBeenCalledWith("/packages/groceries");
|
|
120
|
+
// Verify install
|
|
121
|
+
expect(mockInstallPackageFromBundles).toHaveBeenCalledWith(dc, deps, "00mnkoyo7mizav6tseubjzr3a", { localPath: "/packages/groceries" });
|
|
122
|
+
expect(result.packageId).toBe("00mnkoyo7mizav6tseubjzr3a");
|
|
123
|
+
expect(result.localPath).toBe("/packages/groceries");
|
|
124
|
+
expect(result.remoteRepo).toBe("https://github.com/peers-app/groceries.git");
|
|
125
|
+
expect(result.packageVersion).toBe(mockPv);
|
|
126
|
+
});
|
|
127
|
+
it("auto-disables packages from non-peers-app orgs", async () => {
|
|
128
|
+
const deps = makeMockDeps();
|
|
129
|
+
const dc = mockDataContext();
|
|
130
|
+
deps.fs.exists.mockResolvedValue(false);
|
|
131
|
+
mockGetPackageInfo.mockResolvedValue({
|
|
132
|
+
packageId: "00mnkoyo7mizav6tseubjzr3b",
|
|
133
|
+
name: "external-pkg",
|
|
134
|
+
});
|
|
135
|
+
mockGroupDeviceVar.mockReturnValue(jest.fn());
|
|
136
|
+
mockInstallPackageFromBundles.mockResolvedValue({
|
|
137
|
+
packageVersion: { packageVersionId: "pv" },
|
|
138
|
+
loaded: undefined,
|
|
139
|
+
unchanged: false,
|
|
140
|
+
});
|
|
141
|
+
await (0, package_cloner_1.clonePackage)(dc, deps, {
|
|
142
|
+
remoteRepo: "https://github.com/some-user/external-pkg.git",
|
|
143
|
+
});
|
|
144
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({ disabled: true }), expect.any(Object));
|
|
145
|
+
});
|
|
146
|
+
it("respects explicit disabled override", async () => {
|
|
147
|
+
const deps = makeMockDeps();
|
|
148
|
+
const dc = mockDataContext();
|
|
149
|
+
deps.fs.exists.mockResolvedValue(false);
|
|
150
|
+
mockGetPackageInfo.mockResolvedValue({
|
|
151
|
+
packageId: "00mnkoyo7mizav6tseubjzr3c",
|
|
152
|
+
name: "forced-enabled",
|
|
153
|
+
});
|
|
154
|
+
mockGroupDeviceVar.mockReturnValue(jest.fn());
|
|
155
|
+
mockInstallPackageFromBundles.mockResolvedValue({
|
|
156
|
+
packageVersion: { packageVersionId: "pv" },
|
|
157
|
+
loaded: undefined,
|
|
158
|
+
unchanged: false,
|
|
159
|
+
});
|
|
160
|
+
// External repo but explicitly enabled
|
|
161
|
+
await (0, package_cloner_1.clonePackage)(dc, deps, {
|
|
162
|
+
remoteRepo: "https://github.com/external/pkg.git",
|
|
163
|
+
disabled: false,
|
|
164
|
+
});
|
|
165
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({ disabled: false }), expect.any(Object));
|
|
166
|
+
});
|
|
167
|
+
it("skips build when skipBuild is true", async () => {
|
|
168
|
+
const deps = makeMockDeps();
|
|
169
|
+
const dc = mockDataContext();
|
|
170
|
+
deps.fs.exists.mockResolvedValue(false);
|
|
171
|
+
mockGetPackageInfo.mockResolvedValue({
|
|
172
|
+
packageId: "00mnkoyo7mizav6tseubjzr3d",
|
|
173
|
+
name: "prebuilt",
|
|
174
|
+
});
|
|
175
|
+
mockGroupDeviceVar.mockReturnValue(jest.fn());
|
|
176
|
+
mockInstallPackageFromBundles.mockResolvedValue({
|
|
177
|
+
packageVersion: { packageVersionId: "pv" },
|
|
178
|
+
loaded: undefined,
|
|
179
|
+
unchanged: false,
|
|
180
|
+
});
|
|
181
|
+
await (0, package_cloner_1.clonePackage)(dc, deps, {
|
|
182
|
+
remoteRepo: "https://github.com/peers-app/prebuilt.git",
|
|
183
|
+
skipBuild: true,
|
|
184
|
+
});
|
|
185
|
+
const execCalls = deps.shell.exec.mock.calls.map((c) => c[0]);
|
|
186
|
+
expect(execCalls).toContain("git --version");
|
|
187
|
+
expect(execCalls).toContain("npm --version");
|
|
188
|
+
expect(execCalls).toContain("git clone https://github.com/peers-app/prebuilt.git /packages/prebuilt");
|
|
189
|
+
expect(execCalls).not.toContain("npm install");
|
|
190
|
+
expect(execCalls).not.toContain("npm run build");
|
|
191
|
+
});
|
|
192
|
+
it("generates packageId and patches package.json when repo has no valid ID", async () => {
|
|
193
|
+
const deps = makeMockDeps();
|
|
194
|
+
const dc = mockDataContext();
|
|
195
|
+
deps.fs.exists.mockResolvedValue(false);
|
|
196
|
+
// No valid packageId
|
|
197
|
+
mockGetPackageInfo.mockResolvedValue({
|
|
198
|
+
name: "no-id-package",
|
|
199
|
+
description: "A package without peers config",
|
|
200
|
+
});
|
|
201
|
+
// readJson returns a package.json without peers.packageId
|
|
202
|
+
deps.fs.readJson.mockResolvedValue({
|
|
203
|
+
name: "no-id-package",
|
|
204
|
+
version: "1.0.0",
|
|
205
|
+
});
|
|
206
|
+
mockGroupDeviceVar.mockReturnValue(jest.fn());
|
|
207
|
+
mockInstallPackageFromBundles.mockResolvedValue({
|
|
208
|
+
packageVersion: { packageVersionId: "pv" },
|
|
209
|
+
loaded: undefined,
|
|
210
|
+
unchanged: false,
|
|
211
|
+
});
|
|
212
|
+
const result = await (0, package_cloner_1.clonePackage)(dc, deps, {
|
|
213
|
+
remoteRepo: "https://github.com/peers-app/no-id-package.git",
|
|
214
|
+
});
|
|
215
|
+
// Should have written a patched package.json
|
|
216
|
+
expect(deps.fs.writeFile).toHaveBeenCalledWith("/packages/no-id-package/package.json", expect.any(String));
|
|
217
|
+
const patchedJson = JSON.parse(deps.fs.writeFile.mock.calls[0][1]);
|
|
218
|
+
expect(patchedJson.peers.packageId).toMatch(/^[a-z0-9]{25}$/);
|
|
219
|
+
// Result should use the generated ID
|
|
220
|
+
expect(result.packageId).toMatch(/^[a-z0-9]{25}$/);
|
|
221
|
+
});
|
|
222
|
+
it("delegates to installPackageFromBundles when directory already exists with valid package", async () => {
|
|
223
|
+
const deps = makeMockDeps();
|
|
224
|
+
const dc = mockDataContext();
|
|
225
|
+
// Location exists
|
|
226
|
+
deps.fs.exists.mockResolvedValue(true);
|
|
227
|
+
mockGetPackageInfo.mockResolvedValue({
|
|
228
|
+
packageId: "00mnkoyo7mizav6tseubjzr3e",
|
|
229
|
+
name: "existing",
|
|
230
|
+
});
|
|
231
|
+
const mockPv = { packageVersionId: "pvexist" };
|
|
232
|
+
mockInstallPackageFromBundles.mockResolvedValue({
|
|
233
|
+
packageVersion: mockPv,
|
|
234
|
+
loaded: undefined,
|
|
235
|
+
unchanged: true,
|
|
236
|
+
});
|
|
237
|
+
const result = await (0, package_cloner_1.clonePackage)(dc, deps, {
|
|
238
|
+
remoteRepo: "https://github.com/peers-app/existing.git",
|
|
239
|
+
});
|
|
240
|
+
// Should NOT clone or build
|
|
241
|
+
const execCalls = deps.shell.exec.mock.calls.map((c) => c[0]);
|
|
242
|
+
expect(execCalls).not.toContain(expect.stringContaining("git clone"));
|
|
243
|
+
expect(execCalls).not.toContain("npm install");
|
|
244
|
+
// Should delegate
|
|
245
|
+
expect(mockInstallPackageFromBundles).toHaveBeenCalled();
|
|
246
|
+
expect(result.packageId).toBe("00mnkoyo7mizav6tseubjzr3e");
|
|
247
|
+
});
|
|
248
|
+
it("throws when directory exists but has no valid packageId", async () => {
|
|
249
|
+
const deps = makeMockDeps();
|
|
250
|
+
const dc = mockDataContext();
|
|
251
|
+
deps.fs.exists.mockResolvedValue(true);
|
|
252
|
+
mockGetPackageInfo.mockResolvedValue({ name: "invalid-pkg" });
|
|
253
|
+
await expect((0, package_cloner_1.clonePackage)(dc, deps, { remoteRepo: "https://github.com/user/invalid-pkg.git" })).rejects.toThrow("does not contain a valid Peers package");
|
|
254
|
+
});
|
|
255
|
+
it("uses custom location when provided", async () => {
|
|
256
|
+
const deps = makeMockDeps();
|
|
257
|
+
const dc = mockDataContext();
|
|
258
|
+
deps.fs.exists.mockResolvedValue(false);
|
|
259
|
+
mockGetPackageInfo.mockResolvedValue({
|
|
260
|
+
packageId: "00mnkoyo7mizav6tseubjzr3f",
|
|
261
|
+
name: "custom-loc",
|
|
262
|
+
});
|
|
263
|
+
mockGroupDeviceVar.mockReturnValue(jest.fn());
|
|
264
|
+
mockInstallPackageFromBundles.mockResolvedValue({
|
|
265
|
+
packageVersion: { packageVersionId: "pv" },
|
|
266
|
+
loaded: undefined,
|
|
267
|
+
unchanged: false,
|
|
268
|
+
});
|
|
269
|
+
await (0, package_cloner_1.clonePackage)(dc, deps, {
|
|
270
|
+
remoteRepo: "https://github.com/peers-app/test.git",
|
|
271
|
+
location: "/custom/install/path",
|
|
272
|
+
});
|
|
273
|
+
expect(deps.shell.exec).toHaveBeenCalledWith("git clone https://github.com/peers-app/test.git /custom/install/path");
|
|
274
|
+
expect(deps.shell.exec).toHaveBeenCalledWith("npm install", { cwd: "/custom/install/path" });
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DataContext } from "../context/data-context";
|
|
2
|
+
import type { ICreatePackageOpts, ICreatePackageResult, IPackageInstallerDeps } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Convert a human-readable package name to a filesystem-safe folder slug.
|
|
5
|
+
* "My Widget" → "my-widget", "camelCase Thing" → "camel-case-thing"
|
|
6
|
+
*/
|
|
7
|
+
export declare function nameToFolderSlug(name: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Ensure the package template is available locally.
|
|
10
|
+
* 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.
|
|
13
|
+
*/
|
|
14
|
+
export declare function ensureTemplateCache(deps: IPackageInstallerDeps, templateRepo: string): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Create a new Peers package from the package template.
|
|
17
|
+
*
|
|
18
|
+
* Clones the template repo, patches IDs and metadata, runs npm install + build,
|
|
19
|
+
* initializes a fresh git repo, then delegates to {@link installPackageFromBundles}
|
|
20
|
+
* to register the dev bundle in the database.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createPackage(dataContext: DataContext, deps: IPackageInstallerDeps, opts: ICreatePackageOpts): Promise<ICreatePackageResult>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nameToFolderSlug = nameToFolderSlug;
|
|
4
|
+
exports.ensureTemplateCache = ensureTemplateCache;
|
|
5
|
+
exports.createPackage = createPackage;
|
|
6
|
+
const packages_1 = require("../data/packages");
|
|
7
|
+
const persistent_vars_1 = require("../data/persistent-vars");
|
|
8
|
+
const keys_1 = require("../keys");
|
|
9
|
+
const system_ids_1 = require("../system-ids");
|
|
10
|
+
const utils_1 = require("../utils");
|
|
11
|
+
const package_author_signing_1 = require("./package-author-signing");
|
|
12
|
+
const package_installer_1 = require("./package-installer");
|
|
13
|
+
const DEFAULT_TEMPLATE_REPO = "https://github.com/peers-app/peers-package-template.git";
|
|
14
|
+
const TEMPLATE_CACHE_DIR = ".peers-package-template";
|
|
15
|
+
/**
|
|
16
|
+
* Convert a human-readable package name to a filesystem-safe folder slug.
|
|
17
|
+
* "My Widget" → "my-widget", "camelCase Thing" → "camel-case-thing"
|
|
18
|
+
*/
|
|
19
|
+
function nameToFolderSlug(name) {
|
|
20
|
+
let slug = name.replace(/[^a-zA-Z0-9]/g, " ");
|
|
21
|
+
slug = slug.replace(/\s+/g, " ").trim();
|
|
22
|
+
return (0, utils_1.camelCaseToHyphens)(slug);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Ensure the package template is available locally.
|
|
26
|
+
* On first call, clones the template repo into a dot-prefixed cache directory.
|
|
27
|
+
* On subsequent calls, attempts a `git pull` to refresh (continues silently if offline).
|
|
28
|
+
* @returns The path to the cached template directory.
|
|
29
|
+
*/
|
|
30
|
+
async function ensureTemplateCache(deps, templateRepo) {
|
|
31
|
+
const cachePath = deps.resolvePath(deps.packagesRootDir, TEMPLATE_CACHE_DIR);
|
|
32
|
+
if (await deps.fs.exists(cachePath)) {
|
|
33
|
+
try {
|
|
34
|
+
await deps.shell.exec("git pull", { cwd: cachePath });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Offline or remote unreachable — continue with stale cache
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
await deps.shell.exec(`git clone ${templateRepo} ${cachePath}`);
|
|
42
|
+
}
|
|
43
|
+
return cachePath;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create a new Peers package from the package template.
|
|
47
|
+
*
|
|
48
|
+
* Clones the template repo, patches IDs and metadata, runs npm install + build,
|
|
49
|
+
* initializes a fresh git repo, then delegates to {@link installPackageFromBundles}
|
|
50
|
+
* to register the dev bundle in the database.
|
|
51
|
+
*/
|
|
52
|
+
async function createPackage(dataContext, deps, opts) {
|
|
53
|
+
const location = opts.location ?? deps.resolvePath(deps.packagesRootDir, nameToFolderSlug(opts.name));
|
|
54
|
+
// If directory already exists, treat as an import rather than a fresh create
|
|
55
|
+
if (await deps.fs.exists(location)) {
|
|
56
|
+
const info = await (0, package_installer_1.getPackageInfo)(deps, location);
|
|
57
|
+
if (info.packageId) {
|
|
58
|
+
const result = await (0, package_installer_1.installPackageFromBundles)(dataContext, deps, info.packageId, {
|
|
59
|
+
localPath: location,
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
packageId: info.packageId,
|
|
63
|
+
localPath: location,
|
|
64
|
+
packageVersion: result.packageVersion,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`Directory already exists at ${location} but does not contain a valid Peers package`);
|
|
68
|
+
}
|
|
69
|
+
// Generate fresh IDs
|
|
70
|
+
const packageId = (0, utils_1.newid)();
|
|
71
|
+
const contractId = (0, utils_1.newid)();
|
|
72
|
+
const appScreenId = (0, utils_1.newid)();
|
|
73
|
+
const templateRepo = opts.templateRepo ?? DEFAULT_TEMPLATE_REPO;
|
|
74
|
+
// Get template contents into location
|
|
75
|
+
if (opts.templateRepo && opts.templateRepo !== DEFAULT_TEMPLATE_REPO) {
|
|
76
|
+
// Custom template — clone directly (no caching)
|
|
77
|
+
await deps.shell.exec(`git clone ${templateRepo} ${location}`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Default template — use local cache for speed and offline support
|
|
81
|
+
const cachePath = await ensureTemplateCache(deps, templateRepo);
|
|
82
|
+
await deps.shell.exec(`cp -r ${cachePath}/. ${location}`);
|
|
83
|
+
}
|
|
84
|
+
// Patch package.json
|
|
85
|
+
const packageJsonPath = deps.resolvePath(location, "package.json");
|
|
86
|
+
const packageObj = await deps.fs.readJson(packageJsonPath);
|
|
87
|
+
packageObj.peers = packageObj.peers || {};
|
|
88
|
+
packageObj.peers.packageId = packageId;
|
|
89
|
+
packageObj.name = (0, utils_1.camelCaseToHyphens)(opts.name);
|
|
90
|
+
if (opts.author) {
|
|
91
|
+
packageObj.author = opts.author;
|
|
92
|
+
}
|
|
93
|
+
delete packageObj.repository;
|
|
94
|
+
await deps.fs.writeFile(packageJsonPath, JSON.stringify(packageObj, null, 2));
|
|
95
|
+
// Patch src/consts.ts placeholders
|
|
96
|
+
const constsPath = deps.resolvePath(location, "src", "consts.ts");
|
|
97
|
+
if (await deps.fs.exists(constsPath)) {
|
|
98
|
+
let consts = await deps.fs.readFile(constsPath);
|
|
99
|
+
consts = consts
|
|
100
|
+
.replace("<package-name>", opts.name)
|
|
101
|
+
.replace("<package-id>", packageId)
|
|
102
|
+
.replace("<app-screen-id>", appScreenId)
|
|
103
|
+
.replace("<contract-id>", contractId);
|
|
104
|
+
await deps.fs.writeFile(constsPath, consts);
|
|
105
|
+
}
|
|
106
|
+
// Initialize as a new project: remove template git history, install, build, fresh git
|
|
107
|
+
await deps.shell.exec("rm -rf .git", { cwd: location });
|
|
108
|
+
await deps.shell.exec("npm install", { cwd: location });
|
|
109
|
+
await deps.shell.exec("npm run build", { cwd: location });
|
|
110
|
+
await deps.shell.exec("git init", { cwd: location });
|
|
111
|
+
await deps.shell.exec("git add .", { cwd: location });
|
|
112
|
+
await deps.shell.exec('git commit -m "Initial commit"', { cwd: location });
|
|
113
|
+
// Generate signing keypair and store secret in user's personal group
|
|
114
|
+
let publishPublicKey = "";
|
|
115
|
+
if (opts.personalContext) {
|
|
116
|
+
const keys = (0, keys_1.newKeys)();
|
|
117
|
+
publishPublicKey = keys.publicKey;
|
|
118
|
+
const keyName = `${package_author_signing_1.PACKAGE_SIGNING_KEY_PREFIX}${packageId}`;
|
|
119
|
+
await (0, persistent_vars_1.PersistentVars)(opts.personalContext).save({
|
|
120
|
+
persistentVarId: (0, utils_1.deterministicPvarId)(keyName),
|
|
121
|
+
name: keyName,
|
|
122
|
+
scope: "user",
|
|
123
|
+
value: { value: keys.secretKey },
|
|
124
|
+
isSecret: true,
|
|
125
|
+
description: `Signing key for package "${opts.name}"`,
|
|
126
|
+
modifiedAt: Date.now(),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Register the package record
|
|
130
|
+
await (0, packages_1.Packages)(dataContext).signAndSave({
|
|
131
|
+
packageId,
|
|
132
|
+
name: opts.name,
|
|
133
|
+
description: "",
|
|
134
|
+
createdBy: system_ids_1.peersRootUserId,
|
|
135
|
+
disabled: false,
|
|
136
|
+
publishPublicKey,
|
|
137
|
+
signature: "",
|
|
138
|
+
}, { restoreIfDeleted: true, saveAsSnapshot: true });
|
|
139
|
+
// Set the packageLocalPath device var
|
|
140
|
+
const pvar = (0, persistent_vars_1.groupDeviceVar)(`packageLocalPath_${packageId}`, {
|
|
141
|
+
defaultValue: deps.packagesRootDir,
|
|
142
|
+
dataContext,
|
|
143
|
+
});
|
|
144
|
+
pvar(location);
|
|
145
|
+
// Install the dev bundle
|
|
146
|
+
const result = await (0, package_installer_1.installPackageFromBundles)(dataContext, deps, packageId, {
|
|
147
|
+
localPath: location,
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
packageId,
|
|
151
|
+
localPath: location,
|
|
152
|
+
packageVersion: result.packageVersion,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|