@peers-app/peers-sdk 0.28.7 → 0.28.8

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.
@@ -71,6 +71,12 @@ export interface IContractDefinitionWithPolicy {
71
71
  definition: IContractDefinition;
72
72
  /** Each tool's policy, where the Runtime knows it. */
73
73
  tools?: IContractToolPolicy[];
74
+ /** Active provider's table settings, when known; absent on older hosts. */
75
+ tables?: {
76
+ name: string;
77
+ versionNumber?: number;
78
+ localOnly?: boolean;
79
+ }[];
74
80
  }
75
81
  /**
76
82
  * Consumer shape for the System Contract Catalog.
@@ -47,6 +47,7 @@ function getContractCatalogDefinition() {
47
47
  outputFields: [
48
48
  field("definition", field_type_1.FieldType.object, "The registered definition, in Peers IR."),
49
49
  field("tools", field_type_1.FieldType.object, "Each tool's name, accessLevel, and authorityPropagation on the active provider.", { isArray: true, optional: true }),
50
+ field("tables", field_type_1.FieldType.object, "Each table's name, versionNumber, and localOnly setting on the active provider.", { isArray: true, optional: true }),
50
51
  ],
51
52
  },
52
53
  ],
@@ -42,6 +42,8 @@ export interface IPackageObservedRuntime {
42
42
  export interface IPackageRequestedVersion {
43
43
  /** Group-wide active version (`Packages.activePackageVersionId`). */
44
44
  groupActivePackageVersionId?: string;
45
+ /** The group selected a version whose row is not on this device yet. */
46
+ groupVersionMissing?: boolean;
45
47
  /** This device's override, when set. */
46
48
  deviceActivePackageVersionId?: string;
47
49
  /** The version this device should run: the device override, else the group version. */
