@tokenoftrust/cli 2.0.28 → 2.1.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/bin/tot.mjs +12 -0
- package/package.json +1 -1
- package/src/capability-registry.mjs +295 -0
- package/src/commands/capabilities.mjs +120 -0
- package/src/commands/report.mjs +326 -0
- package/src/go-live-report.mjs +1064 -0
- package/src/readiness-checks.mjs +254 -0
package/bin/tot.mjs
CHANGED
|
@@ -56,6 +56,8 @@ const COMMANDS = [
|
|
|
56
56
|
{ invocation: "grants", blurb: "capability/tier/expiry per store you can act on" },
|
|
57
57
|
{ invocation: "clone [<tenant>]", blurb: "clone a store you can build on" },
|
|
58
58
|
{ invocation: "validate", blurb: "lint your store before you submit" },
|
|
59
|
+
{ invocation: "capabilities", blurb: "what the platform supports, refuses, and has not built yet" },
|
|
60
|
+
{ invocation: "report go-live --pages <dir>", blurb: "your store's go-live readiness report, from its page-check results" },
|
|
59
61
|
{ invocation: "dev", blurb: "run your store locally with save→reload" },
|
|
60
62
|
{ invocation: "preview", blurb: "push your store to a reviewable preview" },
|
|
61
63
|
{ invocation: "ship", blurb: "publish the tenant's current green aggregate live (plan → confirm → ship)" },
|
|
@@ -175,6 +177,16 @@ async function dispatch(cmd, rest, ctx) {
|
|
|
175
177
|
return run(rest, ctx);
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
if (cmd === "capabilities") {
|
|
181
|
+
const { run } = await import("../src/commands/capabilities.mjs");
|
|
182
|
+
return run(rest, ctx);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (cmd === "report") {
|
|
186
|
+
const { run } = await import("../src/commands/report.mjs");
|
|
187
|
+
return run(rest, ctx);
|
|
188
|
+
}
|
|
189
|
+
|
|
178
190
|
if (cmd === "dev") {
|
|
179
191
|
const { run } = await import("../src/commands/dev.mjs");
|
|
180
192
|
return run(rest, ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Token of Trust developer CLI — clone a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Token of Trust",
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the platform supports, what it refuses, and what it has not built yet.
|
|
3
|
+
*
|
|
4
|
+
* A migration constantly needs three answers, and until this file none were available without
|
|
5
|
+
* reading platform source:
|
|
6
|
+
*
|
|
7
|
+
* 1. Does the platform already do X, so I should wire it up rather than build it per-tenant?
|
|
8
|
+
* 2. If so, what populates it and what proves it?
|
|
9
|
+
* 3. If not — is that a GAP worth filing, or a REFUSAL I should stop asking for?
|
|
10
|
+
*
|
|
11
|
+
* THE THIRD TIER IS THE POINT. A registry that lists only what exists teaches nothing about the
|
|
12
|
+
* boundaries, so migrators keep requesting things that will never be granted — and the incautious
|
|
13
|
+
* hand-roll the refused thing locally, because nothing said no. `deliberately-refused` carries its
|
|
14
|
+
* reason for exactly that reason.
|
|
15
|
+
*
|
|
16
|
+
* NOT AUTHORITATIVE BY ASSERTION. Every entry names the file that implements it, and
|
|
17
|
+
* the CLI's capability-registry test proves those paths exist in the platform repository. They are
|
|
18
|
+
* the register's anti-drift proof, not part of what `tot capabilities` prints. A capability whose implementation is
|
|
19
|
+
* deleted or moved fails the suite rather than becoming a doc that lies — which is worse than no
|
|
20
|
+
* doc, because it is trusted.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} Capability
|
|
25
|
+
* @property {string} id stable slug; the argument to `tot capabilities --id`
|
|
26
|
+
* @property {string} label how a human refers to it
|
|
27
|
+
* @property {tier} tier what kind of support this is — see below
|
|
28
|
+
* @property {string} summary one line, in a migrator's terms
|
|
29
|
+
* @property {string[]} implementedBy repo-relative paths that must exist (verified by test)
|
|
30
|
+
* @property {string} [populatedBy] the migration step that fills it, when it carries data
|
|
31
|
+
* @property {string} [verifiedBy] the gate that proves it, so a red gate can cite the capability
|
|
32
|
+
* @property {siteType[]} appliesTo tenant shapes it is relevant to
|
|
33
|
+
* @property {string} [because] REQUIRED for deliberately-refused: why, so nobody re-asks
|
|
34
|
+
* @property {string} [tracking] REQUIRED for not-yet: the filed report, so it is not re-filed
|
|
35
|
+
*
|
|
36
|
+
* @typedef {'platform-core'|'app'|'plugin'|'deliberately-refused'|'not-yet'} tier
|
|
37
|
+
* @typedef {'commerce'|'marketing'} siteType
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** @type {readonly Capability[]} */
|
|
41
|
+
export const CAPABILITIES = [
|
|
42
|
+
// ---- platform-core: already built, wire it up rather than rebuilding per tenant ----
|
|
43
|
+
{
|
|
44
|
+
id: 'product-reviews',
|
|
45
|
+
label: 'Product reviews and ratings',
|
|
46
|
+
tier: 'platform-core',
|
|
47
|
+
summary:
|
|
48
|
+
'Per-product reviews and aggregate rating, harvested from the merchant\'s existing reviews '
|
|
49
|
+
+ 'app during migration and emitted as JSON-LD. Aggregates come ONLY from sourced numbers — '
|
|
50
|
+
+ 'scraped widget markup is refused and a half-populated aggregate is omitted entirely.',
|
|
51
|
+
implementedBy: [
|
|
52
|
+
'packages/public-runtime/src/product.ts',
|
|
53
|
+
'apps/storefront/src/lib/jsonld.ts',
|
|
54
|
+
'apps/storefront/src/lib/reviews.trust-proof.test.ts',
|
|
55
|
+
],
|
|
56
|
+
populatedBy: 'catalog.2 (admin enrichment) — reviews.rating / reviews.rating_count metafields',
|
|
57
|
+
appliesTo: ['commerce'],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'legacy-redirects',
|
|
61
|
+
label: 'Legacy URL redirects',
|
|
62
|
+
tier: 'platform-core',
|
|
63
|
+
summary:
|
|
64
|
+
'Declared old->new paths materialize into the PUBLISHED artifact and are served by the edge '
|
|
65
|
+
+ 'function on the public path. They correctly 404 on the preview/test channel — that is by '
|
|
66
|
+
+ 'design and is not a missing declaration.',
|
|
67
|
+
implementedBy: ['agency-kit/tools/validate-site.mjs'],
|
|
68
|
+
populatedBy: 'content/redirects.json in the tenant repo',
|
|
69
|
+
verifiedBy: 'Step 9 post-cutover crawl (not provable pre-cutover)',
|
|
70
|
+
appliesTo: ['commerce', 'marketing'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'theme-tokens',
|
|
74
|
+
label: 'Tenant theme tokens and style concept',
|
|
75
|
+
tier: 'platform-core',
|
|
76
|
+
summary:
|
|
77
|
+
'A tenant declares colour/type/shape tokens and a style concept; the platform supplies the '
|
|
78
|
+
+ 'component CSS that consumes them. Conversion-role tokens (cta, trust, surface-card) name '
|
|
79
|
+
+ 'the JOB a colour does so a tenant can move one without dragging the rest.',
|
|
80
|
+
implementedBy: [
|
|
81
|
+
'packages/public-runtime/src/tenant.ts',
|
|
82
|
+
'apps/storefront/src/styles/global.css',
|
|
83
|
+
'agency-kit/playbooks/style-concept.md',
|
|
84
|
+
],
|
|
85
|
+
populatedBy: 'Step 4.5 (theme + style guide + style concept)',
|
|
86
|
+
verifiedBy: 'style-concept-audit ecommerceStyleGuide / guideCoverage',
|
|
87
|
+
appliesTo: ['commerce', 'marketing'],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'compliance-notices',
|
|
91
|
+
label: 'Regulated-commerce compliance notices',
|
|
92
|
+
tier: 'platform-core',
|
|
93
|
+
summary:
|
|
94
|
+
'Age gate, PACT Act, Prop 65, adult-signature, excise-tax and shipping-restriction notices '
|
|
95
|
+
+ 'as declared components with real legal meaning — not free text.',
|
|
96
|
+
implementedBy: ['apps/storefront/src/components/compliance'],
|
|
97
|
+
verifiedBy: 'preview_status compliance checks (age_gate, excise, jurisdiction)',
|
|
98
|
+
appliesTo: ['commerce'],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'source-parity-baseline',
|
|
102
|
+
label: 'Source Lighthouse baseline',
|
|
103
|
+
tier: 'platform-core',
|
|
104
|
+
summary:
|
|
105
|
+
'The merchant\'s own pre-migration scores, captured before cutover, so parity can be judged '
|
|
106
|
+
+ 'and improvement proven. NOT re-derivable once the hostname moves.',
|
|
107
|
+
implementedBy: [
|
|
108
|
+
'agency-kit/tools/capture-source-baseline.mjs',
|
|
109
|
+
'docs/devkit-staging/tools/lib/lighthouse-parity.mjs',
|
|
110
|
+
],
|
|
111
|
+
populatedBy: 'Step 8 — capture-source-baseline.mjs, before cutover',
|
|
112
|
+
verifiedBy: 'perf gate parity verdicts (regression / parity / giveaway)',
|
|
113
|
+
appliesTo: ['commerce', 'marketing'],
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// ---- app: a reviewed integration the merchant selects ----
|
|
117
|
+
{
|
|
118
|
+
id: 'analytics-adapter',
|
|
119
|
+
label: 'Analytics via reviewed adapter',
|
|
120
|
+
tier: 'app',
|
|
121
|
+
summary:
|
|
122
|
+
'A tenant selects a REVIEWED adapter and supplies a public destination id. Ids are per '
|
|
123
|
+
+ 'stage — a non-live stage reusing the live id resolves the whole integration to nothing.',
|
|
124
|
+
implementedBy: ['docs/architecture/analytics-activation.md'],
|
|
125
|
+
populatedBy: 'analytics.json in the tenant repo (add-on stage: analytics)',
|
|
126
|
+
appliesTo: ['commerce', 'marketing'],
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// ---- deliberately-refused: stop asking, and do NOT hand-roll it locally ----
|
|
130
|
+
{
|
|
131
|
+
id: 'tag-container',
|
|
132
|
+
label: 'Tag manager / container snippet',
|
|
133
|
+
tier: 'deliberately-refused',
|
|
134
|
+
summary: 'Pasting a GTM-style container id or executable tag markup.',
|
|
135
|
+
because:
|
|
136
|
+
'A tenant never supplies executable markup, selectors, secrets, wildcard origins or a '
|
|
137
|
+
+ 'container id. Refusing all of them is what makes measurement possible on a REGULATED '
|
|
138
|
+
+ 'storefront at all. "Add this tracking snippet" is never "where do I paste it" — it is '
|
|
139
|
+
+ '"which reviewed adapter covers this destination", and if none does that is a reviewed '
|
|
140
|
+
+ 'platform change, not tenant content. Regulated tenants CAN run reviewed adapters, GA4 '
|
|
141
|
+
+ 'included; only uncertified elevated-parent code is refused.',
|
|
142
|
+
implementedBy: ['docs/architecture/analytics-activation.md'],
|
|
143
|
+
appliesTo: ['commerce', 'marketing'],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'fabricated-trust-claims',
|
|
147
|
+
label: 'Ratings, testimonials, badges or guarantees without evidence',
|
|
148
|
+
tier: 'deliberately-refused',
|
|
149
|
+
summary: 'Filling a trust component with a plausible number or quote to clear a gate.',
|
|
150
|
+
because:
|
|
151
|
+
'The theme claimsPolicy requires merchant-provided evidence and human approval for product '
|
|
152
|
+
+ 'claims, customer counts, ratings, testimonials, badges, guarantees and any health or '
|
|
153
|
+
+ 'cessation claim. On a regulated storefront a fabricated rating or badge is a compliance '
|
|
154
|
+
+ 'exposure, not a shortcut. The quality bar REFUSES approval while such components ship as '
|
|
155
|
+
+ 'placeholders, which is the enforcement, not merely the advice.',
|
|
156
|
+
implementedBy: ['docs/devkit-staging/tools/style-concept-audit.mjs'],
|
|
157
|
+
verifiedBy: 'style-concept-audit ecommerceStyleGuide.unsourcedComponents',
|
|
158
|
+
appliesTo: ['commerce'],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'origin-compute-public-path',
|
|
162
|
+
label: 'Origin compute in front of public pages',
|
|
163
|
+
tier: 'deliberately-refused',
|
|
164
|
+
summary: 'Putting the Worker in the public request path to compose a response per request.',
|
|
165
|
+
because:
|
|
166
|
+
'Public pages are CDN -> content-addressed storage with at most ONE submillisecond edge '
|
|
167
|
+
+ 'function. Google ranks on field Core Web Vitals from real visits, so origin latency on a '
|
|
168
|
+
+ 'cache miss is a direct commercial cost and every layer is another term in the uptime '
|
|
169
|
+
+ 'product. Nearly every such request is an undecided variant set: ask what N is. Bounded N '
|
|
170
|
+
+ 'is a cache-key dimension; per-user N is a static shell plus an authenticated hydrate. A '
|
|
171
|
+
+ 'genuine exception amends ADR 0013 rather than becoming a silent local decision.',
|
|
172
|
+
implementedBy: ['docs/architecture/decisions/0013-public-serving-contract.md'],
|
|
173
|
+
appliesTo: ['commerce', 'marketing'],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'tenant-data-in-platform-repo',
|
|
177
|
+
label: 'Committing a merchant\'s content or catalog to the platform repo',
|
|
178
|
+
tier: 'deliberately-refused',
|
|
179
|
+
summary: 'Storing tenant pages, catalog or analytics config in this monorepo.',
|
|
180
|
+
because:
|
|
181
|
+
'A tenant\'s content belongs in its own Gitea repo — the split is what makes extraction a '
|
|
182
|
+
+ 'subtree operation rather than a rescue. Three routes reach this repo and all three are '
|
|
183
|
+
+ 'guarded and fail closed; the third has no gitignore friction at all, so a plain `git add` '
|
|
184
|
+
+ 'silently succeeds. Existing entries are recorded exceptions with reasons, never a pattern '
|
|
185
|
+
+ 'to copy.',
|
|
186
|
+
implementedBy: ['scripts/tenant/check-no-tracked-real-tenants.mjs'],
|
|
187
|
+
verifiedBy: 'pnpm run check (tenant-data guard)',
|
|
188
|
+
appliesTo: ['commerce', 'marketing'],
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// ---- not-yet: a real gap, already filed; do not re-file ----
|
|
192
|
+
{
|
|
193
|
+
id: 'agent-readiness',
|
|
194
|
+
label: 'Agent readiness (WebMCP tools, agent accessibility tree)',
|
|
195
|
+
tier: 'not-yet',
|
|
196
|
+
summary:
|
|
197
|
+
'Whether an AI agent can actually shop the store — Lighthouse 13 scores this as its fifth '
|
|
198
|
+
+ 'category, agentic-browsing: agent accessibility tree, WebMCP registered tools, WebMCP '
|
|
199
|
+
+ 'schema validity, form coverage, CLS, llms.txt. We ship llms.txt (measured live and reported '
|
|
200
|
+
+ 'as advisory preview evidence); the '
|
|
201
|
+
+ 'WebMCP half has no implementation anywhere in the runtime. Validation measures the '
|
|
202
|
+
+ 'category, so the score is visible and the dimension is offerable as an add-on — but a '
|
|
203
|
+
+ 'good score needs the surface built.',
|
|
204
|
+
implementedBy: [
|
|
205
|
+
'agency-kit/tools/capture-source-baseline.mjs',
|
|
206
|
+
'docs/devkit-staging/tools/validate-page-browser.mjs',
|
|
207
|
+
'apps/storefront/src/lib/diag/evidence.ts',
|
|
208
|
+
],
|
|
209
|
+
populatedBy: 'Lighthouse agentic-browsing category, captured per page at Step 8',
|
|
210
|
+
verifiedBy: 'tot report go-live (the agent-readiness row)',
|
|
211
|
+
tracking: 'fb-1790020895985-emlrk6',
|
|
212
|
+
appliesTo: ['commerce'],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'commerce-sync-detection',
|
|
216
|
+
label: 'Automatic order-sync / product-sync status',
|
|
217
|
+
tier: 'not-yet',
|
|
218
|
+
summary:
|
|
219
|
+
'Whether a tenant\'s order sync and product sync are actually wired and exercised. These are '
|
|
220
|
+
+ 'builder-facing jobs on the PUBLIC TOT MCP, run against the merchant\'s existing store, so '
|
|
221
|
+
+ 'Step 8 cannot see them: it validates the storefront, not the integrations behind it. '
|
|
222
|
+
+ 'Until a status read exists, the go-live report carries them as recorded confirmations '
|
|
223
|
+
+ 'with explicit provenance, and an absent record reads as NOT CONFIRMED — never as passing.',
|
|
224
|
+
implementedBy: ['packages/cli/src/go-live-report.mjs'],
|
|
225
|
+
populatedBy: 'commerceSync in the store checkout\'s .tot/migration-state.json (hand-recorded today)',
|
|
226
|
+
tracking: 'fb-1790015097742-qk3m8w',
|
|
227
|
+
appliesTo: ['commerce'],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'site-social-proof',
|
|
231
|
+
label: 'Site-level social proof (aggregate rating, testimonials)',
|
|
232
|
+
tier: 'not-yet',
|
|
233
|
+
summary:
|
|
234
|
+
'A store-wide rating and an attributable testimonial type. Per-PRODUCT reviews are '
|
|
235
|
+
+ 'platform-core and well built; there is no equivalent for the whole store, so the style '
|
|
236
|
+
+ 'guide ships a placeholder the platform gives no honest way to fill. A real aggregate is '
|
|
237
|
+
+ 'derivable from catalog data already harvested.',
|
|
238
|
+
implementedBy: ['apps/storefront/src/components/commerce/ReviewSummary.astro'],
|
|
239
|
+
tracking: 'fb-1790007573300-xs1zcc',
|
|
240
|
+
appliesTo: ['commerce'],
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
/** Tiers whose support is real today — the ones a migration can wire up. */
|
|
245
|
+
export const SUPPORTED_TIERS = ['platform-core', 'app', 'plugin'];
|
|
246
|
+
|
|
247
|
+
/** @param {string} id */
|
|
248
|
+
export function capability(id) {
|
|
249
|
+
return CAPABILITIES.find((c) => c.id === id) ?? null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Capabilities relevant to a tenant shape. A commerce tenant should never be told about a
|
|
254
|
+
* marketing-only capability, and vice versa — that noise is what makes a catalogue unread.
|
|
255
|
+
* @param {string} [siteType]
|
|
256
|
+
*/
|
|
257
|
+
export function capabilitiesFor(siteType) {
|
|
258
|
+
if (!siteType) return CAPABILITIES;
|
|
259
|
+
return CAPABILITIES.filter((c) => c.appliesTo.includes(/** @type {any} */ (siteType)));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Is this supported today, refused on purpose, or simply absent? */
|
|
263
|
+
export function supportStatus(id) {
|
|
264
|
+
const found = capability(id);
|
|
265
|
+
if (!found) return { known: false, supported: false, refused: false, gap: false };
|
|
266
|
+
return {
|
|
267
|
+
known: true,
|
|
268
|
+
supported: SUPPORTED_TIERS.includes(found.tier),
|
|
269
|
+
refused: found.tier === 'deliberately-refused',
|
|
270
|
+
gap: found.tier === 'not-yet',
|
|
271
|
+
capability: found,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The answer a migrator actually wants, in one string: what to DO about this capability.
|
|
277
|
+
* Keeping it here rather than in the CLI means a gate, a report or a tool can all cite the same
|
|
278
|
+
* sentence instead of each inventing its own phrasing.
|
|
279
|
+
*/
|
|
280
|
+
export function describeCapability(id) {
|
|
281
|
+
const s = supportStatus(id);
|
|
282
|
+
if (!s.known) return `Unknown capability "${id}". Run \`tot capabilities\` to see the register.`;
|
|
283
|
+
const c = /** @type {Capability} */ (s.capability);
|
|
284
|
+
if (s.refused) {
|
|
285
|
+
return `${c.label} — REFUSED ON PURPOSE. ${c.because} Do not build a local workaround; if the `
|
|
286
|
+
+ 'need is real it is a reviewed platform change.';
|
|
287
|
+
}
|
|
288
|
+
if (s.gap) {
|
|
289
|
+
return `${c.label} — NOT BUILT YET, already filed as ${c.tracking}. Do not re-file; add `
|
|
290
|
+
+ 'evidence to that report instead.';
|
|
291
|
+
}
|
|
292
|
+
return `${c.label} — SUPPORTED (${c.tier}). ${c.summary}`
|
|
293
|
+
+ (c.populatedBy ? ` Populated by: ${c.populatedBy}.` : '')
|
|
294
|
+
+ (c.verifiedBy ? ` Verified by: ${c.verifiedBy}.` : '');
|
|
295
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot capabilities` — ask the platform what it supports before building something per store.
|
|
3
|
+
*
|
|
4
|
+
* tot capabilities [--site-type commerce|marketing] the whole register
|
|
5
|
+
* tot capabilities --id <capability-id> what fills it, what proves it, what to do
|
|
6
|
+
* tot capabilities --gaps filed gaps: add evidence, do not re-file
|
|
7
|
+
* tot capabilities --refused refused on purpose: do not work around
|
|
8
|
+
* tot capabilities --json machine-readable
|
|
9
|
+
*
|
|
10
|
+
* The register ships inside this CLI release, so the answer tracks the platform version the CLI
|
|
11
|
+
* was released with. Exit 0 on an answer; 2 on a usage error; 4 for an unknown id, so a typo never
|
|
12
|
+
* reads as "no such capability, go build it".
|
|
13
|
+
*/
|
|
14
|
+
import { CAPABILITIES, capabilitiesFor, describeCapability, supportStatus } from "../capability-registry.mjs";
|
|
15
|
+
|
|
16
|
+
const TIER_MARK = {
|
|
17
|
+
"platform-core": "BUILT ",
|
|
18
|
+
app: "APP ",
|
|
19
|
+
plugin: "PLUGIN ",
|
|
20
|
+
"deliberately-refused": "REFUSED ",
|
|
21
|
+
"not-yet": "GAP ",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const SITE_TYPES = ["commerce", "marketing"];
|
|
25
|
+
|
|
26
|
+
export const USAGE = `tot capabilities — what the platform supports, refuses, and has not built yet
|
|
27
|
+
|
|
28
|
+
tot capabilities [--site-type commerce|marketing] list the register
|
|
29
|
+
tot capabilities --id <id> what fills it, what proves it, what to do
|
|
30
|
+
tot capabilities --gaps filed gaps (add evidence, do not re-file)
|
|
31
|
+
tot capabilities --refused refused on purpose (do not work around)
|
|
32
|
+
tot capabilities --json machine-readable output`;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A capability as the public CLI prints it. `implementedBy` names platform-repository files: it is
|
|
36
|
+
* the register's anti-drift proof, checked by the platform's own tests, and means nothing to a
|
|
37
|
+
* developer outside that repository.
|
|
38
|
+
* @param {import("../capability-registry.mjs").Capability} c
|
|
39
|
+
*/
|
|
40
|
+
export function publicCapability(c) {
|
|
41
|
+
const { implementedBy: _internal, ...rest } = c;
|
|
42
|
+
return rest;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {string[]} argv */
|
|
46
|
+
export function parseArgs(argv) {
|
|
47
|
+
/** @type {{ gaps: boolean, refused: boolean, json: boolean, help: boolean, id?: string, siteType?: string, error?: string }} */
|
|
48
|
+
const args = { gaps: false, refused: false, json: false, help: false };
|
|
49
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
50
|
+
const key = argv[i];
|
|
51
|
+
if (key === "--list") continue;
|
|
52
|
+
if (key === "--gaps") { args.gaps = true; continue; }
|
|
53
|
+
if (key === "--refused") { args.refused = true; continue; }
|
|
54
|
+
if (key === "--json") { args.json = true; continue; }
|
|
55
|
+
if (key === "--help" || key === "-h") { args.help = true; continue; }
|
|
56
|
+
if (key === "--id" || key === "--site-type") {
|
|
57
|
+
const value = argv[i + 1];
|
|
58
|
+
if (!value || value.startsWith("--")) return { ...args, error: `missing value for ${key}` };
|
|
59
|
+
if (key === "--id") args.id = value;
|
|
60
|
+
else args.siteType = value;
|
|
61
|
+
i += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
return { ...args, error: `unknown argument: ${key}` };
|
|
65
|
+
}
|
|
66
|
+
if (args.siteType && !SITE_TYPES.includes(args.siteType)) {
|
|
67
|
+
return { ...args, error: `--site-type must be one of: ${SITE_TYPES.join(", ")}` };
|
|
68
|
+
}
|
|
69
|
+
return args;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string[]} argv
|
|
74
|
+
* @param {any} _ctx
|
|
75
|
+
* @param {{ out?: (s: string) => void, err?: (s: string) => void }} [io]
|
|
76
|
+
*/
|
|
77
|
+
export function run(argv, _ctx, io = {}) {
|
|
78
|
+
const out = io.out ?? ((s) => process.stdout.write(s));
|
|
79
|
+
const err = io.err ?? ((s) => process.stderr.write(s));
|
|
80
|
+
const args = parseArgs(argv);
|
|
81
|
+
if (args.help) {
|
|
82
|
+
out(`${USAGE}\n`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
if (args.error) {
|
|
86
|
+
err(`${args.error}\n\n${USAGE}\n`);
|
|
87
|
+
return 2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (args.id) {
|
|
91
|
+
const status = supportStatus(args.id);
|
|
92
|
+
if (args.json) {
|
|
93
|
+
const body = status.capability ? { ...status, capability: publicCapability(status.capability) } : status;
|
|
94
|
+
out(`${JSON.stringify(body, null, 2)}\n`);
|
|
95
|
+
} else {
|
|
96
|
+
out(`${describeCapability(args.id)}\n`);
|
|
97
|
+
}
|
|
98
|
+
return status.known ? 0 : 4;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let rows = capabilitiesFor(args.siteType);
|
|
102
|
+
if (args.gaps) rows = rows.filter((c) => c.tier === "not-yet");
|
|
103
|
+
if (args.refused) rows = rows.filter((c) => c.tier === "deliberately-refused");
|
|
104
|
+
|
|
105
|
+
if (args.json) {
|
|
106
|
+
out(`${JSON.stringify(rows.map(publicCapability), null, 2)}\n`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const width = Math.max(...rows.map((c) => c.id.length), 4);
|
|
111
|
+
for (const c of rows) out(`${TIER_MARK[c.tier] ?? c.tier} ${c.id.padEnd(width)} ${c.label}\n`);
|
|
112
|
+
out(
|
|
113
|
+
`\n${rows.length} of ${CAPABILITIES.length} capabilities`
|
|
114
|
+
+ `${args.siteType ? ` for site type "${args.siteType}"` : ""}. `
|
|
115
|
+
+ "Use --id <id> for what fills it, what proves it, and what to do about it.\n"
|
|
116
|
+
+ "REFUSED means it will never be built — do not work around it locally.\n"
|
|
117
|
+
+ "GAP means it is already filed — add evidence to that report rather than filing again.\n",
|
|
118
|
+
);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|