@thorprovider/storefront 0.3.1 → 0.4.0-mvp.0
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/CHANGELOG.md +23 -0
- package/README.md +248 -99
- package/assets/templates/app/app/[[...slug]]/page.tsx +13 -2
- package/assets/templates/app/app/layout.tsx +18 -4
- package/assets/templates/app/app/robots.ts +13 -0
- package/assets/templates/app/app/sitemap.ts +12 -0
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +542 -0
- package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +40 -57
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +608 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +165 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
- package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
- package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +232 -51
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -3
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +121 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +96 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +34 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +100 -11
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +142 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +17 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
- package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
- package/assets/templates/features/checkout/handlers.ts +153 -0
- package/assets/templates/features/checkout/state.ts +61 -0
- package/assets/templates/features/customer-auth/handlers.ts +59 -0
- package/assets/templates/features/customer-auth/state.ts +36 -0
- package/assets/templates/features/recommendations/state.ts +11 -0
- package/assets/templates/views/account/content.ts +35 -0
- package/assets/templates/views/account/index.tsx +114 -0
- package/assets/templates/views/account/registry.tsx +378 -0
- package/assets/templates/views/account/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +28 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +193 -0
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +33 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
- package/assets/templates/views/category/content.ts +30 -0
- package/assets/templates/views/category/data.ts +12 -0
- package/assets/templates/views/category/index.tsx +230 -0
- package/assets/templates/views/category/props.ts +16 -0
- package/assets/templates/views/category/registry.tsx +103 -0
- package/assets/templates/views/category/schema.ts +78 -0
- package/assets/templates/views/checkout/content.ts +105 -0
- package/assets/templates/views/checkout/data.ts +23 -0
- package/assets/templates/views/checkout/index.tsx +111 -0
- package/assets/templates/views/checkout/props.ts +49 -0
- package/assets/templates/views/checkout/registry.tsx +217 -0
- package/assets/templates/views/checkout/schema.ts +229 -0
- package/assets/templates/views/home/assembly/assembly.json +24 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +72 -0
- package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
- package/assets/templates/views/home/content.ts +70 -0
- package/assets/templates/views/home/data.ts +15 -0
- package/assets/templates/views/home/index.tsx +38 -84
- package/assets/templates/views/home/props.ts +22 -0
- package/assets/templates/views/home/registry.tsx +38 -22
- package/assets/templates/views/home/schema.ts +93 -0
- package/assets/templates/views/product/content.ts +25 -0
- package/assets/templates/views/product/data.ts +48 -0
- package/assets/templates/views/product/index.tsx +104 -0
- package/assets/templates/views/product/props.ts +20 -0
- package/assets/templates/views/product/registry.tsx +78 -0
- package/assets/templates/views/product/schema.ts +108 -0
- package/assets/templates/views/static/content.ts +99 -0
- package/assets/templates/views/static/index.tsx +60 -0
- package/assets/templates/views/static/registry.tsx +66 -0
- package/assets/templates/views/static/schema.ts +45 -0
- package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/index.tsx +8 -70
- package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/registry.tsx +23 -18
- package/assets/templates/views/user-panel/schema.ts +79 -0
- package/bin/storefront.js +1278 -79
- package/commands/sf-add-view.md +44 -13
- package/commands/sf-feature.md +28 -0
- package/commands/sf-theme.md +14 -8
- package/commands/sf-view.md +54 -12
- package/package.json +6 -4
- package/recipes/archetypes.json +28 -1
- package/recipes/catalog.json +361 -0
- package/recipes/catalog.mjs +128 -0
- package/recipes/catalog.schema.json +64 -0
- package/recipes/feature.schema.json +89 -0
- package/recipes/features.json +139 -0
- package/recipes/manifest.schema.json +69 -0
- package/recipes/operations.schema.json +43 -0
- package/recipes/recipe.schema.json +11 -2
- package/recipes/recipes.json +60 -5
- package/recipes/sections.json +24 -5
- package/recipes/validate.mjs +784 -32
- package/scripts/candidate.mjs +236 -0
- package/scripts/composers.mjs +140 -0
- package/scripts/file-classes.mjs +116 -0
- package/scripts/logical-controls.mjs +61 -0
- package/scripts/manifest.mjs +298 -0
- package/scripts/operation-registry.mjs +256 -0
- package/scripts/operations.mjs +720 -0
- package/scripts/ownership-conflict.mjs +356 -0
- package/scripts/pinning.mjs +504 -0
- package/scripts/plan-apply.mjs +843 -0
- package/scripts/preflight.mjs +51 -0
- package/scripts/reconcile.mjs +511 -0
- package/scripts/render-templates.mjs +245 -28
- package/scripts/schema-validate.mjs +132 -0
- package/scripts/section-compat.mjs +300 -0
- package/scripts/section-operations.mjs +508 -0
- package/scripts/sync-assets.mjs +22 -15
- package/scripts/view-assembly.mjs +291 -0
- package/scripts/view-selection.mjs +211 -0
- package/skills/sf-feature/SKILL.md +67 -0
- package/skills/sf-theme-gen/SKILL.md +12 -5
- package/skills/sf-view-gen/SKILL.md +123 -93
- package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -1
- package/skills/sf-view-gen/assets/recipes/catalog.json +361 -0
- package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
- package/skills/sf-view-gen/assets/recipes/features.json +139 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +60 -5
- package/skills/sf-view-gen/assets/recipes/sections.json +24 -5
- package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
- package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
- package/skills/sf-view-gen/references/views.md +44 -10
- package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
- package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
- package/skills/sf-view-gen/scripts/validate.mjs +784 -32
- package/skills/storefront-workflow/SKILL.md +116 -0
- package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
- package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
- package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
- package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
- package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
- package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// E3 tasks 3.1, 3.2, and 3.3 (CU-14, CU-15, CU-16): the `section.add`,
|
|
3
|
+
// `section.remove`, and `section.reorder` operations and the production E3
|
|
4
|
+
// operation registry.
|
|
5
|
+
//
|
|
6
|
+
// E3 extends E1's namespaced, extensible registry (E1 Decision 16) with the
|
|
7
|
+
// `section` namespace; it never widens `E1_OPERATION_REGISTRY` or
|
|
8
|
+
// `CATALOG_OPERATION_REGISTRY`. The production registry
|
|
9
|
+
// `SECTION_OPERATION_REGISTRY` is E1's six operations + E2's catalog logical
|
|
10
|
+
// controls + E3's `section.add`/`section.remove`/`section.reorder`, composed with
|
|
11
|
+
// `extendOperationRegistry`.
|
|
12
|
+
//
|
|
13
|
+
// Each reducer is pure over the manifest: it reads the view's declared
|
|
14
|
+
// `sections`, normalizes and validates it through `section-compat.mjs` (the
|
|
15
|
+
// single normalization authority), validates the host/type allowlist, the
|
|
16
|
+
// editorial `maxSections` bound, id uniqueness, the reserved `slot:` prefix, and
|
|
17
|
+
// (for a reorder) that `order` is a permutation of the view's current entry ids,
|
|
18
|
+
// then returns a NEW manifest (plus, for a removal, the preserved content
|
|
19
|
+
// location) with the change applied. It never mutates its input and returns no
|
|
20
|
+
// manifest on any failure, so a caller writes nothing (no partial writes).
|
|
21
|
+
|
|
22
|
+
import { extendOperationRegistry } from './operation-registry.mjs'
|
|
23
|
+
import { CATALOG_OPERATION_REGISTRY, DIAGNOSTIC_CODES, diagnostic } from './operations.mjs'
|
|
24
|
+
import {
|
|
25
|
+
isEditorialSectionEntry,
|
|
26
|
+
isSlotSectionEntry,
|
|
27
|
+
normalizeViewSections,
|
|
28
|
+
} from './section-compat.mjs'
|
|
29
|
+
import {
|
|
30
|
+
allowedViewNames,
|
|
31
|
+
getRecipe,
|
|
32
|
+
heldSectionReason,
|
|
33
|
+
loadSections,
|
|
34
|
+
} from '../recipes/validate.mjs'
|
|
35
|
+
|
|
36
|
+
// The E3 section-operation diagnostics, alongside E1's shared `DIAGNOSTIC_CODES`
|
|
37
|
+
// (E_INVALID_OPERATION, E_VIEW_NOT_INSTALLED). They mirror the codes
|
|
38
|
+
// `section-compat.mjs` emits so a normalization failure and a direct
|
|
39
|
+
// `section.add`/`section.remove` failure are named consistently.
|
|
40
|
+
export const SECTION_DIAGNOSTIC_CODES = Object.freeze({
|
|
41
|
+
E_UNKNOWN_HOST: 'E_UNKNOWN_HOST',
|
|
42
|
+
E_DISALLOWED_TYPE: 'E_DISALLOWED_TYPE',
|
|
43
|
+
E_DUPLICATE_ID: 'E_DUPLICATE_ID',
|
|
44
|
+
E_RESERVED_ID: 'E_RESERVED_ID',
|
|
45
|
+
E_SECTION_LIMIT: 'E_SECTION_LIMIT',
|
|
46
|
+
E_CORE_SLOT_REMOVAL: 'E_CORE_SLOT_REMOVAL',
|
|
47
|
+
E_UNKNOWN_SECTION_REF: 'E_UNKNOWN_SECTION_REF',
|
|
48
|
+
E_UNKNOWN_ORDER_REF: 'E_UNKNOWN_ORDER_REF',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const RESERVED_ID_PREFIX = 'slot:'
|
|
52
|
+
|
|
53
|
+
function hasOwn(object, key) {
|
|
54
|
+
return Object.prototype.hasOwnProperty.call(object, key)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function requireString(op, field) {
|
|
58
|
+
return typeof op[field] === 'string' && op[field].length > 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fail(code, message, extra = {}) {
|
|
62
|
+
return { ok: false, diagnostics: [diagnostic(code, message, extra)] }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The preserved instance ids of the operated view: an editorial instance whose
|
|
66
|
+
// content `section.remove` kept on disk. `content.ts` (classified user-owned by
|
|
67
|
+
// `file-classes.mjs`) holds the content bytes and is never rewritten, but it
|
|
68
|
+
// cannot itself enumerate the ids, so the id record lives in the manifest as the
|
|
69
|
+
// top-level `preservedSectionIds` map (view name -> ordered ids, written by
|
|
70
|
+
// `section.remove`). It is top-level, not per-view, so every E1 `view.*`/
|
|
71
|
+
// `feature.*` reducer (which spreads the manifest) carries it forward and an
|
|
72
|
+
// explicit re-add cannot accidentally rebind. Auto-mint consults the set so a
|
|
73
|
+
// fresh id never rebinds preserved content; an explicit re-add of a preserved id
|
|
74
|
+
// leaves the set (rebind). This is the concrete disk-backed notion of "preserved"
|
|
75
|
+
// (E3 task 3.7, design §"Remove, content preservation, and re-add same id").
|
|
76
|
+
function preservedIdsFor(manifest, viewName) {
|
|
77
|
+
const map = manifest.preservedSectionIds
|
|
78
|
+
const list =
|
|
79
|
+
map && typeof map === 'object' && !Array.isArray(map) ? map[viewName] : undefined
|
|
80
|
+
const entries = Array.isArray(list) ? list : []
|
|
81
|
+
return new Set(entries.filter((id) => typeof id === 'string' && id.length > 0))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Copy `manifest` with `preservedSectionIds[viewName]` set to exactly `ids`
|
|
85
|
+
// (deduplicated, insertion-ordered). An empty list drops the view's entry, and an
|
|
86
|
+
// empty map drops the top-level field, so a manifest that never preserved
|
|
87
|
+
// anything stays byte-identical to one that preserved and re-added. Never mutates
|
|
88
|
+
// the input.
|
|
89
|
+
function withPreservedIds(manifest, viewName, ids) {
|
|
90
|
+
const current = manifest.preservedSectionIds
|
|
91
|
+
const map = current && typeof current === 'object' && !Array.isArray(current) ? { ...current } : {}
|
|
92
|
+
const deduped = [...new Set(ids)]
|
|
93
|
+
if (deduped.length === 0) delete map[viewName]
|
|
94
|
+
else map[viewName] = deduped
|
|
95
|
+
const out = { ...manifest }
|
|
96
|
+
if (Object.keys(map).length === 0) delete out.preservedSectionIds
|
|
97
|
+
else out.preservedSectionIds = map
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sectionDefinition(type) {
|
|
102
|
+
const sections = loadSections()?.sections
|
|
103
|
+
const list = Array.isArray(sections) ? sections : []
|
|
104
|
+
return list.find((section) => section.name === type) ?? null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The smallest free `<view>-<type>-<n>` (n >= 1) not in `used ∪ preserved`, so an
|
|
108
|
+
// auto-minted id never rebinds preserved content (design: `section.add` auto-mint).
|
|
109
|
+
function mintSectionId(viewName, type, used, preserved) {
|
|
110
|
+
for (let n = 1; ; n += 1) {
|
|
111
|
+
const candidate = `${viewName}-${type}-${n}`
|
|
112
|
+
if (!used.has(candidate) && !preserved.has(candidate)) return candidate
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The user-owned file that holds a view's editorial instance content, keyed by
|
|
117
|
+
// instance id (design §"Remove, content preservation, and re-add same id"). It is
|
|
118
|
+
// the view's `content.ts` module, which `file-classes.mjs` classifies user-owned
|
|
119
|
+
// and `reconcile` seeds once and never rewrites, so `section.remove` preserves
|
|
120
|
+
// the content simply by not touching the file; the reducer reports this
|
|
121
|
+
// project-relative POSIX path through the operation's `removals` so the write
|
|
122
|
+
// boundary surfaces it in the result's `preserved` list. The helper is pure (it
|
|
123
|
+
// derives the path from the manifest name and the view), so the reducer stays
|
|
124
|
+
// filesystem-free.
|
|
125
|
+
function sectionContentPath(manifest, viewName) {
|
|
126
|
+
return `lib/${manifest.name}/views/${viewName}/content.ts`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reduce a `section.add` operation over the manifest (E3 task 3.1). Validates the
|
|
131
|
+
* host allowlist (`E_UNKNOWN_HOST` for an unknown view, `E_DISALLOWED_TYPE` for a
|
|
132
|
+
* type the view does not allow), id uniqueness against `used` (`E_DUPLICATE_ID`),
|
|
133
|
+
* the reserved `slot:` prefix (`E_RESERVED_ID`), and the editorial `maxSections`
|
|
134
|
+
* bound (`E_SECTION_LIMIT`). An omitted `id` is minted from `used ∪ preserved`,
|
|
135
|
+
* where `preserved` is the view's recorded removed ids (so a fresh id never
|
|
136
|
+
* rebinds preserved content). An explicit `id` that is still present is
|
|
137
|
+
* `E_DUPLICATE_ID`; an explicit `id` that is preserved (removed, its content
|
|
138
|
+
* still on disk) succeeds and rebinds — it leaves the preserved record. Returns
|
|
139
|
+
* `{ ok: true, manifest }` with the instance inserted at `at` (0-based, default
|
|
140
|
+
* append) or `{ ok: false, diagnostics }`. The input manifest is never mutated.
|
|
141
|
+
*/
|
|
142
|
+
export function reduceSectionAdd(manifest, op, options = {}) {
|
|
143
|
+
if (!requireString(op, 'view')) {
|
|
144
|
+
return fail(
|
|
145
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
146
|
+
'section.add requires a non-empty string "view".',
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
if (!requireString(op, 'type')) {
|
|
150
|
+
return fail(
|
|
151
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
152
|
+
'section.add requires a non-empty string "type".',
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
const viewName = op.view
|
|
156
|
+
const recipe = getRecipe(viewName)
|
|
157
|
+
if (!recipe) {
|
|
158
|
+
return fail(
|
|
159
|
+
SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_HOST,
|
|
160
|
+
`Unknown view "${viewName}" is not a section host. Allowed views: ${allowedViewNames().join(', ')}.`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
const allowedSections = Array.isArray(recipe.allowedSections) ? recipe.allowedSections : []
|
|
164
|
+
if (!allowedSections.includes(op.type)) {
|
|
165
|
+
const held = heldSectionReason(op.type)
|
|
166
|
+
const heldDetail = held ? ` Section "${op.type}" is held: ${held}.` : ''
|
|
167
|
+
return fail(
|
|
168
|
+
SECTION_DIAGNOSTIC_CODES.E_DISALLOWED_TYPE,
|
|
169
|
+
`Section type "${op.type}" is not an allowed section for view "${viewName}". Allowed sections: ${allowedSections.join(', ') || '(none)'}.${heldDetail}`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
const installed = manifest.views.find((view) => view.name === viewName)
|
|
173
|
+
if (!installed) {
|
|
174
|
+
return fail(
|
|
175
|
+
DIAGNOSTIC_CODES.E_VIEW_NOT_INSTALLED,
|
|
176
|
+
`View "${viewName}" is not installed; add the view before adding a section to it.`,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
const registeredSlots = Array.isArray(recipe.registeredSlots) ? recipe.registeredSlots : []
|
|
180
|
+
const maxSections = typeof recipe.maxSections === 'number' ? recipe.maxSections : null
|
|
181
|
+
const preserved = preservedIdsFor(manifest, viewName)
|
|
182
|
+
|
|
183
|
+
// Normalize/validate the current composition through the single authority so a
|
|
184
|
+
// legacy or malformed list is adapted or refused consistently, with no output
|
|
185
|
+
// on failure.
|
|
186
|
+
const normalized = normalizeViewSections(viewName, installed.sections ?? [], registeredSlots, {
|
|
187
|
+
allowedSections,
|
|
188
|
+
maxSections,
|
|
189
|
+
})
|
|
190
|
+
if (!normalized.ok) return { ok: false, diagnostics: normalized.diagnostics }
|
|
191
|
+
const current = normalized.sections
|
|
192
|
+
const used = new Set(current.map((entry) => entry.id))
|
|
193
|
+
|
|
194
|
+
let id
|
|
195
|
+
if (hasOwn(op, 'id')) {
|
|
196
|
+
if (!requireString(op, 'id')) {
|
|
197
|
+
return fail(
|
|
198
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
199
|
+
'section.add "id" must be a non-empty string when present.',
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
if (op.id.startsWith(RESERVED_ID_PREFIX)) {
|
|
203
|
+
return fail(
|
|
204
|
+
SECTION_DIAGNOSTIC_CODES.E_RESERVED_ID,
|
|
205
|
+
`section.add id "${op.id}" must not start with the reserved "${RESERVED_ID_PREFIX}" prefix.`,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
if (used.has(op.id)) {
|
|
209
|
+
return fail(
|
|
210
|
+
SECTION_DIAGNOSTIC_CODES.E_DUPLICATE_ID,
|
|
211
|
+
`section.add id "${op.id}" is already present in view "${viewName}".`,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
id = op.id
|
|
215
|
+
} else {
|
|
216
|
+
id = mintSectionId(viewName, op.type, used, preserved)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let variant
|
|
220
|
+
if (hasOwn(op, 'variant')) {
|
|
221
|
+
if (!requireString(op, 'variant')) {
|
|
222
|
+
return fail(
|
|
223
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
224
|
+
'section.add "variant" must be a non-empty string when present.',
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
const definition = sectionDefinition(op.type)
|
|
228
|
+
const variants = Array.isArray(definition?.variants) ? definition.variants : []
|
|
229
|
+
if (variants.length > 0 && !variants.includes(op.variant)) {
|
|
230
|
+
return fail(
|
|
231
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
232
|
+
`Variant "${op.variant}" is not a declared variant for section "${op.type}". Declared: ${variants.join(', ')}.`,
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
variant = op.variant
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const editorialCount = current.filter((entry) => isEditorialSectionEntry(entry)).length
|
|
239
|
+
if (maxSections !== null && editorialCount + 1 > maxSections) {
|
|
240
|
+
return fail(
|
|
241
|
+
SECTION_DIAGNOSTIC_CODES.E_SECTION_LIMIT,
|
|
242
|
+
`Adding a "${op.type}" instance to view "${viewName}" would exceed maxSections ${maxSections} (currently ${editorialCount}).`,
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let at = current.length
|
|
247
|
+
if (hasOwn(op, 'at')) {
|
|
248
|
+
if (!Number.isInteger(op.at) || op.at < 0 || op.at > current.length) {
|
|
249
|
+
return fail(
|
|
250
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
251
|
+
`section.add "at" must be an integer between 0 and ${current.length}.`,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
at = op.at
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const entry = { id, type: op.type, ...(variant === undefined ? {} : { variant }) }
|
|
258
|
+
const sections = [...current.slice(0, at), entry, ...current.slice(at)]
|
|
259
|
+
const views = manifest.views.map((view) =>
|
|
260
|
+
view.name === viewName ? { ...view, sections } : view,
|
|
261
|
+
)
|
|
262
|
+
// Re-adding an explicit preserved id rebinds its content: the id leaves the
|
|
263
|
+
// preserved record and becomes a used id. An auto-minted id is already absent
|
|
264
|
+
// from the preserved set, so this is a no-op for it.
|
|
265
|
+
const next = withPreservedIds(
|
|
266
|
+
{ ...manifest, views },
|
|
267
|
+
viewName,
|
|
268
|
+
[...preserved].filter((preservedId) => preservedId !== id),
|
|
269
|
+
)
|
|
270
|
+
return { ok: true, manifest: next }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Reduce a `section.remove` operation over the manifest (E3 task 3.2,
|
|
275
|
+
* CU-15/CU-16). A reserved `slot:` id is a core registered slot and is rejected
|
|
276
|
+
* with `E_CORE_SLOT_REMOVAL` (design's stable code); an id that does not name a
|
|
277
|
+
* present editorial instance is rejected with `E_UNKNOWN_SECTION_REF`. On
|
|
278
|
+
* success it drops the editorial entry from the ordered composition, records the
|
|
279
|
+
* removed id as preserved in the view's `preservedSectionIds`, and returns the
|
|
280
|
+
* removal holder carrying the instance's user-owned content location, which the
|
|
281
|
+
* write boundary reports as preserved while `reconcile` leaves the file
|
|
282
|
+
* byte-unchanged. Never mutates its input and returns no manifest on failure, so
|
|
283
|
+
* a caller writes nothing (no partial writes).
|
|
284
|
+
*/
|
|
285
|
+
export function reduceSectionRemove(manifest, op, options = {}) {
|
|
286
|
+
if (!requireString(op, 'view')) {
|
|
287
|
+
return fail(
|
|
288
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
289
|
+
'section.remove requires a non-empty string "view".',
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
if (!requireString(op, 'id')) {
|
|
293
|
+
return fail(
|
|
294
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
295
|
+
'section.remove requires a non-empty string "id".',
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
const viewName = op.view
|
|
299
|
+
const recipe = getRecipe(viewName)
|
|
300
|
+
if (!recipe) {
|
|
301
|
+
return fail(
|
|
302
|
+
SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_HOST,
|
|
303
|
+
`Unknown view "${viewName}" is not a section host. Allowed views: ${allowedViewNames().join(', ')}.`,
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
const installed = manifest.views.find((view) => view.name === viewName)
|
|
307
|
+
if (!installed) {
|
|
308
|
+
return fail(
|
|
309
|
+
DIAGNOSTIC_CODES.E_VIEW_NOT_INSTALLED,
|
|
310
|
+
`View "${viewName}" is not installed; add the view before removing a section from it.`,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
// Core registered slots are non-removable (design "core transactional slots are
|
|
314
|
+
// never removed"): any reserved `slot:` id is refused before normalization with
|
|
315
|
+
// the stable `E_CORE_SLOT_REMOVAL` code.
|
|
316
|
+
if (op.id.startsWith(RESERVED_ID_PREFIX)) {
|
|
317
|
+
return fail(
|
|
318
|
+
SECTION_DIAGNOSTIC_CODES.E_CORE_SLOT_REMOVAL,
|
|
319
|
+
`section.remove id "${op.id}" is a reserved core slot of view "${viewName}"; registered slots cannot be removed.`,
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
const registeredSlots = Array.isArray(recipe.registeredSlots) ? recipe.registeredSlots : []
|
|
323
|
+
const allowedSections = Array.isArray(recipe.allowedSections) ? recipe.allowedSections : []
|
|
324
|
+
const maxSections = typeof recipe.maxSections === 'number' ? recipe.maxSections : null
|
|
325
|
+
|
|
326
|
+
// Normalize/validate the current composition through the single authority so a
|
|
327
|
+
// legacy or malformed list is adapted or refused consistently, with no output
|
|
328
|
+
// on failure.
|
|
329
|
+
const normalized = normalizeViewSections(viewName, installed.sections ?? [], registeredSlots, {
|
|
330
|
+
allowedSections,
|
|
331
|
+
maxSections,
|
|
332
|
+
})
|
|
333
|
+
if (!normalized.ok) return { ok: false, diagnostics: normalized.diagnostics }
|
|
334
|
+
const current = normalized.sections
|
|
335
|
+
|
|
336
|
+
const target = current.find(
|
|
337
|
+
(entry) => entry.id === op.id && isEditorialSectionEntry(entry),
|
|
338
|
+
)
|
|
339
|
+
if (!target) {
|
|
340
|
+
return fail(
|
|
341
|
+
SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_SECTION_REF,
|
|
342
|
+
`section.remove id "${op.id}" does not name an editorial section instance in view "${viewName}".`,
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const sections = current.filter((entry) => entry.id !== op.id)
|
|
347
|
+
const views = manifest.views.map((view) =>
|
|
348
|
+
view.name === viewName ? { ...view, sections } : view,
|
|
349
|
+
)
|
|
350
|
+
// Record the removed id as preserved: its user-owned content stays in the
|
|
351
|
+
// view's `content.ts` (unchanged), and the manifest holds the id so a later
|
|
352
|
+
// explicit re-add rebinds it while auto-mint avoids it.
|
|
353
|
+
const preserved = preservedIdsFor(manifest, viewName)
|
|
354
|
+
const next = withPreservedIds({ ...manifest, views }, viewName, [...preserved, op.id])
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
manifest: next,
|
|
358
|
+
removals: [
|
|
359
|
+
{ kind: 'section', view: viewName, id: op.id, contentPath: sectionContentPath(manifest, viewName) },
|
|
360
|
+
],
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Reduce a `section.reorder` operation over the manifest (E3 task 3.3,
|
|
366
|
+
* CU-15/CU-16). `order` MUST be a permutation of the view's current entry ids:
|
|
367
|
+
* every current id exactly once, no unknown id, no duplicate, and every
|
|
368
|
+
* registered core slot present. The current composition is normalized through
|
|
369
|
+
* the single `section-compat.mjs` authority first, so a legacy or malformed list
|
|
370
|
+
* is adapted or refused consistently. A successful reorder keeps every entry's
|
|
371
|
+
* id, type, variant, and content: it returns a new manifest whose view's
|
|
372
|
+
* `sections` are the same normalized entries in the requested order, never
|
|
373
|
+
* mutating its input and returning no manifest on failure (no partial writes).
|
|
374
|
+
*
|
|
375
|
+
* Stable codes (design diagnostic table): an id not present in the view ->
|
|
376
|
+
* `E_UNKNOWN_ORDER_REF`; a registered core slot that is omitted or listed twice
|
|
377
|
+
* -> `E_CORE_SLOT_REMOVAL`; a present editorial id listed twice ->
|
|
378
|
+
* `E_DUPLICATE_ID`.
|
|
379
|
+
*/
|
|
380
|
+
export function reduceSectionReorder(manifest, op, options = {}) {
|
|
381
|
+
if (!requireString(op, 'view')) {
|
|
382
|
+
return fail(
|
|
383
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
384
|
+
'section.reorder requires a non-empty string "view".',
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
if (!Array.isArray(op.order)) {
|
|
388
|
+
return fail(
|
|
389
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
390
|
+
'section.reorder requires an "order" array of entry ids.',
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
const viewName = op.view
|
|
394
|
+
const recipe = getRecipe(viewName)
|
|
395
|
+
if (!recipe) {
|
|
396
|
+
return fail(
|
|
397
|
+
SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_HOST,
|
|
398
|
+
`Unknown view "${viewName}" is not a section host. Allowed views: ${allowedViewNames().join(', ')}.`,
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
const installed = manifest.views.find((view) => view.name === viewName)
|
|
402
|
+
if (!installed) {
|
|
403
|
+
return fail(
|
|
404
|
+
DIAGNOSTIC_CODES.E_VIEW_NOT_INSTALLED,
|
|
405
|
+
`View "${viewName}" is not installed; add the view before reordering its sections.`,
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
const registeredSlots = Array.isArray(recipe.registeredSlots) ? recipe.registeredSlots : []
|
|
409
|
+
const allowedSections = Array.isArray(recipe.allowedSections) ? recipe.allowedSections : []
|
|
410
|
+
const maxSections = typeof recipe.maxSections === 'number' ? recipe.maxSections : null
|
|
411
|
+
|
|
412
|
+
// Normalize/validate the current composition through the single authority so a
|
|
413
|
+
// legacy or malformed list is adapted or refused consistently, with no output
|
|
414
|
+
// on failure.
|
|
415
|
+
const normalized = normalizeViewSections(viewName, installed.sections ?? [], registeredSlots, {
|
|
416
|
+
allowedSections,
|
|
417
|
+
maxSections,
|
|
418
|
+
})
|
|
419
|
+
if (!normalized.ok) return { ok: false, diagnostics: normalized.diagnostics }
|
|
420
|
+
const current = normalized.sections
|
|
421
|
+
const byId = new Map(current.map((entry) => [entry.id, entry]))
|
|
422
|
+
const slotIds = new Set(current.filter((entry) => isSlotSectionEntry(entry)).map((entry) => entry.id))
|
|
423
|
+
|
|
424
|
+
const order = op.order
|
|
425
|
+
for (const id of order) {
|
|
426
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
427
|
+
return fail(
|
|
428
|
+
DIAGNOSTIC_CODES.E_INVALID_OPERATION,
|
|
429
|
+
'section.reorder "order" entries must be non-empty strings.',
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const seen = new Set()
|
|
435
|
+
for (const id of order) {
|
|
436
|
+
if (!byId.has(id)) {
|
|
437
|
+
return fail(
|
|
438
|
+
SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_ORDER_REF,
|
|
439
|
+
`section.reorder names id "${id}" which is not present in view "${viewName}".`,
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
if (seen.has(id)) {
|
|
443
|
+
if (slotIds.has(id)) {
|
|
444
|
+
return fail(
|
|
445
|
+
SECTION_DIAGNOSTIC_CODES.E_CORE_SLOT_REMOVAL,
|
|
446
|
+
`section.reorder lists the core slot "${id}" more than once; registered slots appear exactly once.`,
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
return fail(
|
|
450
|
+
SECTION_DIAGNOSTIC_CODES.E_DUPLICATE_ID,
|
|
451
|
+
`section.reorder lists id "${id}" more than once.`,
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
seen.add(id)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Every current entry id must appear exactly once. An omitted registered slot is
|
|
458
|
+
// `E_CORE_SLOT_REMOVAL` (design: "order omits ... a registered slot"); an omitted
|
|
459
|
+
// editorial id is the dual of an unknown reference, refused with
|
|
460
|
+
// `E_UNKNOWN_ORDER_REF` (the design named no separate code; recorded in
|
|
461
|
+
// `design.md`).
|
|
462
|
+
for (const id of byId.keys()) {
|
|
463
|
+
if (seen.has(id)) continue
|
|
464
|
+
if (slotIds.has(id)) {
|
|
465
|
+
return fail(
|
|
466
|
+
SECTION_DIAGNOSTIC_CODES.E_CORE_SLOT_REMOVAL,
|
|
467
|
+
`section.reorder omits the core slot "${id}"; registered slots cannot be removed or omitted.`,
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
return fail(
|
|
471
|
+
SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_ORDER_REF,
|
|
472
|
+
`section.reorder "order" is not a permutation of view "${viewName}" entry ids: it omits "${id}".`,
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const sections = order.map((id) => byId.get(id))
|
|
477
|
+
const views = manifest.views.map((view) =>
|
|
478
|
+
view.name === viewName ? { ...view, sections } : view,
|
|
479
|
+
)
|
|
480
|
+
return { ok: true, manifest: { ...manifest, views } }
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// E3's production registry: E1's six operations + E2's catalog logical controls +
|
|
484
|
+
// E3's `section.add`/`section.remove`/`section.reorder`. `E1_OPERATION_REGISTRY`
|
|
485
|
+
// and `CATALOG_OPERATION_REGISTRY` are never mutated, so E1's vocabulary and E2's
|
|
486
|
+
// controls stay exactly as accepted.
|
|
487
|
+
export const SECTION_OPERATION_REGISTRY = extendOperationRegistry(CATALOG_OPERATION_REGISTRY, {
|
|
488
|
+
operations: [
|
|
489
|
+
{
|
|
490
|
+
name: 'section.add',
|
|
491
|
+
required: ['view', 'type'],
|
|
492
|
+
optional: ['id', 'variant', 'at'],
|
|
493
|
+
reducer: reduceSectionAdd,
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: 'section.remove',
|
|
497
|
+
required: ['view', 'id'],
|
|
498
|
+
optional: [],
|
|
499
|
+
reducer: reduceSectionRemove,
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
name: 'section.reorder',
|
|
503
|
+
required: ['view', 'order'],
|
|
504
|
+
optional: [],
|
|
505
|
+
reducer: reduceSectionReorder,
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
})
|
package/scripts/sync-assets.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Keeps the per-skill copies of shared assets in sync with the single source.
|
|
3
3
|
//
|
|
4
|
-
// Source of truth: recipes/ at the package root.
|
|
5
|
-
// Generated copies
|
|
6
|
-
//
|
|
7
|
-
// skills/sf-view-gen/
|
|
4
|
+
// Source of truth: `recipes/` and `scripts/schema-validate.mjs` at the package root.
|
|
5
|
+
// Generated copies (the installed skill must be executable from its own assets,
|
|
6
|
+
// so the catalog loader, its schema, and the schema validator travel with it):
|
|
7
|
+
// skills/sf-view-gen/assets/recipes/{recipes,archetypes,sections,features}.json
|
|
8
|
+
// skills/sf-view-gen/assets/recipes/{catalog,catalog.schema}.json
|
|
9
|
+
// skills/sf-view-gen/scripts/{validate,catalog,schema-validate}.mjs
|
|
8
10
|
//
|
|
9
|
-
// Usage:
|
|
11
|
+
// Paths are package-root-relative. Usage:
|
|
10
12
|
// node scripts/sync-assets.mjs # write the copies from source
|
|
11
13
|
// node scripts/sync-assets.mjs --check # report divergence, exit 1 if any
|
|
12
14
|
|
|
@@ -19,13 +21,16 @@ import { fileURLToPath } from 'node:url'
|
|
|
19
21
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
20
22
|
const PKG_ROOT = resolve(__dirname, '..')
|
|
21
23
|
|
|
22
|
-
const SOURCE = join(PKG_ROOT, 'recipes')
|
|
23
|
-
|
|
24
24
|
const MANIFEST = [
|
|
25
|
-
['recipes.json',
|
|
26
|
-
['archetypes.json',
|
|
27
|
-
['sections.json',
|
|
28
|
-
['
|
|
25
|
+
['recipes/recipes.json', 'skills/sf-view-gen/assets/recipes/recipes.json'],
|
|
26
|
+
['recipes/archetypes.json', 'skills/sf-view-gen/assets/recipes/archetypes.json'],
|
|
27
|
+
['recipes/sections.json', 'skills/sf-view-gen/assets/recipes/sections.json'],
|
|
28
|
+
['recipes/features.json', 'skills/sf-view-gen/assets/recipes/features.json'],
|
|
29
|
+
['recipes/catalog.json', 'skills/sf-view-gen/assets/recipes/catalog.json'],
|
|
30
|
+
['recipes/catalog.schema.json', 'skills/sf-view-gen/assets/recipes/catalog.schema.json'],
|
|
31
|
+
['recipes/catalog.mjs', 'skills/sf-view-gen/scripts/catalog.mjs'],
|
|
32
|
+
['recipes/validate.mjs', 'skills/sf-view-gen/scripts/validate.mjs'],
|
|
33
|
+
['scripts/schema-validate.mjs', 'skills/sf-view-gen/scripts/schema-validate.mjs'],
|
|
29
34
|
]
|
|
30
35
|
|
|
31
36
|
function sha256(text) {
|
|
@@ -34,8 +39,9 @@ function sha256(text) {
|
|
|
34
39
|
|
|
35
40
|
export async function checkAssets() {
|
|
36
41
|
const diverged = []
|
|
37
|
-
for (const [
|
|
38
|
-
const source = await readFile(join(
|
|
42
|
+
for (const [sourceRel, destRel] of MANIFEST) {
|
|
43
|
+
const source = await readFile(join(PKG_ROOT, sourceRel), 'utf8')
|
|
44
|
+
const destPath = join(PKG_ROOT, destRel)
|
|
39
45
|
if (!existsSync(destPath)) {
|
|
40
46
|
diverged.push(`${destPath} (missing)`)
|
|
41
47
|
continue
|
|
@@ -50,8 +56,9 @@ export async function checkAssets() {
|
|
|
50
56
|
|
|
51
57
|
async function syncAssets() {
|
|
52
58
|
let written = 0
|
|
53
|
-
for (const [
|
|
54
|
-
const source = await readFile(join(
|
|
59
|
+
for (const [sourceRel, destRel] of MANIFEST) {
|
|
60
|
+
const source = await readFile(join(PKG_ROOT, sourceRel), 'utf8')
|
|
61
|
+
const destPath = join(PKG_ROOT, destRel)
|
|
55
62
|
await mkdir(dirname(destPath), { recursive: true })
|
|
56
63
|
await writeFile(destPath, source, 'utf8')
|
|
57
64
|
written++
|