@@ -101,8 +103,8 @@ export interface IPackageOperationResult {
101
103
  /** True when `createPackage` scaffolded a new package instead of importing an existing folder. */
102
104
  created?: boolean;
103
105
  /**
104
- * Non-fatal problem the caller should show: for example a missing package template, or a
105
- * reloaded dev build that reuses a released version number and should be bumped.
106
+ * Non-fatal problem the caller should show, for example a missing package template.
107
+ * Older hosts also report released-number conflicts here instead of refusing reload.
106
108
  */
107
109
  warning?: string;
108
110
  /** The package after the operation. */
@@ -120,7 +122,7 @@ export interface IPackageReloadOutcome {
120
122
  changed?: boolean;
121
123
  /** Failure message when `ok` is false. */
122
124
  error?: string;
123
- /** Non-fatal problem, such as a dev build that reuses a released version number. */
125
+ /** Non-fatal problem; older hosts may warn here about a reused released number. */
124
126
  warning?: string;
125
127
  }
126
128
  /** Result of `reloadAllPackages`. */
@@ -141,11 +143,19 @@ export interface IPromotePackageVersionResult extends IPackageOperationResult {
141
143
  /** True when this device holds the package signing key and signed the promoted version. */
142
144
  signed: boolean;
143
145
  /**
144
- * True when the version this device was running now matches the group's follow policy
145
- * and was activated, for the group unless this device overrides group settings.
146
+ * True when activation ran, either explicitly with activateForGroup or automatically
147
+ * under the group follow policy. See groupRunsPromotedVersion for the group outcome.
146
148
  */
147
149
  activated: boolean;
150
+ /** Group selection after promotion, when one is set. */
151
+ groupActivePackageVersionId?: string;
152
+ /** True even when the group already selected this id before it was re-tagged. */
153
+ groupRunsPromotedVersion?: boolean;
154
+ /** What the group now runs, why selection stayed unchanged, and any next command. */
155
+ activationNote?: string;
148
156
  }
157
+ /** Explain a group version missing locally and how to select an available release. */
158
+ export declare function packageGroupVersionWarning(packageId: string, requested: Pick<IPackageRequestedVersion, "groupActivePackageVersionId" | "groupVersionMissing">): string | undefined;
149
159
  /**
150
160
  * Release channels a version may be promoted to from its current tag: `dev` to `beta` or
151
161
  * `stable`, and `beta` to `stable`. Missing or empty tags on legacy versions mean `dev`,
@@ -207,7 +217,7 @@ export interface IPackagesConsumer extends IContractConsumerBase {
207
217
  packageVersionId?: string;
208
218
  scope?: "device" | "group";
209
219
  }): Promise<IPackageOperationResult>;
210
- /** Rebuild the dev version from disk when it changed, then force-reload the package. */
220
+ /** Read local bundles, then force-reload the selected version even when unchanged. */
211
221
  reloadPackage(input: {
212
222
  packageId: string;
213
223
  }): Promise<IPackageOperationResult>;
@@ -228,10 +238,13 @@ export interface IPackagesConsumer extends IContractConsumerBase {
228
238
  * Promote a stored version to `beta` or `stable`: re-tag, re-sign with the package author
229
239
  * key when this device has it, record the promotion in the version's history, and
230
240
  * activate it when this device was running it and the group follows the new tag.
241
+ * activateForGroup explicitly selects it for the group and follows that selection here.
231
242
  */
232
243
  promotePackageVersion(input: {
233
244
  packageVersionId: string;
234
245
  versionTag: string;
246
+ /** Also select the promoted version for the group and follow it here (Admin). */
247
+ activateForGroup?: boolean;
235
248
  }): Promise<IPromotePackageVersionResult>;
236
249
  /** Install the default packages into the bound data context. */
237
250
  installDefaultPackages(): Promise<{
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PACKAGES_CONTRACT_VERSION = void 0;
4
+ exports.packageGroupVersionWarning = packageGroupVersionWarning;
4
5
  exports.packageVersionPromotionTargets = packageVersionPromotionTargets;
5
6
  exports.isPackageRepositoryUrl = isPackageRepositoryUrl;
6
7
  exports.getPackagesContractDefinition = getPackagesContractDefinition;
@@ -9,6 +10,12 @@ const field_type_1 = require("../../types/field-type");
9
10
  const system_contracts_1 = require("../system-contracts");
10
11
  /** Version of the built-in System Packages contract. */
11
12
  exports.PACKAGES_CONTRACT_VERSION = 1;
13
+ /** Explain a group version missing locally and how to select an available release. */
14
+ function packageGroupVersionWarning(packageId, requested) {
15
+ if (!requested.groupVersionMissing || !requested.groupActivePackageVersionId)
16
+ return undefined;
17
+ return `The group's selected version ${requested.groupActivePackageVersionId} isn't on this device; it may not have synced yet. To select an available beta or stable version for the group, run: peers packages activate ${packageId} <packageVersionId> --group`;
18
+ }
12
19
  /**
13
20
  * Release channels a version may be promoted to from its current tag: `dev` to `beta` or
14
21
  * `stable`, and `beta` to `stable`. Missing or empty tags on legacy versions mean `dev`,
@@ -138,7 +145,7 @@ function getPackagesContractDefinition() {
138
145
  },
139
146
  {
140
147
  name: "reloadPackage",
141
- usageDescription: "Rebuild the dev version from the package folder when it changed, then force-reload the package on this device.",
148
+ usageDescription: "Read local bundles, then force-reload the selected version even when unchanged. Changed builds at a released number are refused.",
142
149
  inputFields: [packageIdField],
143
150
  outputFields: operationResultFields,
144
151
  },
@@ -170,10 +177,11 @@ function getPackagesContractDefinition() {
170
177
  },
171
178
  {
172
179
  name: "promotePackageVersion",
173
- usageDescription: "Promote a stored package version to beta or stable (dev to beta or stable, beta to stable). Re-signs it with the package author key when this device has it, records the promotion in its history, and activates it when this device was running it and the group follows the new tag.",
180
+ usageDescription: "Promote a stored package version to beta or stable (dev to beta or stable, beta to stable). Re-signs it with the package author key when this device has it, records the promotion in its history, and activates it when this device was running it and the group follows the new tag. Optionally selects it for the group with activateForGroup. Reports the group selection and any next command.",
174
181
  inputFields: [
175
182
  field("packageVersionId", field_type_1.FieldType.id, "Version to promote."),
176
183
  field("versionTag", field_type_1.FieldType.string, "New release channel: beta or stable."),
184
+ field("activateForGroup", field_type_1.FieldType.boolean, "Also select this version for the group and follow it here (Admin).", { optional: true }),
177
185
  ],
178
186
  outputFields: [
179
187
  ...operationResultFields,
@@ -181,6 +189,11 @@ function getPackagesContractDefinition() {
181
189
  field("version", field_type_1.FieldType.string, "Its version number."),
182
190
  field("versionTag", field_type_1.FieldType.string, "Its new release channel."),
183
191
  field("signed", field_type_1.FieldType.boolean, "True when this device signed the promoted version."),
192
+ field("groupActivePackageVersionId", field_type_1.FieldType.id, "Group selection after promotion.", {
193
+ optional: true,
194
+ }),
195
+ field("groupRunsPromotedVersion", field_type_1.FieldType.boolean, "Whether the group now selects the promoted version, including an already-selected id.", { optional: true }),
196
+ field("activationNote", field_type_1.FieldType.string, "What the group now runs and any next command.", { optional: true }),
184
197
  field("activated", field_type_1.FieldType.boolean, "True when the promotion also activated the version for the group or this device."),
185
198
  ],
186
199
  },
@@ -115,6 +115,10 @@ declare const schema: z.ZodObject<{
115
115
  }>;
116
116
  /** A signed package build and its bundle hashes, tag, and lifecycle history. */
117
117
  export type IPackageVersion = z.infer<typeof schema>;
118
+ /** Find a beta or stable build with the same package and version number, excluding a supplied id. */
119
+ export declare function releasedVersionWithNumber(versions: readonly IPackageVersion[], candidate: Pick<IPackageVersion, "packageId" | "version"> & {
120
+ packageVersionId?: string;
121
+ }): IPackageVersion | undefined;
118
122
  /** Package version table with signature and group promotion permission checks. */
119
123
  export declare class PackageVersionsTable extends Table<IPackageVersion> {
120
124
  static isPassthrough: boolean;
@@ -35,6 +35,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
35
35
  };
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.PackageVersionsTable = void 0;
38
+ exports.releasedVersionWithNumber = releasedVersionWithNumber;
38
39
  exports.PackageVersions = PackageVersions;
39
40
  exports.computePackageVersionHash = computePackageVersionHash;
40
41
  exports.isVersionInRange = isVersionInRange;
@@ -100,6 +101,13 @@ const metaData = {
100
101
  fields: (0, types_1.schemaToFields)(schema),
101
102
  indexes: [{ fields: ["packageId"] }],
102
103
  };
104
+ /** Find a beta or stable build with the same package and version number, excluding a supplied id. */
105
+ function releasedVersionWithNumber(versions, candidate) {
106
+ return versions.find((other) => other.packageId === candidate.packageId &&
107
+ other.packageVersionId !== candidate.packageVersionId &&
108
+ other.version === candidate.version &&
109
+ (other.versionTag === "beta" || other.versionTag === "stable"));
110
+ }
103
111
  // Symbol.for keeps signer registration shared by separately installed SDK copies.
104
112
  const signingKey = Symbol.for("@peers-app/peers-sdk/package-version-signing");
105
113
  const signingGlobals = globalThis;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.main = main;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const path = require("node:path");
6
+ const sdk_version_1 = require("../sdk-version");
6
7
  const build_package_1 = require("./build-package");
7
8
  const contract_add_1 = require("./contract-add");
8
9
  const freeze_contracts_1 = require("./freeze-contracts");
@@ -11,7 +12,7 @@ const USAGE = `Usage: peers-package <command> [options]
11
12
  Commands:
12
13
  manifest Write dist/manifest.json from the package's declarations (src/package.ts).
13
14
  build Write the manifest and dist/package.bundle.js, the artifact the Runtime
14
- installs.
15
+ installs. Prints the SDK version used to build it.
15
16
  contract add <source> Write src/contracts/<name>.v<version>.ts, a declaration of a contract this
16
17
  package implements or calls, generated from <source>: a declaration module
17
18
  and its export (path/to/file.ts#Name), a provider's manifest.json or
@@ -96,13 +97,13 @@ async function main(argv) {
96
97
  console.error(`Build failed: ${result.message}`);
97
98
  return;
98
99
  }
99
- console.log(`Built ${relative(result.artifactPath)} (${summarize(result)}; provider ${Math.round(result.providerChars / 1024)} KB)`);
100
+ console.log(`Built ${relative(result.artifactPath)} (${summarize(result)}; SDK ${sdk_version_1.SDK_VERSION}; provider ${Math.round(result.providerChars / 1024)} KB)`);
100
101
  });
101
102
  console.log("Watching for changes");
102
103
  return new Promise(() => undefined);
103
104
  }
104
105
  const result = await (0, build_package_1.buildPackage)(options);
105
- console.log(`Built ${relative(result.artifactPath)} (${summarize(result)}; provider ${Math.round(result.providerChars / 1024)} KB)`);
106
+ console.log(`Built ${relative(result.artifactPath)} (${summarize(result)}; SDK ${sdk_version_1.SDK_VERSION}; provider ${Math.round(result.providerChars / 1024)} KB)`);
106
107
  return 0;
107
108
  }
108
109
  /** `peers-package contract add <source> [options]` or `peers-package contract freeze [options]`. */
@@ -1,11 +1,13 @@
1
+ import { type IContractSource } from "./contract-source";
1
2
  /** What {@link addContract} takes. */
2
3
  export interface IContractAddOptions {
3
4
  /**
4
5
  * Where the contract comes from: a declaration module and its export
5
6
  * (`../voice-hub/src/contracts/voice-skill.ts#VoiceSkill`), a provider's `manifest.json` or
6
- * `package.bundle.js`, or the JSON `getContractDefinition` returned.
7
+ * `package.bundle.js`, the JSON `getContractDefinition` returned, or an in-memory
8
+ * contract source (for example from `contractSourceFromDefinition`).
7
9
  */
8
- source: string;
10
+ source: string | IContractSource;
9
11
  /** `contractId@version`, for a manifest that provides more than one contract. */
10
12
  contract?: string;
11
13
  /** Package root. Defaults to the current directory. */
@@ -19,9 +19,10 @@ const contract_source_1 = require("./contract-source");
19
19
  */
20
20
  async function addContract(options) {
21
21
  const root = path.resolve(options.root ?? process.cwd());
22
- const contract = await readContractSource(root, options);
22
+ const contract = typeof options.source === "string"
23
+ ? await readContractSource(root, { ...options, source: options.source })
24
+ : options.source;
23
25
  const generated = (0, contract_source_1.generateContractDeclaration)(contract, {
24
- from: sourceLabel(root, options.source),
25
26
  ...(options.name ? { exportName: options.name } : {}),
26
27
  });
27
28
  const target = path.join(path.resolve(root, options.outDir ?? "src/contracts"), generated.fileName);
@@ -41,15 +42,6 @@ function splitSource(source) {
41
42
  ? { file: source.slice(0, hash), exportName: source.slice(hash + 1) }
42
43
  : { file: source };
43
44
  }
44
- /**
45
- * The source as the file's header names it: relative to the package root, with `/`, so the
46
- * generated file (and `--check`) doesn't depend on where the command ran or on local paths.
47
- */
48
- function sourceLabel(root, source) {
49
- const { file, exportName } = splitSource(source);
50
- const relative = path.relative(root, path.resolve(file)).split(path.sep).join("/");
51
- return exportName === undefined ? relative : `${relative}#${exportName}`;
52
- }
53
45
  async function readContractSource(root, options) {
54
46
  const { file, exportName } = splitSource(options.source);
55
47
  if (exportName !== undefined) {
@@ -49,11 +49,14 @@ export declare function parseContractSelector(selector: string): {
49
49
  * propagation. Refinements (a regex, a minimum) and defaults made by code aren't in the IR, so
50
50
  * generated schemas check only what the IR describes. Objects keep keys their fields don't list.
51
51
  *
52
+ * Table storage settings are preserved: implementing the declaration uses its tables.
53
+ * Default settings (schema version 1 and synced storage) are omitted.
54
+ *
52
55
  * @param input - The contract and its tables.
53
- * @param options - Where it came from (for the file's header), and an export name to use
54
- * instead of the one made from the contract's name.
56
+ * @param options - An export name to use instead of the one made from the contract's name.
55
57
  */
56
- export declare function generateContractDeclaration(input: IContractSource, options: {
57
- from: string;
58
+ export declare function generateContractDeclaration(input: IContractSource, options?: {
59
+ /** Ignored for compatibility with older callers; generated headers never name a source. */
60
+ from?: string;
58
61
  exportName?: string;
59
62
  }): IGeneratedContractDeclaration;
@@ -67,6 +67,7 @@ function contractSourceFromDefinition(input) {
67
67
  throw new Error("Expected getContractDefinition's result: { definition, tools }");
68
68
  }
69
69
  const policy = new Map((input.tools ?? []).map((tool) => [tool.name, tool]));
70
+ const tablePolicy = new Map((input.tables ?? []).map((table) => [table.name, table]));
70
71
  const tools = definition.tools.map((tool) => {
71
72
  const toolPolicy = policy.get(tool.name);
72
73
  if (toolPolicy?.accessLevel === undefined) {
@@ -101,12 +102,17 @@ function contractSourceFromDefinition(input) {
101
102
  ...(definition.observables?.length ? { observables: definition.observables } : {}),
102
103
  ...(definition.events?.length ? { events: definition.events } : {}),
103
104
  },
104
- tables: (definition.tables ?? []).map((table) => ({
105
- name: table.name,
106
- description: table.description,
107
- primaryKeyName: table.primaryKeyName,
108
- fields: table.fields,
109
- })),
105
+ tables: (definition.tables ?? []).map((table) => {
106
+ const settings = tablePolicy.get(table.name);
107
+ return {
108
+ name: table.name,
109
+ description: table.description,
110
+ primaryKeyName: table.primaryKeyName,
111
+ fields: table.fields,
112
+ ...(settings?.versionNumber === undefined ? {} : { versionNumber: settings.versionNumber }),
113
+ ...(settings?.localOnly ? { localOnly: true } : {}),
114
+ };
115
+ }),
110
116
  };
111
117
  }
112
118
  /**
@@ -131,11 +137,13 @@ function parseContractSelector(selector) {
131
137
  * propagation. Refinements (a regex, a minimum) and defaults made by code aren't in the IR, so
132
138
  * generated schemas check only what the IR describes. Objects keep keys their fields don't list.
133
139
  *
140
+ * Table storage settings are preserved: implementing the declaration uses its tables.
141
+ * Default settings (schema version 1 and synced storage) are omitted.
142
+ *
134
143
  * @param input - The contract and its tables.
135
- * @param options - Where it came from (for the file's header), and an export name to use
136
- * instead of the one made from the contract's name.
144
+ * @param options - An export name to use instead of the one made from the contract's name.
137
145
  */
138
- function generateContractDeclaration(input, options) {
146
+ function generateContractDeclaration(input, options = {}) {
139
147
  const { contract, tables } = input;
140
148
  assertVersion(contract.version, "Contract version");
141
149
  const exportName = options.exportName ?? pascalCase(contract.name);
@@ -145,7 +153,7 @@ function generateContractDeclaration(input, options) {
145
153
  const imports = new Set(["declareContract"]);
146
154
  const code = new Code(imports);
147
155
  const lines = [];
148
- lines.push(`// Generated by \`peers-package contract add\` from ${docText(options.from)}.`, "// Regenerate it rather than edit it; a stable package release freezes the contract's shape.", "");
156
+ lines.push(`// Generated contract declaration. Regenerate with \`peers contracts add ${docText(contract.contractId)}@${contract.version}\``, "// or `peers-package contract add <source>`; do not edit by hand.", "");
149
157
  const eventNames = new Map();
150
158
  for (const event of contract.events ?? []) {
151
159
  const name = uniqueName(`${camelCase(exportName)}${pascalCase(event.name)}`, new Set([...RESERVED_NAMES, exportName, ...eventNames.values()]));
@@ -223,7 +231,7 @@ class Code {
223
231
  ["description", JSON.stringify(table.description ?? "")],
224
232
  ["primaryKeyName", JSON.stringify(table.primaryKeyName)],
225
233
  ["schema", this.objectSchema(table.fields, depth + 1)],
226
- ...(table.versionNumber === undefined
234
+ ...(table.versionNumber === undefined || table.versionNumber === 1
227
235
  ? []
228
236
  : [["versionNumber", String(table.versionNumber)]]),
229
237
  ...(table.localOnly ? [["localOnly", "true"]] : []),
@@ -237,7 +245,7 @@ class Code {
237
245
  ["name", JSON.stringify(tool.name)],
238
246
  ["usageDescription", JSON.stringify(tool.usageDescription)],
239
247
  ["accessLevel", this.accessLevel(level)],
240
- ...(tool.authorityPropagation
248
+ ...(tool.authorityPropagation && tool.authorityPropagation !== "cap"
241
249
  ? [["authorityPropagation", JSON.stringify(tool.authorityPropagation)]]
242
250
  : []),
243
251
  ...(tool.remoteAccessLevel === undefined
@@ -323,7 +331,10 @@ class Code {
323
331
  return `generatedField(${schema}, ${this.shapeSource(metadata)}${valueType === undefined ? "" : `, ${this.fieldType(valueType)}`})`;
324
332
  }
325
333
  shapeSource(shape) {
326
- const entries = Object.entries(shape).map(([key, value]) => {
334
+ const entries = Object.entries(shape)
335
+ .filter(([, value]) => value !== undefined)
336
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
337
+ .map(([key, value]) => {
327
338
  const source = key === "type"
328
339
  ? this.fieldType(value)
329
340
  : key === "fields"
@@ -422,6 +433,8 @@ function dataSource(value) {
422
433
  return `[${value.map(dataSource).join(", ")}]`;
423
434
  if (value && typeof value === "object") {
424
435
  return `{ ${Object.entries(value)
436
+ .filter(([, entry]) => entry !== undefined)
437
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
425
438
  .map(([key, entry]) => `${propertyKey(key)}: ${dataSource(entry)}`)
426
439
  .join(", ")} }`;
427
440
  }
@@ -0,0 +1,3 @@
1
+ /** Node-only helpers for generating package contract declarations. */
2
+ export { addContract, type IContractAddOptions, type IContractAddResult } from "./contract-add";
3
+ export { contractSourceFromDefinition, contractSourceFromManifest, generateContractDeclaration, type IContractSource, type IGeneratedContractDeclaration, parseContractSelector, } from "./contract-source";
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseContractSelector = exports.generateContractDeclaration = exports.contractSourceFromManifest = exports.contractSourceFromDefinition = exports.addContract = void 0;
4
+ /** Node-only helpers for generating package contract declarations. */
5
+ var contract_add_1 = require("./contract-add");
6
+ Object.defineProperty(exports, "addContract", { enumerable: true, get: function () { return contract_add_1.addContract; } });
7
+ var contract_source_1 = require("./contract-source");
8
+ Object.defineProperty(exports, "contractSourceFromDefinition", { enumerable: true, get: function () { return contract_source_1.contractSourceFromDefinition; } });
9
+ Object.defineProperty(exports, "contractSourceFromManifest", { enumerable: true, get: function () { return contract_source_1.contractSourceFromManifest; } });
10
+ Object.defineProperty(exports, "generateContractDeclaration", { enumerable: true, get: function () { return contract_source_1.generateContractDeclaration; } });
11
+ Object.defineProperty(exports, "parseContractSelector", { enumerable: true, get: function () { return contract_source_1.parseContractSelector; } });
@@ -126,5 +126,6 @@ async function clonePackage(dataContext, deps, opts) {
126
126
  localPath: location,
127
127
  remoteRepo: opts.remoteRepo,
128
128
  packageVersion: result.packageVersion,
129
+ warning: result.warning,
129
130
  };
130
131
  }
@@ -313,6 +313,7 @@ async function importExistingPackage(dataContext, deps, opts) {
313
313
  packageId,
314
314
  localPath: opts.location,
315
315
  packageVersion: result.packageVersion,
316
+ warning: result.warning,
316
317
  };
317
318
  }
318
319
  /**
@@ -413,6 +414,7 @@ async function createPackage(dataContext, deps, opts) {
413
414
  packageId,
414
415
  localPath: location,
415
416
  packageVersion: result.packageVersion,
417
+ warning: result.warning,
416
418
  templateWarning,
417
419
  };
418
420
  }
@@ -9,7 +9,9 @@ import type { IInstallOpts, IInstallResult, IPackageInfo, IPackageInstallerDeps
9
9
  * only when content has changed, upserts the PackageVersion, and optionally
10
10
  * triggers the package loader and device version resolution.
11
11
  *
12
- * When the bundle hash matches the existing PackageVersion, no files or PV are
12
+ * A matching released version is returned without any writes or loading, even when
13
+ * the group has no selected version. Changed bundles at a released number are refused.
14
+ * When the bundle hash matches the existing dev PackageVersion, no files or PV are
13
15
  * rewritten. An Admin's Packages row still gets a missing `activePackageVersionId`
14
16
  * (and description), and when the id was missing the version is activated on
15
17
  * this device unless `skipResolve`.
@@ -14,8 +14,10 @@ const package_version_resolver_1 = require("../data/package-version-resolver");
14
14
  const package_versions_1 = require("../data/package-versions");
15
15
  const packages_1 = require("../data/packages");
16
16
  const persistent_vars_1 = require("../data/persistent-vars");
17
+ const sdk_version_1 = require("../sdk-version");
17
18
  const utils_1 = require("../utils");
18
19
  const package_author_signer_1 = require("./package-author-signer");
20
+ const package_sdk_version_1 = require("./package-sdk-version");
19
21
  /**
20
22
  * Install or update a dev PackageVersion from locally-built bundle files.
21
23
  *
@@ -24,7 +26,9 @@ const package_author_signer_1 = require("./package-author-signer");
24
26
  * only when content has changed, upserts the PackageVersion, and optionally
25
27
  * triggers the package loader and device version resolution.
26
28
  *
27
- * When the bundle hash matches the existing PackageVersion, no files or PV are
29
+ * A matching released version is returned without any writes or loading, even when
30
+ * the group has no selected version. Changed bundles at a released number are refused.
31
+ * When the bundle hash matches the existing dev PackageVersion, no files or PV are
28
32
  * rewritten. An Admin's Packages row still gets a missing `activePackageVersionId`
29
33
  * (and description), and when the id was missing the version is activated on
30
34
  * this device unless `skipResolve`.
@@ -39,6 +43,7 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
39
43
  // 2. Read package info
40
44
  const info = await getPackageInfo(deps, localPath);
41
45
  const version = info.version ?? "0.0.1";
46
+ const warning = await installedSdkWarning(deps, localPath);
42
47
  // 3. Read bundle files
43
48
  const distDir = deps.resolvePath(localPath, "dist");
44
49
  const packageBundlePath = deps.resolvePath(distDir, "package.bundle.js");
@@ -60,12 +65,26 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
60
65
  const uiHash = uiBundle ? (0, file_types_1.computeFileHash)(uiBundle) : undefined;
61
66
  // 5. Compute PV hash
62
67
  const pvHash = (0, package_versions_1.computePackageVersionHash)(version, "", packageHash, routesHash, uiHash);
63
- // 6. Early exit if unchanged. Still repair missing appNavs (see syncDevBundles
64
- // skipLoad), and still fill an empty activePackageVersionId (e.g. reimport after
65
- // the Packages row was deleted). If it was empty, activate on the device unless
66
- // skipResolve.
68
+ // 6. Check released numbers before looking for a mutable local build.
67
69
  const pvTable = (0, package_versions_1.PackageVersions)(dataContext);
68
- const existingPv = await pvTable.findOne({ packageId, versionTag });
70
+ const released = (0, package_versions_1.releasedVersionWithNumber)(await pvTable.list({ packageId }), {
71
+ packageId,
72
+ version,
73
+ });
74
+ if (released) {
75
+ if (released.packageBundleFileHash !== packageHash ||
76
+ released.routesBundleFileHash !== routesHash ||
77
+ released.uiBundleFileHash !== uiHash) {
78
+ throw new Error(`v${version} is already released as ${released.versionTag}; bump the version in package.json, rebuild, and reload`);
79
+ }
80
+ return { packageVersion: released, loaded: undefined, unchanged: true, warning };
81
+ }
82
+ // Released rows are immutable builds: a new number gets a new row even when
83
+ // an SDK caller explicitly installs with a release tag.
84
+ const existingPv = versionTag === "beta" || versionTag === "stable"
85
+ ? undefined
86
+ : await pvTable.findOne({ packageId, versionTag });
87
+ // An unchanged dev build can still repair appNavs and an empty group selection.
69
88
  if (existingPv && existingPv.packageVersionHash === pvHash) {
70
89
  let loaded;
71
90
  let unchanged = true;
@@ -77,7 +96,7 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
77
96
  skipResolve: opts?.skipResolve,
78
97
  resolveOnlyWhenActivated: true,
79
98
  });
80
- return { packageVersion: existingPv, loaded, unchanged };
99
+ return { packageVersion: existingPv, loaded, unchanged, warning };
81
100
  }
82
101
  // 7. Save bundle files (deduped by hash)
83
102
  const packageFile = await saveBundleFile(dataContext, packageBundle, `${packageId}-package.bundle.js`, packageId);
@@ -122,7 +141,17 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
122
141
  await activateInstalledPackageVersion(dataContext, packageId, savedPv, info, localPath, {
123
142
  skipResolve: opts?.skipResolve,
124
143
  });
125
- return { packageVersion: savedPv, loaded, unchanged: false };
144
+ return { packageVersion: savedPv, loaded, unchanged: false, warning };
145
+ }
146
+ async function installedSdkWarning(deps, localPath) {
147
+ try {
148
+ const metadata = await deps.fs.readJson(deps.resolvePath(localPath, "node_modules", "@peers-app", "peers-sdk", "package.json"));
149
+ return (0, package_sdk_version_1.packageSdkVersionWarning)(metadata?.version, sdk_version_1.SDK_VERSION);
150
+ }
151
+ catch {
152
+ // Packages may ship without node_modules, or the host may not be able to read it.
153
+ return undefined;
154
+ }
126
155
  }
127
156
  /**
128
157
  * Sync the Packages row with an installed PackageVersion, then activate it on
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Warn when a package's installed SDK is newer than the host's, using SemVer precedence.
3
+ * Missing or invalid versions are ignored; build metadata does not affect precedence.
4
+ */
5
+ export declare function packageSdkVersionWarning(packageVersion: unknown, hostVersion: unknown): string | undefined;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.packageSdkVersionWarning = packageSdkVersionWarning;
4
+ /**
5
+ * Warn when a package's installed SDK is newer than the host's, using SemVer precedence.
6
+ * Missing or invalid versions are ignored; build metadata does not affect precedence.
7
+ */
8
+ function packageSdkVersionWarning(packageVersion, hostVersion) {
9
+ const incoming = parseVersion(packageVersion);
10
+ const host = parseVersion(hostVersion);
11
+ if (!incoming || !host || compareVersions(incoming, host) <= 0)
12
+ return undefined;
13
+ return `Built with @peers-app/peers-sdk ${packageVersion}, newer than this Peers (${hostVersion}); update Peers, or pin the SDK to ${hostVersion} in package.json`;
14
+ }
15
+ function parseVersion(value) {
16
+ if (typeof value !== "string")
17
+ return undefined;
18
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value);
19
+ if (!match || match[0] !== value)
20
+ return undefined;
21
+ const prerelease = match[4]?.split(".") ?? [];
22
+ if (prerelease.some((part) => /^0\d+$/.test(part)))
23
+ return undefined;
24
+ return { core: match.slice(1, 4), prerelease };
25
+ }
26
+ function compareIdentifiers(a, b) {
27
+ const aNumeric = /^\d+$/.test(a);
28
+ const bNumeric = /^\d+$/.test(b);
29
+ if (aNumeric && bNumeric && a.length !== b.length)
30
+ return a.length - b.length;
31
+ if (aNumeric !== bNumeric)
32
+ return aNumeric ? -1 : 1;
33
+ return a === b ? 0 : a > b ? 1 : -1;
34
+ }
35
+ function compareVersions(a, b) {
36
+ for (let index = 0; index < a.core.length; index++) {
37
+ const difference = compareIdentifiers(a.core[index], b.core[index]);
38
+ if (difference)
39
+ return difference;
40
+ }
41
+ if (!a.prerelease.length || !b.prerelease.length) {
42
+ return Number(!a.prerelease.length) - Number(!b.prerelease.length);
43
+ }
44
+ for (let index = 0; index < Math.min(a.prerelease.length, b.prerelease.length); index++) {
45
+ const difference = compareIdentifiers(a.prerelease[index], b.prerelease[index]);
46
+ if (difference)
47
+ return difference;
48
+ }
49
+ return a.prerelease.length - b.prerelease.length;
50
+ }
@@ -45,7 +45,7 @@ export interface IPackageInstallerDeps {
45
45
  export interface IInstallOpts {
46
46
  /** Override local path; otherwise read from the packageLocalPath device var. */
47
47
  localPath?: string;
48
- /** Version tag for the created PackageVersion. Defaults to `"dev"`. */
48
+ /** Tag for a new build (default `"dev"`). An identical existing release is returned as-is. */
49
49
  versionTag?: string;
50
50
  /** Skip keeping the package in the loader cache for activation. AppNavs are still backfilled when missing. */
51
51
  skipLoad?: boolean;
@@ -54,12 +54,15 @@ export interface IInstallOpts {
54
54
  }
55
55
  /** Result of {@link installPackageFromBundles}. */
56
56
  export interface IInstallResult {
57
+ /** Non-fatal SDK compatibility warning for a folder install. */
58
+ warning?: string;
57
59
  packageVersion: IPackageVersion;
58
60
  loaded: IPeersPackage | undefined;
59
61
  /**
60
62
  * True when the on-disk bundles match the existing PackageVersion hash and this
61
63
  * call persisted no new appNavs. Bundle files and the PackageVersion row are not
62
- * rewritten; the Packages row and device activation may still be updated.
64
+ * rewritten; for a dev version the Packages row and device activation may still
65
+ * be updated. A matching beta/stable version causes no writes or loading.
63
66
  */
64
67
  unchanged: boolean;
65
68
  }
@@ -109,11 +112,13 @@ export interface ITemplateCacheResult {
109
112
  }
110
113
  /** Result of {@link createPackage}. */
111
114
  export interface ICreatePackageResult {
115
+ /** Non-fatal SDK compatibility warning from installing the folder's bundles. */
116
+ warning?: string;
112
117
  /** New or imported package ID. */
113
118
  packageId: string;
114
119
  /** On-disk package directory. */
115
120
  localPath: string;
116
- /** Installed local/dev package version record. */
121
+ /** Installed build, or an identical already-released version. */
117
122
  packageVersion: IPackageVersion;
118
123
  /**
119
124
  * Present when the default template cache could not be refreshed and a
@@ -136,12 +141,14 @@ export interface IClonePackageOpts {
136
141
  }
137
142
  /** Result of {@link clonePackage}. */
138
143
  export interface IClonePackageResult {
144
+ /** Non-fatal SDK compatibility warning from installing the folder's bundles. */
145
+ warning?: string;
139
146
  /** Imported package ID from the cloned repo. */
140
147
  packageId: string;
141
148
  /** On-disk clone directory. */
142
149
  localPath: string;
143
150
  /** Git URL that was cloned. */
144
151
  remoteRepo: string;
145
- /** Installed local/dev package version record. */
152
+ /** Installed build, or an identical already-released version. */
146
153
  packageVersion: IPackageVersion;
147
154
  }
@@ -0,0 +1,2 @@
1
+ /** Version of this installed SDK, included by host bundlers from its package metadata. */
2
+ export declare const SDK_VERSION: string;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDK_VERSION = void 0;
4
+ /** Version of this installed SDK, included by host bundlers from its package metadata. */
5
+ exports.SDK_VERSION = require("../package.json").version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.28.7",
3
+ "version": "0.28.8",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"