@synap-core/workspace-templates 0.6.3 → 0.6.4
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/index.d.ts +43 -1
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/catalog.test.ts +99 -1
- package/src/catalog.ts +93 -0
- package/src/index.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synap-core/workspace-templates",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "pnpm@9.15.4",
|
|
6
6
|
"description": "Canonical workspace definitions — YAML source + typed exports for browser, backend, CLI, and marketplace",
|
package/src/catalog.test.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { test } from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { mergeCatalog, type RemoteEntry } from "./catalog.js";
|
|
10
|
+
import { mergeCatalog, computeTemplateUpdates, type RemoteEntry } from "./catalog.js";
|
|
11
11
|
import type { WorkspaceYaml } from "./types.js";
|
|
12
12
|
|
|
13
13
|
const BUNDLE_VERSION = "0.6.0";
|
|
@@ -154,3 +154,101 @@ test("maps a bundled template's meta fields onto the entry", () => {
|
|
|
154
154
|
assert.equal(e.category, "workspace");
|
|
155
155
|
assert.equal(e.isPrivate, true, "isPublic:false ⇒ isPrivate");
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
// ── computeTemplateUpdates ───────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
test("computeTemplateUpdates: no update when installed equals catalog latest (semver)", () => {
|
|
161
|
+
const { entries } = mergeCatalog({
|
|
162
|
+
bundled: [bundled("crm")],
|
|
163
|
+
remote: [remote("crm", { version: "0.7.0" })],
|
|
164
|
+
bundleVersion: BUNDLE_VERSION, // remote strictly newer → remote wins, carries "0.7.0"
|
|
165
|
+
});
|
|
166
|
+
const [check] = computeTemplateUpdates([{ slug: "crm", version: "0.7.0" }], entries);
|
|
167
|
+
assert.equal(check.latestVersion, "0.7.0");
|
|
168
|
+
assert.equal(check.updateAvailable, false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("computeTemplateUpdates: update available when installed is older (semver)", () => {
|
|
172
|
+
const { entries } = mergeCatalog({
|
|
173
|
+
bundled: [bundled("crm")],
|
|
174
|
+
remote: [remote("crm", { version: "0.7.0" })],
|
|
175
|
+
bundleVersion: BUNDLE_VERSION,
|
|
176
|
+
});
|
|
177
|
+
const [check] = computeTemplateUpdates([{ slug: "crm", version: "0.5.0" }], entries);
|
|
178
|
+
assert.equal(check.latestVersion, "0.7.0");
|
|
179
|
+
assert.equal(check.updateAvailable, true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// NOTE: uses a non-colliding slug so the remote row is kept as a DISTINCT entry
|
|
183
|
+
// regardless of version — a hash string never parses as semver, so on a slug
|
|
184
|
+
// COLLISION `remoteIsStrictlyNewer` would (correctly, per mergeCatalog's existing
|
|
185
|
+
// squatting-defense semantics) keep the bundle winning and never surface the hash.
|
|
186
|
+
test("computeTemplateUpdates: hash-vs-semver — any mismatch with a hash version signals an update", () => {
|
|
187
|
+
const { entries } = mergeCatalog({
|
|
188
|
+
bundled: [bundled("crm")],
|
|
189
|
+
remote: [remote("hash-template", { version: "h-abc123" })],
|
|
190
|
+
bundleVersion: BUNDLE_VERSION,
|
|
191
|
+
});
|
|
192
|
+
const [staleSemver] = computeTemplateUpdates(
|
|
193
|
+
[{ slug: "hash-template", version: "0.5.0" }],
|
|
194
|
+
entries,
|
|
195
|
+
);
|
|
196
|
+
assert.equal(staleSemver.latestVersion, "h-abc123");
|
|
197
|
+
assert.equal(staleSemver.updateAvailable, true, "semver installed vs hash latest ⇒ differ ⇒ update");
|
|
198
|
+
|
|
199
|
+
const [sameHash] = computeTemplateUpdates(
|
|
200
|
+
[{ slug: "hash-template", version: "h-abc123" }],
|
|
201
|
+
entries,
|
|
202
|
+
);
|
|
203
|
+
assert.equal(sameHash.updateAvailable, false, "identical hash ⇒ no update");
|
|
204
|
+
|
|
205
|
+
const [differentHash] = computeTemplateUpdates(
|
|
206
|
+
[{ slug: "hash-template", version: "h-old999" }],
|
|
207
|
+
entries,
|
|
208
|
+
);
|
|
209
|
+
assert.equal(differentHash.updateAvailable, true, "different hash ⇒ update");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("computeTemplateUpdates: slug missing from catalog ⇒ no update, latestVersion undefined", () => {
|
|
213
|
+
const { entries } = mergeCatalog({
|
|
214
|
+
bundled: [bundled("crm")],
|
|
215
|
+
remote: [],
|
|
216
|
+
bundleVersion: BUNDLE_VERSION,
|
|
217
|
+
});
|
|
218
|
+
const [check] = computeTemplateUpdates([{ slug: "ghost-template", version: "1.0.0" }], entries);
|
|
219
|
+
assert.equal(check.latestVersion, undefined);
|
|
220
|
+
assert.equal(check.updateAvailable, false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("computeTemplateUpdates: bundled winner compares against bundleVersion when passed", () => {
|
|
224
|
+
const { entries } = mergeCatalog({
|
|
225
|
+
bundled: [bundled("crm")],
|
|
226
|
+
remote: [],
|
|
227
|
+
bundleVersion: BUNDLE_VERSION,
|
|
228
|
+
});
|
|
229
|
+
const stale = computeTemplateUpdates(
|
|
230
|
+
[{ slug: "crm", version: "0.1.0" }],
|
|
231
|
+
entries,
|
|
232
|
+
BUNDLE_VERSION,
|
|
233
|
+
);
|
|
234
|
+
assert.equal(stale[0].latestVersion, BUNDLE_VERSION);
|
|
235
|
+
assert.equal(stale[0].updateAvailable, true);
|
|
236
|
+
|
|
237
|
+
const noBundleVersion = computeTemplateUpdates([{ slug: "crm", version: "0.1.0" }], entries);
|
|
238
|
+
assert.equal(noBundleVersion[0].latestVersion, undefined, "no bundleVersion ⇒ nothing to compare");
|
|
239
|
+
assert.equal(noBundleVersion[0].updateAvailable, false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("computeTemplateUpdates: passes isPrivate through from the winning entry", () => {
|
|
243
|
+
const { entries } = mergeCatalog({
|
|
244
|
+
bundled: [bundled("crm")],
|
|
245
|
+
remote: [remote("private-pack", { isPrivate: true, version: "1.0.0" })],
|
|
246
|
+
bundleVersion: BUNDLE_VERSION,
|
|
247
|
+
});
|
|
248
|
+
const [check] = computeTemplateUpdates(
|
|
249
|
+
[{ slug: "private-pack", version: "0.9.0" }],
|
|
250
|
+
entries,
|
|
251
|
+
);
|
|
252
|
+
assert.equal(check.isPrivate, true);
|
|
253
|
+
assert.equal(check.updateAvailable, true);
|
|
254
|
+
});
|
package/src/catalog.ts
CHANGED
|
@@ -371,3 +371,96 @@ export function assembleCatalog(input: AssembleCatalogInput): MergeCatalogResult
|
|
|
371
371
|
remoteStatus: input.remoteStatus,
|
|
372
372
|
});
|
|
373
373
|
}
|
|
374
|
+
|
|
375
|
+
// ============================================================================
|
|
376
|
+
// Installed-vs-catalog update check
|
|
377
|
+
// ============================================================================
|
|
378
|
+
|
|
379
|
+
/** A template a pod has installed — the door reads `settings.packageVersion` off the workspace row. */
|
|
380
|
+
export interface InstalledTemplate {
|
|
381
|
+
slug: string;
|
|
382
|
+
version: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Per-slug verdict: is a newer catalog version available than what's installed? */
|
|
386
|
+
export interface TemplateUpdateCheck {
|
|
387
|
+
slug: string;
|
|
388
|
+
installedVersion: string;
|
|
389
|
+
/** `undefined` when the slug has no matching catalog entry, or a bundled winner and no `bundleVersion` was passed. */
|
|
390
|
+
latestVersion: string | undefined;
|
|
391
|
+
updateAvailable: boolean;
|
|
392
|
+
/** Passed through from the winning catalog entry, so callers can route private updates through the authed path. */
|
|
393
|
+
isPrivate?: boolean;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* `true` when `version` is a CP content-hash stamp (`"h-<hash>"`) rather than semver.
|
|
398
|
+
* Content-hash versions are NOT semver-comparable — see `computeTemplateUpdates`.
|
|
399
|
+
*/
|
|
400
|
+
export function isHashVersion(version: string | undefined): boolean {
|
|
401
|
+
return typeof version === "string" && version.startsWith("h-");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Compare an installed version against a catalog's latest — reusing
|
|
406
|
+
* `remoteIsStrictlyNewer` for the semver path. Either side being a content-hash
|
|
407
|
+
* (`"h-..."`) makes the pair NOT semver-comparable, so an update is signalled
|
|
408
|
+
* whenever the two raw strings simply differ (equal ⇒ no update, by construction
|
|
409
|
+
* the CP only reissues a new hash when the template content actually changed).
|
|
410
|
+
*/
|
|
411
|
+
function isNewerVersion(installedVersion: string, latestVersion: string | undefined): boolean {
|
|
412
|
+
if (latestVersion === undefined) return false;
|
|
413
|
+
if (isHashVersion(installedVersion) || isHashVersion(latestVersion)) {
|
|
414
|
+
return installedVersion !== latestVersion;
|
|
415
|
+
}
|
|
416
|
+
return remoteIsStrictlyNewer(latestVersion, installedVersion);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Compute, per installed template, whether the CATALOG (bundled + remote,
|
|
421
|
+
* already merged by `mergeCatalog`/`assembleCatalog`) carries a newer version
|
|
422
|
+
* than what the pod installed — the shared "update available" check all three
|
|
423
|
+
* doors (browser, synap-app, CLI) reuse instead of re-deriving it.
|
|
424
|
+
*
|
|
425
|
+
* `installed` and `catalog` are BOTH already-fetched/computed by the caller —
|
|
426
|
+
* this stays PURE, same as the rest of this module (no fetch, no auth).
|
|
427
|
+
*
|
|
428
|
+
* The catalog's winning entry carries the latest version signal two ways:
|
|
429
|
+
* • `source: "remote"` → `remote.definition?.sourcePackage?.version` (semver
|
|
430
|
+
* OR, going forward, a CP content-hash `"h-<hash>"` stamp).
|
|
431
|
+
* • `source: "bundled"` → bundled entries carry no version field at all
|
|
432
|
+
* (`TemplateMeta` has none); pass `bundleVersion` (this client's compiled
|
|
433
|
+
* package version, the same value fed to `mergeCatalog`) to compare against
|
|
434
|
+
* it. Omit `bundleVersion` and bundled-backed slugs report `latestVersion:
|
|
435
|
+
* undefined, updateAvailable: false` (nothing comparable).
|
|
436
|
+
*
|
|
437
|
+
* A slug with no matching catalog entry ⇒ `updateAvailable: false`,
|
|
438
|
+
* `latestVersion: undefined` — there is nothing to update TO.
|
|
439
|
+
*/
|
|
440
|
+
export function computeTemplateUpdates(
|
|
441
|
+
installed: InstalledTemplate[],
|
|
442
|
+
catalog: CatalogEntry[],
|
|
443
|
+
bundleVersion?: string,
|
|
444
|
+
): TemplateUpdateCheck[] {
|
|
445
|
+
const bySlug = new Map(catalog.map((entry) => [entry.slug, entry]));
|
|
446
|
+
|
|
447
|
+
return installed.map(({ slug, version: installedVersion }) => {
|
|
448
|
+
const entry = bySlug.get(slug);
|
|
449
|
+
if (!entry) {
|
|
450
|
+
return { slug, installedVersion, latestVersion: undefined, updateAvailable: false };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const latestVersion =
|
|
454
|
+
entry.source === "remote"
|
|
455
|
+
? entry.remote.definition?.sourcePackage?.version
|
|
456
|
+
: bundleVersion;
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
slug,
|
|
460
|
+
installedVersion,
|
|
461
|
+
latestVersion,
|
|
462
|
+
updateAvailable: isNewerVersion(installedVersion, latestVersion),
|
|
463
|
+
isPrivate: entry.isPrivate,
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -61,7 +61,13 @@ export {
|
|
|
61
61
|
|
|
62
62
|
export type { WorkspaceTemplateSlug } from "./templates.js";
|
|
63
63
|
|
|
64
|
-
export {
|
|
64
|
+
export {
|
|
65
|
+
mergeCatalog,
|
|
66
|
+
rowToRemoteEntry,
|
|
67
|
+
assembleCatalog,
|
|
68
|
+
computeTemplateUpdates,
|
|
69
|
+
isHashVersion,
|
|
70
|
+
} from "./catalog.js";
|
|
65
71
|
export type {
|
|
66
72
|
MergeCatalogInput,
|
|
67
73
|
MergeCatalogResult,
|
|
@@ -73,6 +79,8 @@ export type {
|
|
|
73
79
|
RemoteCatalogEntry,
|
|
74
80
|
CpPackageRow,
|
|
75
81
|
AssembleCatalogInput,
|
|
82
|
+
InstalledTemplate,
|
|
83
|
+
TemplateUpdateCheck,
|
|
76
84
|
} from "./catalog.js";
|
|
77
85
|
|
|
78
86
|
export {
|