@synap-core/workspace-templates 0.5.0 → 0.6.1
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 +734 -17
- package/dist/index.js +25031 -20
- package/dist/index.js.map +1 -0
- package/package.json +7 -5
- package/src/agent-fleet.yaml +16 -8
- package/src/blockchain-ecosystem.yaml +372 -0
- package/src/catalog.test.ts +156 -0
- package/src/catalog.ts +285 -0
- package/src/communication.yaml +689 -0
- package/src/content-os.yaml +252 -771
- package/src/crm.yaml +181 -2071
- package/src/define.ts +292 -6
- package/src/dev-dashboard.yaml +3 -0
- package/src/ecosystem.yaml +478 -0
- package/src/enterprise-os.yaml +183 -0
- package/src/foundation.yaml +226 -0
- package/src/grants.yaml +1096 -0
- package/src/hr.yaml +6 -0
- package/src/index.ts +32 -5
- package/src/internal-runbook.yaml +499 -0
- package/src/legal.yaml +6 -6
- package/src/marketing.yaml +48 -12
- package/src/operations.yaml +146 -420
- package/src/social.yaml +246 -0
- package/src/templates.ts +12812 -12260
- package/src/the-arch.yaml +182 -0
- package/src/types.ts +105 -0
- package/dist/define.d.ts +0 -97
- package/dist/define.d.ts.map +0 -1
- package/dist/define.js +0 -91
- package/dist/index.d.ts.map +0 -1
- package/dist/templates.d.ts +0 -7
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -24237
- package/dist/types.d.ts +0 -350
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -8
- package/src/radar.yaml +0 -297
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mergeCatalog — the ONE merge door for the workspace-template catalog.
|
|
3
|
+
* ====================================================================
|
|
4
|
+
*
|
|
5
|
+
* Both marketplace entry points (browser + CLI) and the pod need to show ONE
|
|
6
|
+
* list of installable workspace templates drawn from TWO sources:
|
|
7
|
+
*
|
|
8
|
+
* 1. BUNDLED — the canonical `@synap-core/workspace-templates` package that
|
|
9
|
+
* ships inside every client (this package). Always present, zero network.
|
|
10
|
+
* 2. REMOTE — rows in the control-plane package registry (`GET /api/packages`),
|
|
11
|
+
* which includes both the official templates re-published by
|
|
12
|
+
* `seed-templates.ts` AND private / third-party packages that never shipped
|
|
13
|
+
* in the bundle.
|
|
14
|
+
*
|
|
15
|
+
* The two overlap: every official template exists in BOTH sources (once bundled,
|
|
16
|
+
* once seeded to the CP). Naively concatenating them shows 21 duplicate cards.
|
|
17
|
+
* `mergeCatalog` de-dupes them into ONE catalog with a deterministic winner.
|
|
18
|
+
*
|
|
19
|
+
* WHY THIS LIVES HERE (and is PURE):
|
|
20
|
+
* This is the ONLY module the root `pnpm-workspace.yaml` lets `synap-cli`,
|
|
21
|
+
* `browser`, `synap-control-plane-api`, AND `synap-backend/packages/api` all
|
|
22
|
+
* resolve. `@synap-core/marketplace` pulls React and is `private: true`, so the
|
|
23
|
+
* Node CLI cannot import it. Keep this file pure data — no fetch, no auth, no
|
|
24
|
+
* React, no Node-only APIs. Each entry point has a DIFFERENT transport (browser
|
|
25
|
+
* cookie/`cpFetch`, CLI Bearer, pod anonymous); a door that fetched would force
|
|
26
|
+
* ONE auth model on all three and become the next lossy seam. Transport lives
|
|
27
|
+
* in the consumers; this door only merges arrays they already fetched.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { WorkspaceYaml } from "./types.js";
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Inputs
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A bundled template — the canonical `WorkspaceYaml` this package ships. Callers
|
|
38
|
+
* pass `listPublicTemplates()` (18) or `listWorkspaceTemplates()` (21); the door
|
|
39
|
+
* merges whatever it is given and does NOT filter (see the publicity note below).
|
|
40
|
+
*/
|
|
41
|
+
export type BundledEntry = WorkspaceYaml;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A control-plane registry row, mapped into the minimal shape the door needs.
|
|
45
|
+
* Consumers build this from a `MarketplacePackage` (browser) / the CP
|
|
46
|
+
* `GET /api/packages` JSON (CLI) — a thin, transport-specific projection.
|
|
47
|
+
*
|
|
48
|
+
* `definition` is carried OPAQUE: it is both (a) the install payload the consumer
|
|
49
|
+
* feeds back to the pod, and (b) the home of the ONE comparable version signal,
|
|
50
|
+
* `definition.sourcePackage.version` — the real npm semver of the published
|
|
51
|
+
* `@synap-core/workspace-templates` build that generated an official row
|
|
52
|
+
* (`seed-templates.ts` stamps it). The door only reads that one nested field; the
|
|
53
|
+
* rest of `definition` is passed through untouched.
|
|
54
|
+
*/
|
|
55
|
+
export interface RemoteEntry {
|
|
56
|
+
slug: string;
|
|
57
|
+
name: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
/** CP package category / type (e.g. `"workspace"`, `"view"`). */
|
|
60
|
+
category?: string;
|
|
61
|
+
tags?: string[];
|
|
62
|
+
icon?: string | null;
|
|
63
|
+
/**
|
|
64
|
+
* `true` when the CP row is not publicly listed (`isPublic === false`). Browse
|
|
65
|
+
* rows leave this `undefined` BY CONSTRUCTION — the browse route hard-filters
|
|
66
|
+
* `WHERE is_public = true`, so every browse row is public; `undefined` ⇒ public,
|
|
67
|
+
* never "unknown-so-hide".
|
|
68
|
+
*/
|
|
69
|
+
isPrivate?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The CP `definition` jsonb — opaque install payload. The door reads only
|
|
72
|
+
* `definition.sourcePackage.version` (the winner signal). Absent on browse rows
|
|
73
|
+
* (the list route strips `definition`), which is fine: absence ⇒ bundle wins.
|
|
74
|
+
*/
|
|
75
|
+
definition?: {
|
|
76
|
+
sourcePackage?: { name?: string; version?: string };
|
|
77
|
+
[k: string]: unknown;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether the caller could reach / authenticate against the remote source. */
|
|
82
|
+
export type RemoteSourceStatus = "ok" | "unauthenticated" | "unreachable";
|
|
83
|
+
|
|
84
|
+
export interface MergeCatalogInput {
|
|
85
|
+
/** Bundled templates (this package). Always "ok" — it is in-process data. */
|
|
86
|
+
bundled: BundledEntry[];
|
|
87
|
+
/** Already-fetched CP rows. Empty/omitted when the fetch failed. */
|
|
88
|
+
remote?: RemoteEntry[];
|
|
89
|
+
/**
|
|
90
|
+
* This client's bundled-package version (`package.json` `version`, live `0.6.0`).
|
|
91
|
+
* Compared against each remote row's `sourcePackage.version` to decide whether
|
|
92
|
+
* THIS client's bundle is stale. Callers pass the compiled-in package version.
|
|
93
|
+
*/
|
|
94
|
+
bundleVersion: string;
|
|
95
|
+
/**
|
|
96
|
+
* The transport's verdict on the remote fetch. `"ok"` (default) ⇒ merge the
|
|
97
|
+
* `remote` array. `"unauthenticated"` / `"unreachable"` ⇒ ignore `remote` and
|
|
98
|
+
* surface the degradation in `sources.remote`, so the consumer can render
|
|
99
|
+
* "18 available — private ones need login" instead of FAILING OPEN EMPTY.
|
|
100
|
+
*/
|
|
101
|
+
remoteStatus?: RemoteSourceStatus;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Output
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
interface CatalogEntryBase {
|
|
109
|
+
slug: string;
|
|
110
|
+
name: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
/** Package category / type. Bundled templates are always `"workspace"`. */
|
|
113
|
+
category?: string;
|
|
114
|
+
tags: string[];
|
|
115
|
+
icon?: string | null;
|
|
116
|
+
/** `true` ⇒ not publicly listed (internal bundled template, or private CP row). */
|
|
117
|
+
isPrivate: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A catalog entry backed by a bundled template. Install via `toWorkspaceDefinition()` / `toPackageDefinition()` on `template`. */
|
|
121
|
+
export interface BundledCatalogEntry extends CatalogEntryBase {
|
|
122
|
+
source: "bundled";
|
|
123
|
+
template: WorkspaceYaml;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A catalog entry backed by a CP registry row. Install from `remote.definition`. */
|
|
127
|
+
export interface RemoteCatalogEntry extends CatalogEntryBase {
|
|
128
|
+
source: "remote";
|
|
129
|
+
remote: RemoteEntry;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** One merged card. `source` discriminates the install path; common fields feed a picker. */
|
|
133
|
+
export type CatalogEntry = BundledCatalogEntry | RemoteCatalogEntry;
|
|
134
|
+
|
|
135
|
+
export interface MergeCatalogResult {
|
|
136
|
+
entries: CatalogEntry[];
|
|
137
|
+
/**
|
|
138
|
+
* Per-source health. LOAD-BEARING, not decoration: it is what lets a consumer
|
|
139
|
+
* distinguish "remote returned nothing" from "remote unreachable" and avoid the
|
|
140
|
+
* FAILS-OPEN-EMPTY trap (agent sees empty catalog → thinks pod is empty → dupes).
|
|
141
|
+
*/
|
|
142
|
+
sources: { bundled: "ok"; remote: RemoteSourceStatus };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Winner signal
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
function parseSemver(v: string | undefined): [number, number, number] | null {
|
|
150
|
+
if (!v) return null;
|
|
151
|
+
const m = /^\s*v?(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
152
|
+
if (!m) return null; // e.g. a range string ("range:>=0.6.0 <1.0.0") — not a version
|
|
153
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Is the remote row STRICTLY newer than this client's bundle? Only then does the
|
|
158
|
+
* remote win a slug conflict — it means THIS client is stale and the CP has a
|
|
159
|
+
* fresher official build. Every other case (equal, older, unparseable, or no
|
|
160
|
+
* `sourcePackage` at all) ⇒ `false` ⇒ BUNDLE WINS. That default is correct four
|
|
161
|
+
* ways, strongest last: (1) the CP is provably staler in the common case; (2) the
|
|
162
|
+
* CP row is a lossy re-encode of the bundle; (3) zero network; (4) the pod's boot
|
|
163
|
+
* reconcile already converges every workspace to the BUNDLED shape — bundle-wins
|
|
164
|
+
* agrees with the reconciler; CP-wins would fight it every boot.
|
|
165
|
+
*/
|
|
166
|
+
function remoteIsStrictlyNewer(
|
|
167
|
+
remoteVersion: string | undefined,
|
|
168
|
+
bundleVersion: string,
|
|
169
|
+
): boolean {
|
|
170
|
+
const r = parseSemver(remoteVersion);
|
|
171
|
+
const b = parseSemver(bundleVersion);
|
|
172
|
+
if (!r || !b) return false;
|
|
173
|
+
for (let i = 0; i < 3; i++) {
|
|
174
|
+
if (r[i] > b[i]) return true;
|
|
175
|
+
if (r[i] < b[i]) return false;
|
|
176
|
+
}
|
|
177
|
+
return false; // equal ⇒ not strictly newer ⇒ bundle wins
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Mapping
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
function bundledToEntry(tpl: WorkspaceYaml): BundledCatalogEntry {
|
|
185
|
+
return {
|
|
186
|
+
source: "bundled",
|
|
187
|
+
slug: tpl.meta.slug,
|
|
188
|
+
name: tpl.meta.name,
|
|
189
|
+
description: tpl.meta.description,
|
|
190
|
+
// Every bundled template is a full workspace — the CP calls this category
|
|
191
|
+
// "workspace" (PACKAGE_TYPES). Finer display grouping is derived from `tags`
|
|
192
|
+
// by the consumer (browser `categoryFor`), not baked in here.
|
|
193
|
+
category: "workspace",
|
|
194
|
+
tags: tpl.meta.tags ?? [],
|
|
195
|
+
icon: tpl.meta.icon,
|
|
196
|
+
isPrivate: tpl.meta.isPublic === false,
|
|
197
|
+
template: tpl,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function remoteToEntry(row: RemoteEntry): RemoteCatalogEntry {
|
|
202
|
+
return {
|
|
203
|
+
source: "remote",
|
|
204
|
+
slug: row.slug,
|
|
205
|
+
name: row.name,
|
|
206
|
+
description: row.description,
|
|
207
|
+
category: row.category,
|
|
208
|
+
tags: row.tags ?? [],
|
|
209
|
+
icon: row.icon ?? null,
|
|
210
|
+
isPrivate: row.isPrivate ?? false,
|
|
211
|
+
remote: row,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// The door
|
|
217
|
+
// ============================================================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Merge bundled + remote template catalogs into ONE de-duplicated list.
|
|
221
|
+
*
|
|
222
|
+
* DEDUPE KEY = `slug` — NOT `slug@version`. This is forced by the live data, not
|
|
223
|
+
* a preference: every CP flagship row is hardcoded `version: "1.0.0"`
|
|
224
|
+
* (`seed-templates.ts`) and `TemplateMeta` carries NO version field at all, so a
|
|
225
|
+
* `slug@version` key would collide on nothing → 21 duplicate cards. The real
|
|
226
|
+
* per-build version lives only in `definition.sourcePackage.version`, which is
|
|
227
|
+
* the WINNER signal, never the identity key.
|
|
228
|
+
*
|
|
229
|
+
* CONFLICT RESOLUTION (remote slug already claimed by a bundled template):
|
|
230
|
+
* • remote `sourcePackage.version` strictly > `bundleVersion` → remote wins
|
|
231
|
+
* (this client is stale — the CP has a fresher official build).
|
|
232
|
+
* • otherwise (equal / older / no `sourcePackage`) → BUNDLE wins. The
|
|
233
|
+
* no-`sourcePackage` case is a third-party package SQUATTING an official slug;
|
|
234
|
+
* the bundle wins unconditionally (squatting defense).
|
|
235
|
+
*
|
|
236
|
+
* A remote slug that does NOT collide with any bundled template is a DISTINCT
|
|
237
|
+
* entry (a private or third-party package) and is always kept.
|
|
238
|
+
*/
|
|
239
|
+
export function mergeCatalog(input: MergeCatalogInput): MergeCatalogResult {
|
|
240
|
+
const { bundled, bundleVersion } = input;
|
|
241
|
+
const remoteStatus: RemoteSourceStatus = input.remoteStatus ?? "ok";
|
|
242
|
+
const remote = input.remote ?? [];
|
|
243
|
+
|
|
244
|
+
// Insertion-ordered map keyed by slug. Bundled entries seed it first, so they
|
|
245
|
+
// are the default winner; a qualifying remote row overwrites in place.
|
|
246
|
+
const bySlug = new Map<string, CatalogEntry>();
|
|
247
|
+
|
|
248
|
+
for (const tpl of bundled) {
|
|
249
|
+
bySlug.set(tpl.meta.slug, bundledToEntry(tpl));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Only fold in remote rows when the transport actually reached the source.
|
|
253
|
+
// A failed fetch must NOT silently look like "the CP has no templates".
|
|
254
|
+
if (remoteStatus === "ok") {
|
|
255
|
+
for (const row of remote) {
|
|
256
|
+
const existing = bySlug.get(row.slug);
|
|
257
|
+
|
|
258
|
+
if (!existing) {
|
|
259
|
+
// No bundled template owns this slug → distinct (private / third-party).
|
|
260
|
+
bySlug.set(row.slug, remoteToEntry(row));
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (existing.source !== "bundled") {
|
|
265
|
+
// Two remote rows with the same slug (shouldn't happen — CP slugs are
|
|
266
|
+
// unique) — keep the first, defensively.
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Slug conflict with an official bundled template. Remote wins ONLY if it
|
|
271
|
+
// is a strictly-newer official build; else the bundle stays (older, equal,
|
|
272
|
+
// or a squatting third-party with no sourcePackage).
|
|
273
|
+
const remoteVersion = row.definition?.sourcePackage?.version;
|
|
274
|
+
if (remoteIsStrictlyNewer(remoteVersion, bundleVersion)) {
|
|
275
|
+
bySlug.set(row.slug, remoteToEntry(row));
|
|
276
|
+
}
|
|
277
|
+
// else: no-op — bundled entry already in place wins.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
entries: [...bySlug.values()],
|
|
283
|
+
sources: { bundled: "ok", remote: remoteStatus },
|
|
284
|
+
};
|
|
285
|
+
}
|