@ankhorage/devtools 1.10.11 → 1.10.13
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/tools/skills/assets/zora-designer/SKILL.md +63 -56
- package/dist/tools/skills/assets/zora-designer/references/artifact.md +6 -4
- package/dist/tools/skills/assets/zora-designer/references/screens.md +90 -0
- package/dist/tools/skills/assets/zora-designer/references/workflow.md +84 -126
- package/dist/tools/skills/assets/zora-designer/scripts/generate-template-catalog.mjs +88 -0
- package/dist/tools/skills/assets/zora-designer/scripts/owner-api.mjs +67 -39
- package/dist/tools/skills/assets/zora-designer/scripts/scaffold-template.mjs +59 -139
- package/package.json +1 -1
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: zora-designer
|
|
3
3
|
description: >
|
|
4
|
-
|
|
5
|
-
evidence, and author
|
|
6
|
-
|
|
7
|
-
reconstruction with ZORA elements, or template creation. Do not use for illustration-only work.
|
|
4
|
+
Configure an owner-backed application design, generate one screen or a coherent screen series,
|
|
5
|
+
audit supplied evidence, and author portable Ankhorage templates. Use for category-driven design
|
|
6
|
+
decisions, ZORA screen generation, visual audits, or template creation.
|
|
8
7
|
---
|
|
9
8
|
|
|
10
9
|
# ZORA Designer
|
|
11
10
|
|
|
12
|
-
Design, audit, and author through the target repository's released owner APIs.
|
|
13
|
-
`zora-designer.md`
|
|
14
|
-
|
|
11
|
+
Design, audit, and author through the target repository's released owner APIs. The complete
|
|
12
|
+
`AppManifest` is runtime authority; `zora-designer.md` and generated screen images are design
|
|
13
|
+
evidence.
|
|
15
14
|
|
|
16
15
|
## Route the request
|
|
17
16
|
|
|
18
|
-
- `interactive`:
|
|
19
|
-
|
|
20
|
-
- `
|
|
21
|
-
|
|
22
|
-
- `
|
|
23
|
-
and
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
- `interactive`: run the progressive configuration conversation in
|
|
18
|
+
[workflow.md](references/workflow.md). Do not create code or images before confirmation.
|
|
19
|
+
- `screen`: resolve the configuration, then read [screens.md](references/screens.md) and design one
|
|
20
|
+
screen.
|
|
21
|
+
- `screens`: resolve the configuration, then read [screens.md](references/screens.md) and design an
|
|
22
|
+
ordered series with shared navigation, state, geometry, and tokens.
|
|
23
|
+
- `audit`: read [audit.md](references/audit.md) and evaluate supplied image or runtime evidence.
|
|
24
|
+
- `template`: resolve the configuration and screen model, then author one portable template through
|
|
25
|
+
the workflow below.
|
|
26
|
+
|
|
27
|
+
Natural language is enough. Treat short replies as answers to the current question, not permission
|
|
28
|
+
to infer later decisions. A reply such as “go on” advances to the next unresolved decision. Only an
|
|
29
|
+
explicit request such as “accept all recommended values” resolves the remaining recommendations at
|
|
30
|
+
once.
|
|
27
31
|
|
|
28
32
|
## Start with the owners
|
|
29
33
|
|
|
@@ -33,57 +37,60 @@ From the target repository, run:
|
|
|
33
37
|
bun .agents/skills/zora-designer/scripts/owner-api.mjs inspect
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Use owner terminology verbatim: `GeneratedColorRole`, `ThemeTokens.colors`, `ThemeSemantics`, and
|
|
42
|
-
`SemanticColorToken`. Do not introduce aliases or naming cleanup here.
|
|
40
|
+
Use its installed owner output for categories, recommendations, harmonies, tone pairs, navigation
|
|
41
|
+
types, ZORA elements, events, recipes, and version provenance. Never copy owner catalogs, component
|
|
42
|
+
schemas, token inventories, color algorithms, action types, or manifest implementations into this
|
|
43
|
+
skill.
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
Compile chosen values with the same helper before composing screens. Inspect both computed modes,
|
|
46
|
+
including their resolved Surface themes and all owner diagnostics. Never hand-calculate a value the
|
|
47
|
+
owner exposes.
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
existing brief target, category preset, then safe global default. Record an origin for each value.
|
|
48
|
-
Keep target design separate from observed runtime state and report drift rather than overwriting one
|
|
49
|
-
with the other.
|
|
49
|
+
## Preserve the complete UX
|
|
50
50
|
|
|
51
|
-
For
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
For every screen region, prefer the exact semantic ZORA element supported by current metadata.
|
|
52
|
+
Visual resemblance alone is insufficient. If no exact element exists, preserve the requested UX
|
|
53
|
+
with an obvious supported placeholder such as a secondary-surface `Box`, and record the capability
|
|
54
|
+
gap. Do not invent props, application components, or successful behavior.
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
Bind every interaction expressible by installed Contracts and ZORA event metadata. Leave an
|
|
57
|
+
unsupported interaction visibly present and explicitly unbound without blocking unrelated design
|
|
58
|
+
work. Release validation still decides whether the complete manifest is shippable.
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
element's semantic responsibility, structure, states/interactions, accessibility contract, props,
|
|
59
|
-
events, and data requirements fit. Visual resemblance alone is insufficient.
|
|
60
|
+
## Template output
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
A Templates repository template is exactly one portable unit:
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
```text
|
|
65
|
+
src/templates/categories/{appCategory}/{slug}/
|
|
66
|
+
createAppManifest.ts
|
|
67
|
+
assets/
|
|
68
|
+
screens/
|
|
69
|
+
images/
|
|
70
|
+
```
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
`createAppManifest.ts` default-exports a function returning the complete `AppManifest`.
|
|
73
|
+
`assets/screens/` contains design evidence only. Runtime media uses real application image regions
|
|
74
|
+
under `assets/images/`; rebuild text, controls, icons, surfaces, and layout with ZORA.
|
|
69
75
|
|
|
70
|
-
|
|
76
|
+
Scaffold only a reviewed, release-valid manifest:
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
```text
|
|
79
|
+
bun .agents/skills/zora-designer/scripts/scaffold-template.mjs scaffold-input.json
|
|
80
|
+
```
|
|
75
81
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
The helper creates the template directory and regenerates discovery from the filesystem. Do not add
|
|
83
|
+
category registries, seed definitions, fallback templates, compatibility paths, or per-template
|
|
84
|
+
barrels.
|
|
79
85
|
|
|
80
86
|
## Validate before handoff
|
|
81
87
|
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
|
|
88
|
+
- confirm the interactive decision sequence completed or the user explicitly accepted remaining
|
|
89
|
+
recommendations;
|
|
90
|
+
- compile light and dark independently through installed owner APIs;
|
|
91
|
+
- validate selected ZORA nodes, props, events, actions, and complete manifest contracts;
|
|
92
|
+
- run the screen composition gate from [screens.md](references/screens.md) before returning screen
|
|
93
|
+
output;
|
|
94
|
+
- keep unsupported interactions and capability gaps explicit;
|
|
95
|
+
- keep concept screens separate from runtime assets;
|
|
96
|
+
- for deterministic artifact shape, read [artifact.md](references/artifact.md).
|
|
@@ -67,8 +67,10 @@ Aggregate validation in this order: blocker, failure, not run, pass with non-blo
|
|
|
67
67
|
pass. Resolve the application gate separately. A runtime blocker affects aggregate status only when
|
|
68
68
|
runtime application is in the requested scope.
|
|
69
69
|
|
|
70
|
-
Every
|
|
71
|
-
|
|
70
|
+
Every capability that lacks an exact metadata-supported ZORA element records a non-blocking
|
|
71
|
+
capability gap and uses a visible `Box` placeholder in the concept composition. An owner diagnostic
|
|
72
|
+
with `severity: error` is a blocker. Record an owner issue link when one exists and the condition
|
|
73
|
+
for replacing the placeholder with a released real ZORA element.
|
|
72
74
|
|
|
73
75
|
## Deterministic audit fields
|
|
74
76
|
|
|
@@ -89,5 +91,5 @@ state that it was not persisted.
|
|
|
89
91
|
|
|
90
92
|
Before returning, confirm the artifact matches the requested mode, every resolved value has an
|
|
91
93
|
origin, owner identifiers were inspected, exact measurements came from tools, invisible behavior
|
|
92
|
-
was not passed from screenshots,
|
|
93
|
-
authority.
|
|
94
|
+
was not passed from screenshots, owner errors block release, capability gaps are explicit, and the
|
|
95
|
+
manifest remains canonical runtime authority.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Screen and Screen-Series Design
|
|
2
|
+
|
|
3
|
+
Read this reference for `screen` and `screens` after the interactive configuration is confirmed.
|
|
4
|
+
Generated images are concept evidence; the manifest and ZORA metadata remain implementation
|
|
5
|
+
authority.
|
|
6
|
+
|
|
7
|
+
## Preconditions
|
|
8
|
+
|
|
9
|
+
Do not generate or implement a screen until these are resolved:
|
|
10
|
+
|
|
11
|
+
- audience and primary task;
|
|
12
|
+
- platform, input modes, theme coverage, and an explicit target viewport or responsive range;
|
|
13
|
+
- primary color, harmony, per-mode tone pairs, typography/profile, and compiled owner themes;
|
|
14
|
+
- ordered screen list with one purpose and primary action per screen;
|
|
15
|
+
- navigator type, route topology, and back/cancel behavior.
|
|
16
|
+
|
|
17
|
+
If the user supplies only `mobile`, recommend a neutral 390 by 844 logical-point portrait concept
|
|
18
|
+
viewport. Ask about iOS- or Android-specific chrome only when it changes the requested result.
|
|
19
|
+
|
|
20
|
+
## Compose the series before rendering
|
|
21
|
+
|
|
22
|
+
For every screen record:
|
|
23
|
+
|
|
24
|
+
1. purpose, primary task, primary action, and success outcome;
|
|
25
|
+
2. content hierarchy and exact visible copy;
|
|
26
|
+
3. default plus relevant loading, empty, partial, error, offline, disabled, pressed, selected, and
|
|
27
|
+
focus states;
|
|
28
|
+
4. route relationship and continuity of selected item, filters, progress, drafts, or other state;
|
|
29
|
+
5. exact metadata-backed ZORA elements, supported events/actions, data needs, and capability gaps;
|
|
30
|
+
6. safe areas, keyboard/overlay behavior, scroll ownership, and narrow/wide behavior;
|
|
31
|
+
7. real image content that must become an application asset rather than screenshot UI.
|
|
32
|
+
|
|
33
|
+
Create one shared shell specification for a series: viewport, safe areas, gutters, header geometry,
|
|
34
|
+
navigation geometry, surface treatment, type roles, icon family, content density, and state styling.
|
|
35
|
+
Reuse it unchanged unless a screen has a documented immersive or modal exception.
|
|
36
|
+
|
|
37
|
+
## Mobile geometry and typography gate
|
|
38
|
+
|
|
39
|
+
Use compiled Surface/ZORA tokens when they define these values. Where the owner leaves a design-
|
|
40
|
+
brief decision open, use these starting constraints and record them rather than asking an image
|
|
41
|
+
model to improvise:
|
|
42
|
+
|
|
43
|
+
- 16–24 logical-point horizontal gutters and preserved top/bottom safe areas;
|
|
44
|
+
- at least 44 by 44 pt touch bounds, or the stricter target-platform convention;
|
|
45
|
+
- screen title uses the semantic `h1` role, normally near 32/40 on mobile, one line when practical
|
|
46
|
+
and never more than two; reserve `display` for splash, onboarding, or genuine marketing moments;
|
|
47
|
+
- section titles use `h2`/`h3`; body content normally starts near 16/24; metadata never becomes tiny
|
|
48
|
+
merely to fit more content;
|
|
49
|
+
- prose measures approximately 45–75 characters per line, with reading surfaces optimized near the
|
|
50
|
+
comfortable middle of that range;
|
|
51
|
+
- bottom navigation uses one stable height, label baseline, icon family, selected treatment, and
|
|
52
|
+
safe-area inset across the series;
|
|
53
|
+
- a screen must expose its purpose, primary content, and primary action without competing giant
|
|
54
|
+
headings or decorative elements.
|
|
55
|
+
|
|
56
|
+
Inspect the skill-owned design rules before rendering:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
bun .agents/skills/zora-designer/scripts/audit.mjs catalog
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Apply every relevant `generation` rule from that catalog. The rubric belongs to this skill; owner
|
|
63
|
+
catalogs and design-system algorithms do not.
|
|
64
|
+
|
|
65
|
+
## Concept-image specification
|
|
66
|
+
|
|
67
|
+
Generate one image per distinct screen or state. Every prompt must include:
|
|
68
|
+
|
|
69
|
+
- exact logical viewport and portrait/landscape orientation;
|
|
70
|
+
- shared shell and active navigator item;
|
|
71
|
+
- resolved owner palette and type-role sizes, not only mood adjectives;
|
|
72
|
+
- exact screen title, copy that must be legible, hierarchy, components, and primary action;
|
|
73
|
+
- content quantity that fits the viewport at the declared type scale;
|
|
74
|
+
- invariants shared with the other screens;
|
|
75
|
+
- prohibitions against device frames, presentation boards, watermarks, illegible labels, invented
|
|
76
|
+
tabs, and oversized marketing typography on ordinary application screens.
|
|
77
|
+
|
|
78
|
+
For a series, keep the resolved configuration and shell verbatim across prompts. Do not generate a
|
|
79
|
+
single contact sheet as a substitute for individual screens unless the user explicitly requests one.
|
|
80
|
+
|
|
81
|
+
## Review and iterate
|
|
82
|
+
|
|
83
|
+
Inspect each output at its original dimensions. Reject and regenerate a screen when its title,
|
|
84
|
+
navigation, copy, safe areas, hierarchy, component semantics, or geometry diverges from the brief.
|
|
85
|
+
Use one targeted correction per iteration. Review the full ordered series together for shared shell,
|
|
86
|
+
type scale, navigation, color allocation, state continuity, and visual rhythm.
|
|
87
|
+
|
|
88
|
+
A visual review can validate visible composition only. It cannot prove runtime actions,
|
|
89
|
+
accessibility semantics, focus order, text scaling, or responsive behavior; record those as
|
|
90
|
+
not-assessable until implementation evidence exists.
|
|
@@ -1,137 +1,95 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
Use this workflow for `interactive`, `screen`, `screens`, and `template`. Audit uses the same
|
|
4
|
-
compiled target baseline when a redesign is requested; it never fills unknown observed values with
|
|
5
|
-
presets and pretends they were measured.
|
|
6
|
-
|
|
7
|
-
## 1. Establish mode, intent, and evidence
|
|
8
|
-
|
|
9
|
-
Record the audience, primary task, requested screen or ordered series, platform, input modes,
|
|
10
|
-
viewport constraints, target theme modes, available source artifacts, and requested deliverable.
|
|
11
|
-
Inspect the current manifest, theme, installed packages, ZORA metadata, fonts, and existing
|
|
12
|
-
`zora-designer.md` before prompting.
|
|
13
|
-
|
|
14
|
-
For an input image or series, decompose each screen into semantic regions. Give each region a stable
|
|
15
|
-
evidence ID, purpose, content/data responsibility, state and interaction needs, accessibility needs,
|
|
16
|
-
and its relationship to other regions. Preserve image order and original dimensions. Do not map by
|
|
17
|
-
appearance alone.
|
|
18
|
-
|
|
19
|
-
## 2. Build the design in dependency order
|
|
20
|
-
|
|
21
|
-
Interactive questions and compiler calls follow this exact order. Skip a question only when the
|
|
22
|
-
answer is already supplied or reliably discovered.
|
|
23
|
-
|
|
24
|
-
1. **Category and intent.** Read `APP_CATEGORIES` and `CATEGORY_PRESETS` from Templates. Show the
|
|
25
|
-
preset's ordered primary-color and font-family recommendations and explain that they are starting
|
|
26
|
-
points, not user stereotypes. Defer font activation until its availability and owning loader are
|
|
27
|
-
verified.
|
|
28
|
-
2. **Primary color.** Offer the preset's first recommended primary color and any verified project
|
|
29
|
-
brand color. Let the user accept or supply a color. The owner normalizes and validates it.
|
|
30
|
-
3. **Harmony.** Only after primary is resolved, show `recommendedHarmonies` and the installed owner
|
|
31
|
-
harmony catalog. Preserve the selected canonical identifier.
|
|
32
|
-
4. **Per-mode tone combination.** Show the preset recommendation first, then read all valid choices
|
|
33
|
-
from `TONE_PAIR_CATALOG`. Resolve light and dark independently with `resolveTonePair`; do not copy
|
|
34
|
-
a list into prose or code.
|
|
35
|
-
5. **Generated colors.** Call `resolveCategoryDesignPreset` or `compileCategoryDesign`. Inspect
|
|
36
|
-
generated owner roles and diagnostics; never hand-calculate secondary, accent, severity,
|
|
37
|
-
background, neutral, or on-colors.
|
|
38
|
-
6. **Computed Surface output.** Inspect `computedTheme.light.surfaceTheme` and
|
|
39
|
-
`computedTheme.dark.surfaceTheme`, including `ThemeTokens.colors`, `ThemeSemantics`, provenance,
|
|
40
|
-
selections, and diagnostics. Record `GeneratedColorRole` and `SemanticColorToken` names exactly.
|
|
41
|
-
7. **Composition.** Resolve density, shape, layout, navigation, content hierarchy, states, and exact
|
|
42
|
-
ZORA elements from metadata. Typography recommendations remain advisory until the chosen font is
|
|
43
|
-
verified and the owning app/module can load it; never install or bundle a font implicitly.
|
|
44
|
-
8. **Confirmation and output.** Show high-impact choices, their origins, diagnostics, gaps, and the
|
|
45
|
-
requested artifact. Persist runtime state only through the canonical manifest/contracts.
|
|
46
|
-
|
|
47
|
-
Use separate light/dark tone pairs and owner compilation even when the two modes share a primary
|
|
48
|
-
seed. Never generate dark mode by inversion.
|
|
49
|
-
|
|
50
|
-
## 3. Inspect installed owner APIs
|
|
51
|
-
|
|
52
|
-
Run from the target repository:
|
|
1
|
+
# Owner-Backed Interactive and Template Workflow
|
|
53
2
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
The output reports installed versions, category presets, tone pairs, component metadata, recipe
|
|
59
|
-
metadata, and required exports. An error names the missing package/export, minimum owner release,
|
|
60
|
-
and update action. There is no fallback calculation.
|
|
61
|
-
|
|
62
|
-
For deterministic composition, provide JSON to:
|
|
63
|
-
|
|
64
|
-
```text
|
|
65
|
-
bun .agents/skills/zora-designer/scripts/owner-api.mjs compose design-input.json
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
The input contains `category`, optional `theme` overrides, `navigator`, `screens`, and optional
|
|
69
|
-
`regions`. Each region has `id`, `requestedCapability`, `screenId`, `parentNodeId`, an optional exact
|
|
70
|
-
`component`, explicit `props`, and `evidenceId`. The helper:
|
|
71
|
-
|
|
72
|
-
- compiles the category through Templates and the released ZORA compiler;
|
|
73
|
-
- validates exact component names and props against `ZORA_COMPONENT_META`;
|
|
74
|
-
- emits metadata-derived `MissingElement` nodes for unresolved regions;
|
|
75
|
-
- composes the canonical manifest in draft mode while a gap exists;
|
|
76
|
-
- reports `applicationGate: blocked` and owner actions for every gap;
|
|
77
|
-
- preserves Templates and ZORA diagnostics.
|
|
78
|
-
|
|
79
|
-
The agent remains responsible for semantic matching. A requested component name is an explicit
|
|
80
|
-
decision to validate, not a fuzzy-search request.
|
|
3
|
+
Use this workflow for `interactive`, `screen`, `screens`, and `template`. Ask only about unresolved
|
|
4
|
+
decisions, one decision at a time. Show the owner recommendation first with a short reason. Do not
|
|
5
|
+
silently accept later recommendations because an earlier answer was brief.
|
|
81
6
|
|
|
82
|
-
##
|
|
7
|
+
## 1. Inspect intent and owners
|
|
83
8
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
selected/focus states. Define narrow and wide behavior where applicable.
|
|
87
|
-
|
|
88
|
-
For a series also define the shared shell, canonical route topology, state continuity, back/cancel
|
|
89
|
-
behavior, shared recipes and terminology, and one common theme. A concept image series must remain
|
|
90
|
-
ordered and visually coherent. Label every generated image as a concept; replace it with an actual
|
|
91
|
-
runtime capture only after running the manifest.
|
|
92
|
-
|
|
93
|
-
## 5. Audit a URL, image, or series
|
|
94
|
-
|
|
95
|
-
For a URL, capture the relevant modes, viewports, and states using a browser/runtime tool. For one
|
|
96
|
-
image or a series, retain original dimensions and input order. Every evidence item records location,
|
|
97
|
-
observation, evidence level, confidence factor, reproduction, and limitations.
|
|
98
|
-
|
|
99
|
-
Read [audit.md](audit.md), create a criterion input JSON, and run:
|
|
9
|
+
Read repository instructions, an existing manifest or `zora-designer.md`, installed fonts, platform
|
|
10
|
+
configuration, and relevant screens. Run:
|
|
100
11
|
|
|
101
12
|
```text
|
|
102
|
-
bun .agents/skills/zora-designer/scripts/
|
|
13
|
+
bun .agents/skills/zora-designer/scripts/owner-api.mjs inspect
|
|
103
14
|
```
|
|
104
15
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
16
|
+
The output is the choice source. Do not use remembered categories, color options, tone pairs,
|
|
17
|
+
navigator types, ZORA elements, or events. Resolve explicit user input before project state,
|
|
18
|
+
existing brief values, category recommendations, and global defaults. Record the origin of every
|
|
19
|
+
resolved decision.
|
|
20
|
+
|
|
21
|
+
## 2. Ask in dependency order
|
|
22
|
+
|
|
23
|
+
Advance through this sequence. Skip only a value already supplied or reliably discovered.
|
|
24
|
+
|
|
25
|
+
1. **Category and intent.** Resolve the canonical app category, product name, audience, and primary
|
|
26
|
+
task. For `screen` and `screens`, audience and primary task are required.
|
|
27
|
+
2. **Platform and theme coverage.** Resolve mobile, desktop, or responsive scope; technology;
|
|
28
|
+
input modes; target viewport or content constraints; light, dark, or both; and the default mode
|
|
29
|
+
when relevant.
|
|
30
|
+
3. **Primary color.** Show the category recommendations in owner order and any verified project
|
|
31
|
+
brand color. Let the user accept one or supply a CSS color. Preserve the brand seed; the owner
|
|
32
|
+
compiler selects accessible functional steps.
|
|
33
|
+
4. **Harmony.** After primary is resolved, show the category recommendation first and only the
|
|
34
|
+
harmony identifiers reported by Color Theory. Explain the visual energy and complexity using the
|
|
35
|
+
owner catalog descriptions.
|
|
36
|
+
5. **Tone pairs.** After harmony is resolved, offer the category's separate light and dark
|
|
37
|
+
recommendations, followed by mode-compatible owner entries. Explain that `X-on-Y` means accent
|
|
38
|
+
family on foundation family; structural foregrounds remain independently resolved semantic
|
|
39
|
+
tokens.
|
|
40
|
+
6. **Typography and profile.** Resolve a verified body font, optional heading pairing, density,
|
|
41
|
+
shape, motion, and contrast target. Do not install fonts. Offer the category/profile
|
|
42
|
+
recommendation first and allow the user to accept the recommended system settings together.
|
|
43
|
+
7. **Screens.** Ask for the ordered screen list and purpose of each screen. Then establish the
|
|
44
|
+
primary action, essential content, required data states, and continuity across the series. Do not
|
|
45
|
+
generate images yet.
|
|
46
|
+
8. **Navigator.** After the screen topology exists, offer only Contracts navigator types and map
|
|
47
|
+
every route, initial route, hidden/detail route, back/cancel path, and primary-navigation label.
|
|
48
|
+
9. **Compile and confirm.** Compile both theme modes through the owner helper. Summarize high-impact
|
|
49
|
+
decisions, origins, diagnostics, unsupported capabilities, screen order, and route topology. Ask
|
|
50
|
+
for confirmation before creating screen images, code, or a template.
|
|
51
|
+
|
|
52
|
+
If the user changes an earlier decision, invalidate and revisit only dependent later decisions.
|
|
53
|
+
Changing primary invalidates harmony-derived compilation and tone output but does not erase the
|
|
54
|
+
screen brief. Changing the screen list invalidates navigator confirmation.
|
|
55
|
+
|
|
56
|
+
## 3. Compile owner output
|
|
57
|
+
|
|
58
|
+
Provide the resolved `category`, theme overrides, navigator, screens, and region decisions to:
|
|
115
59
|
|
|
116
60
|
```text
|
|
117
|
-
bun .agents/skills/zora-designer/scripts/
|
|
61
|
+
bun .agents/skills/zora-designer/scripts/owner-api.mjs compose design-input.json
|
|
118
62
|
```
|
|
119
63
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
-
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
|
|
135
|
-
-
|
|
136
|
-
|
|
137
|
-
|
|
64
|
+
Use owner-returned theme configuration, generated roles, computed Surface themes, diagnostics,
|
|
65
|
+
component metadata, and manifest composition. Light and dark are independent compilations; never
|
|
66
|
+
derive dark mode by inversion. A failed owner diagnostic remains visible. Do not replace it with
|
|
67
|
+
local math or an undocumented token.
|
|
68
|
+
|
|
69
|
+
For every region, validate its exact component name, props, allowed-parent relationship, and event
|
|
70
|
+
metadata. Use supported navigation and actions when available. Represent missing capability without
|
|
71
|
+
removing the intended flow or inventing a contract.
|
|
72
|
+
|
|
73
|
+
## 4. Produce the requested deliverable
|
|
74
|
+
|
|
75
|
+
- `interactive`: write or return the confirmed `zora-designer.md`; do not produce screen images or
|
|
76
|
+
implementation unless requested.
|
|
77
|
+
- `screen` or `screens`: read [screens.md](screens.md), create the screen specification, then produce
|
|
78
|
+
only the requested image, code, or manifest deliverable.
|
|
79
|
+
- `template`: author a complete `AppManifest`, validate it in release mode, generate or extract every
|
|
80
|
+
required runtime image, and run the Templates scaffolder.
|
|
81
|
+
|
|
82
|
+
For design-first templates, store reference images under `assets/screens/` and crop only real image
|
|
83
|
+
content into `assets/images/`. For direct authoring, generate application images directly under
|
|
84
|
+
`assets/images/`. Runtime media never references a complete screen image.
|
|
85
|
+
|
|
86
|
+
## 5. Delivery checks
|
|
87
|
+
|
|
88
|
+
- every decision is explicit, discovered, or carries a recorded default origin;
|
|
89
|
+
- both theme modes compile without hidden local fallback calculations;
|
|
90
|
+
- the screen model and navigator agree;
|
|
91
|
+
- every selected ZORA node and prop is metadata-supported;
|
|
92
|
+
- supported events/actions are bound and unsupported behavior is recorded;
|
|
93
|
+
- screen generation passes the composition gate;
|
|
94
|
+
- release templates contain one default-exported `createAppManifest()` and bundled media paths;
|
|
95
|
+
- the complete manifest validates against the installed owner contract.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { access, readdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { loadContractsApi } from './owner-api.mjs';
|
|
7
|
+
|
|
8
|
+
const CATEGORY_ROOT = 'src/templates/categories';
|
|
9
|
+
|
|
10
|
+
/*** Regenerate the portable template catalog from canonical template directories. */
|
|
11
|
+
export async function generateTemplateCatalog(targetDirectory = process.cwd(), appCategories) {
|
|
12
|
+
const root = resolve(targetDirectory);
|
|
13
|
+
const canonicalAppCategories = appCategories ?? (await loadContractsApi(root)).APP_CATEGORIES;
|
|
14
|
+
const categoriesRoot = join(root, CATEGORY_ROOT);
|
|
15
|
+
const definitions = [];
|
|
16
|
+
|
|
17
|
+
for (const categoryEntry of await readDirectories(categoriesRoot)) {
|
|
18
|
+
const category = categoryEntry.name.replaceAll('-', '_');
|
|
19
|
+
if (!canonicalAppCategories.includes(category)) {
|
|
20
|
+
throw new Error(`Unknown app category directory: ${categoryEntry.name}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const categoryRoot = join(categoriesRoot, categoryEntry.name);
|
|
24
|
+
for (const templateEntry of await readDirectories(categoryRoot)) {
|
|
25
|
+
const manifestPath = join(categoryRoot, templateEntry.name, 'createAppManifest.ts');
|
|
26
|
+
if (!(await pathExists(manifestPath))) continue;
|
|
27
|
+
definitions.push({
|
|
28
|
+
category,
|
|
29
|
+
categoryDirectory: categoryEntry.name,
|
|
30
|
+
slug: templateEntry.name,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
definitions.sort((left, right) =>
|
|
36
|
+
`${left.category}/${left.slug}`.localeCompare(`${right.category}/${right.slug}`),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const imports = definitions
|
|
40
|
+
.map(
|
|
41
|
+
(definition, index) =>
|
|
42
|
+
`import createAppManifest${index} from './categories/${definition.categoryDirectory}/${definition.slug}/createAppManifest';`,
|
|
43
|
+
)
|
|
44
|
+
.join('\n');
|
|
45
|
+
const entries = definitions
|
|
46
|
+
.map(
|
|
47
|
+
(definition, index) => ` {
|
|
48
|
+
category: '${definition.category}',
|
|
49
|
+
slug: '${definition.slug}',
|
|
50
|
+
sourceRoot: '${CATEGORY_ROOT}/${definition.categoryDirectory}/${definition.slug}',
|
|
51
|
+
createAppManifest: createAppManifest${index},
|
|
52
|
+
},`,
|
|
53
|
+
)
|
|
54
|
+
.join('\n');
|
|
55
|
+
const definitionsSource = definitions.length === 0 ? '[]' : `[\n${entries}\n]`;
|
|
56
|
+
|
|
57
|
+
const source = `${imports}${imports ? '\n\n' : ''}import type { TemplateDefinition } from './catalog';
|
|
58
|
+
|
|
59
|
+
export const TEMPLATE_DEFINITIONS: readonly TemplateDefinition[] = ${definitionsSource};
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const outputPath = join(root, 'src/templates/catalog.generated.ts');
|
|
63
|
+
await writeFile(outputPath, source, 'utf8');
|
|
64
|
+
return { outputPath, templateCount: definitions.length };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/*** Read child directories in stable lexical order. */
|
|
68
|
+
async function readDirectories(directory) {
|
|
69
|
+
return (await readdir(directory, { withFileTypes: true }))
|
|
70
|
+
.filter((entry) => entry.isDirectory())
|
|
71
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/*** Return whether a filesystem path exists. */
|
|
75
|
+
async function pathExists(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
await access(filePath);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (import.meta.main) {
|
|
85
|
+
const targetDirectory = process.argv[2] ?? process.cwd();
|
|
86
|
+
const result = await generateTemplateCatalog(targetDirectory);
|
|
87
|
+
console.log(`Generated ${result.templateCount} template catalog entries.`);
|
|
88
|
+
}
|
|
@@ -5,11 +5,23 @@ import { dirname, join, parse, resolve } from 'node:path';
|
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
|
|
7
7
|
const OWNER_RELEASES = {
|
|
8
|
+
colorTheory: { packageName: '@ankhorage/color-theory', minimumVersion: '0.3.0' },
|
|
9
|
+
contracts: { packageName: '@ankhorage/contracts', minimumVersion: '8.2.0' },
|
|
8
10
|
templates: { packageName: '@ankhorage/templates', minimumVersion: '8.0.0' },
|
|
9
11
|
zora: { packageName: '@ankhorage/zora', minimumVersion: '4.0.0' },
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
const OWNER_REQUIREMENTS = {
|
|
15
|
+
colorTheory: {
|
|
16
|
+
...OWNER_RELEASES.colorTheory,
|
|
17
|
+
specifier: '@ankhorage/color-theory',
|
|
18
|
+
exports: ['COLOR_HARMONIES', 'COLOR_HARMONY_CATALOG'],
|
|
19
|
+
},
|
|
20
|
+
contracts: {
|
|
21
|
+
...OWNER_RELEASES.contracts,
|
|
22
|
+
specifier: '@ankhorage/contracts',
|
|
23
|
+
exports: ['APP_CATEGORIES', 'NAVIGATOR_TYPES'],
|
|
24
|
+
},
|
|
13
25
|
templates: {
|
|
14
26
|
...OWNER_RELEASES.templates,
|
|
15
27
|
specifier: '@ankhorage/templates',
|
|
@@ -45,35 +57,58 @@ export async function loadOwnerApis(targetDirectory = process.cwd()) {
|
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
return {
|
|
60
|
+
colorTheory: loaded.colorTheory.module,
|
|
61
|
+
contracts: loaded.contracts.module,
|
|
48
62
|
templates: loaded.templates.module,
|
|
49
63
|
zoraTheme: loaded.zoraTheme.module,
|
|
50
64
|
zoraMetadata: loaded.zoraMetadata.module,
|
|
51
65
|
versions: {
|
|
66
|
+
colorTheory: loaded.colorTheory.version,
|
|
67
|
+
contracts: loaded.contracts.version,
|
|
52
68
|
templates: loaded.templates.version,
|
|
53
69
|
zora: loaded.zoraTheme.version,
|
|
54
70
|
},
|
|
55
71
|
};
|
|
56
72
|
}
|
|
57
73
|
|
|
74
|
+
/*** Load only Contracts for tooling that runs before another owner package has been built. */
|
|
75
|
+
export async function loadContractsApi(targetDirectory = process.cwd()) {
|
|
76
|
+
return (await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.contracts)).module;
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
/*** Return installed catalogs and metadata names without copying owner definitions. */
|
|
59
80
|
export async function inspectOwnerApis(targetDirectory = process.cwd()) {
|
|
60
81
|
const owners = await loadOwnerApis(targetDirectory);
|
|
82
|
+
const componentMetadata = Object.values(owners.zoraMetadata.ZORA_COMPONENT_META);
|
|
61
83
|
return {
|
|
62
84
|
versions: owners.versions,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
recommendedHarmonies: preset.recommendedHarmonies,
|
|
68
|
-
tonePairs: preset.tonePairs,
|
|
69
|
-
})),
|
|
85
|
+
appCategories: owners.contracts.APP_CATEGORIES,
|
|
86
|
+
categoryPresets: owners.templates.CATEGORY_PRESETS,
|
|
87
|
+
harmonyIds: owners.colorTheory.COLOR_HARMONIES,
|
|
88
|
+
harmonies: owners.colorTheory.COLOR_HARMONY_CATALOG,
|
|
70
89
|
tonePairs: owners.templates.TONE_PAIR_CATALOG,
|
|
71
|
-
|
|
90
|
+
navigatorTypes: owners.contracts.NAVIGATOR_TYPES,
|
|
91
|
+
components: componentMetadata.map((meta) => meta.name).sort(),
|
|
92
|
+
events: componentMetadata
|
|
93
|
+
.flatMap((meta) =>
|
|
94
|
+
Object.values(meta.events ?? {}).map((event) => ({
|
|
95
|
+
component: meta.name,
|
|
96
|
+
eventType: event.eventType,
|
|
97
|
+
label: event.label,
|
|
98
|
+
description: event.description,
|
|
99
|
+
payloadFields: event.payloadFields,
|
|
100
|
+
})),
|
|
101
|
+
)
|
|
102
|
+
.sort((left, right) =>
|
|
103
|
+
`${left.component}:${left.eventType}`.localeCompare(
|
|
104
|
+
`${right.component}:${right.eventType}`,
|
|
105
|
+
),
|
|
106
|
+
),
|
|
72
107
|
themeRecipes: Object.keys(owners.zoraMetadata.ZORA_THEME_RECIPE_META).sort(),
|
|
73
108
|
};
|
|
74
109
|
}
|
|
75
110
|
|
|
76
|
-
/***
|
|
111
|
+
/*** Compose one design without turning missing runtime/UI capabilities into a design blocker. */
|
|
77
112
|
export async function composeDesign(input, targetDirectory = process.cwd()) {
|
|
78
113
|
const owners = await loadOwnerApis(targetDirectory);
|
|
79
114
|
assertRecord(input, 'Design input');
|
|
@@ -90,7 +125,6 @@ export async function composeDesign(input, targetDirectory = process.cwd()) {
|
|
|
90
125
|
const design = owners.templates.compileCategoryDesign(input.category, input.theme ?? {});
|
|
91
126
|
const { computedTheme, ...resolvedDesign } = design;
|
|
92
127
|
const requestedAuthoringState = input.authoringState === 'release' ? 'release' : 'draft';
|
|
93
|
-
const authoringState = regionResult.gaps.length === 0 ? requestedAuthoringState : 'draft';
|
|
94
128
|
const composition = owners.templates.composeCategoryAppManifest({
|
|
95
129
|
category: input.category,
|
|
96
130
|
name: input.name,
|
|
@@ -103,14 +137,14 @@ export async function composeDesign(input, targetDirectory = process.cwd()) {
|
|
|
103
137
|
modules: input.modules,
|
|
104
138
|
modulesConfig: input.modulesConfig,
|
|
105
139
|
theme: input.theme,
|
|
106
|
-
authoringState,
|
|
140
|
+
authoringState: requestedAuthoringState,
|
|
107
141
|
});
|
|
108
142
|
const ownerDiagnostics = [
|
|
109
143
|
...design.diagnostics,
|
|
110
144
|
...computedTheme.diagnostics,
|
|
111
145
|
...composition.diagnostics,
|
|
112
146
|
];
|
|
113
|
-
const
|
|
147
|
+
const blockers = ownerDiagnostics.filter((diagnostic) => diagnostic.severity === 'error');
|
|
114
148
|
|
|
115
149
|
return {
|
|
116
150
|
owners: owners.versions,
|
|
@@ -118,14 +152,15 @@ export async function composeDesign(input, targetDirectory = process.cwd()) {
|
|
|
118
152
|
computedTheme,
|
|
119
153
|
composition,
|
|
120
154
|
regionDiagnostics: regionResult.diagnostics,
|
|
155
|
+
capabilityGaps: regionResult.gaps,
|
|
121
156
|
ownerDiagnostics,
|
|
122
157
|
requestedAuthoringState,
|
|
123
|
-
applicationGate: blocked ? 'blocked' : 'pass',
|
|
124
|
-
blockers
|
|
158
|
+
applicationGate: composition.status === 'blocked' ? 'blocked' : 'pass',
|
|
159
|
+
blockers,
|
|
125
160
|
};
|
|
126
161
|
}
|
|
127
162
|
|
|
128
|
-
/*** Resolve explicit region
|
|
163
|
+
/*** Resolve explicit region decisions using exact metadata or a visible non-blocking Box placeholder. */
|
|
129
164
|
export function resolveRegionNodes(screens, regions, componentMeta) {
|
|
130
165
|
const resolvedScreens = structuredClone(screens);
|
|
131
166
|
const diagnostics = [];
|
|
@@ -152,7 +187,6 @@ export function resolveRegionNodes(screens, regions, componentMeta) {
|
|
|
152
187
|
return { screens: resolvedScreens, diagnostics, gaps };
|
|
153
188
|
}
|
|
154
189
|
|
|
155
|
-
/*** Resolve one exact metadata-backed component or the owner-defined MissingElement placeholder. */
|
|
156
190
|
function resolveRegionNode(region, componentMeta) {
|
|
157
191
|
const component = typeof region.component === 'string' ? region.component : null;
|
|
158
192
|
const meta = component === null ? null : componentMeta[component];
|
|
@@ -170,36 +204,30 @@ function resolveRegionNode(region, componentMeta) {
|
|
|
170
204
|
};
|
|
171
205
|
}
|
|
172
206
|
|
|
173
|
-
const
|
|
174
|
-
if (!
|
|
175
|
-
throw new Error(
|
|
176
|
-
'Installed @ankhorage/zora metadata does not expose the canonical MissingElement contract.',
|
|
177
|
-
);
|
|
207
|
+
const placeholderMeta = componentMeta.Box;
|
|
208
|
+
if (!placeholderMeta?.directManifestNode) {
|
|
209
|
+
throw new Error('Installed @ankhorage/zora metadata does not expose Box as a manifest node.');
|
|
178
210
|
}
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
typeof region.reason === 'string' && region.reason.trim() !== ''
|
|
184
|
-
? region.reason
|
|
185
|
-
: `No exact metadata-supported ZORA element was selected for region "${region.id}".`,
|
|
186
|
-
...(typeof region.evidenceId === 'string' ? { evidenceId: region.evidenceId } : {}),
|
|
187
|
-
};
|
|
188
|
-
assertSupportedProps(props, missingMeta, region.id);
|
|
211
|
+
const reason =
|
|
212
|
+
typeof region.reason === 'string' && region.reason.trim() !== ''
|
|
213
|
+
? region.reason
|
|
214
|
+
: `No exact metadata-supported ZORA element was selected for region "${region.id}".`;
|
|
189
215
|
const gap = {
|
|
190
|
-
id: `
|
|
191
|
-
scope: '
|
|
192
|
-
owner: '@ankhorage/zora',
|
|
216
|
+
id: `capability-gap:${region.id}`,
|
|
217
|
+
scope: 'capability',
|
|
193
218
|
regionId: region.id,
|
|
194
219
|
requestedCapability: region.requestedCapability,
|
|
195
220
|
evidenceId: region.evidenceId ?? null,
|
|
196
|
-
|
|
197
|
-
reason: props.reason,
|
|
198
|
-
unblockCondition: 'Replace MissingElement with a released exact ZORA element and revalidate.',
|
|
221
|
+
reason,
|
|
199
222
|
};
|
|
200
223
|
return {
|
|
201
|
-
node: { id: region.id, type:
|
|
202
|
-
diagnostic: {
|
|
224
|
+
node: { id: region.id, type: placeholderMeta.name, props: {} },
|
|
225
|
+
diagnostic: {
|
|
226
|
+
regionId: region.id,
|
|
227
|
+
status: 'placeholder',
|
|
228
|
+
component: placeholderMeta.name,
|
|
229
|
+
...gap,
|
|
230
|
+
},
|
|
203
231
|
gap,
|
|
204
232
|
};
|
|
205
233
|
}
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
|
|
7
|
+
import { generateTemplateCatalog } from './generate-template-catalog.mjs';
|
|
7
8
|
import { loadOwnerApis } from './owner-api.mjs';
|
|
8
9
|
|
|
9
|
-
/***
|
|
10
|
+
/*** Scaffold one complete portable template and refresh filesystem discovery. */
|
|
10
11
|
export async function scaffoldTemplate(input) {
|
|
11
12
|
assertRecord(input, 'Scaffold input');
|
|
12
|
-
for (const field of ['targetDirectory', 'category', '
|
|
13
|
+
for (const field of ['targetDirectory', 'category', 'slug']) {
|
|
13
14
|
assertNonEmptyString(input[field], field);
|
|
14
15
|
}
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
['default', 'starter'].includes(input.templateId)
|
|
18
|
-
) {
|
|
19
|
-
throw new Error('templateId must be a non-reserved kebab-case identifier.');
|
|
16
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(input.slug)) {
|
|
17
|
+
throw new Error('slug must be a kebab-case identifier.');
|
|
20
18
|
}
|
|
21
19
|
assertRecord(input.manifest, 'manifest');
|
|
22
20
|
|
|
@@ -30,165 +28,85 @@ export async function scaffoldTemplate(input) {
|
|
|
30
28
|
if (input.manifest.metadata?.category !== input.category) {
|
|
31
29
|
throw new Error('Scaffold category must match manifest.metadata.category.');
|
|
32
30
|
}
|
|
31
|
+
if (input.manifest.metadata?.slug !== input.slug) {
|
|
32
|
+
throw new Error('Scaffold slug must match manifest.metadata.slug.');
|
|
33
|
+
}
|
|
33
34
|
|
|
34
35
|
const owners = await loadOwnerApis(targetDirectory);
|
|
35
36
|
const composition = owners.templates.validateTemplateManifest(input.manifest, 'release');
|
|
36
|
-
if (composition.status !== 'ready') {
|
|
37
|
-
throw new Error(
|
|
38
|
-
`Manifest is not release-ready: ${composition.diagnostics.map((item) => item.message).join('; ')}`,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
37
|
const manifest = owners.templates.assertTemplateManifestReady(composition);
|
|
42
|
-
|
|
38
|
+
|
|
43
39
|
const categoryDirectory = resolve(
|
|
44
40
|
targetDirectory,
|
|
45
|
-
'src/templates/
|
|
46
|
-
|
|
41
|
+
'src/templates/categories',
|
|
42
|
+
input.category.replaceAll('_', '-'),
|
|
47
43
|
);
|
|
48
44
|
assertInside(targetDirectory, categoryDirectory);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
|
|
46
|
+
const templateDirectory = resolve(categoryDirectory, input.slug);
|
|
47
|
+
assertInside(categoryDirectory, templateDirectory);
|
|
48
|
+
if (await pathExists(templateDirectory)) {
|
|
52
49
|
throw new Error(
|
|
53
|
-
`Template source already exists: ${relative(targetDirectory,
|
|
50
|
+
`Template source already exists: ${relative(targetDirectory, templateDirectory)}`,
|
|
54
51
|
);
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
69
|
-
const files = createTemplateFiles({ manifest, manifestName, factoryName });
|
|
70
|
-
|
|
71
|
-
await mkdir(variantDirectory, { recursive: true });
|
|
72
|
-
for (const [fileName, contents] of Object.entries(files)) {
|
|
73
|
-
await writeFile(join(variantDirectory, fileName), contents);
|
|
74
|
-
}
|
|
75
|
-
await writeFile(registryPath, registrySourceUpdated);
|
|
54
|
+
const screensDirectory = join(templateDirectory, 'assets', 'screens');
|
|
55
|
+
const imagesDirectory = join(templateDirectory, 'assets', 'images');
|
|
56
|
+
await mkdir(screensDirectory, { recursive: true });
|
|
57
|
+
await mkdir(imagesDirectory, { recursive: true });
|
|
58
|
+
await writeFile(
|
|
59
|
+
join(templateDirectory, 'createAppManifest.ts'),
|
|
60
|
+
createManifestSource(manifest),
|
|
61
|
+
'utf8',
|
|
62
|
+
);
|
|
63
|
+
await rm(join(categoryDirectory, '.gitkeep'), { force: true });
|
|
64
|
+
await generateTemplateCatalog(targetDirectory, owners.contracts.APP_CATEGORIES);
|
|
76
65
|
|
|
77
66
|
return {
|
|
78
67
|
targetDirectory,
|
|
79
|
-
|
|
80
|
-
createdFiles:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
templateDirectory: relative(targetDirectory, templateDirectory),
|
|
69
|
+
createdFiles: [relative(targetDirectory, join(templateDirectory, 'createAppManifest.ts'))],
|
|
70
|
+
assetDirectories: [
|
|
71
|
+
relative(targetDirectory, screensDirectory),
|
|
72
|
+
relative(targetDirectory, imagesDirectory),
|
|
73
|
+
],
|
|
84
74
|
};
|
|
85
75
|
}
|
|
86
76
|
|
|
87
|
-
/***
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
export const ${manifestName} = ${JSON.stringify(manifest, null, 2)} satisfies AppManifest;
|
|
92
|
-
`;
|
|
93
|
-
const templateSource = `import type { AppManifest } from '@ankhorage/contracts';
|
|
77
|
+
/*** Serialize one complete manifest as the template's canonical default export. */
|
|
78
|
+
function createManifestSource(manifest) {
|
|
79
|
+
return `import type { AppManifest } from '@ankhorage/contracts';
|
|
94
80
|
|
|
95
|
-
|
|
96
|
-
import { ${manifestName} } from './manifest';
|
|
81
|
+
const manifest = ${JSON.stringify(manifest, null, 2)} satisfies AppManifest;
|
|
97
82
|
|
|
98
|
-
/*** Create the
|
|
99
|
-
export function
|
|
100
|
-
|
|
101
|
-
if (theme === undefined) {
|
|
102
|
-
throw new Error('The authored template requires one resolved theme.');
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
...${manifestName},
|
|
106
|
-
metadata: {
|
|
107
|
-
...${manifestName}.metadata,
|
|
108
|
-
name: seed.appName,
|
|
109
|
-
slug: seed.slug,
|
|
110
|
-
version: seed.version ?? ${manifestName}.metadata.version,
|
|
111
|
-
themeId: theme.id,
|
|
112
|
-
},
|
|
113
|
-
themes: [theme],
|
|
114
|
-
activeThemeId: theme.id,
|
|
115
|
-
};
|
|
83
|
+
/*** Create the complete portable application manifest for this template. */
|
|
84
|
+
export default function createAppManifest(): AppManifest {
|
|
85
|
+
return structuredClone(manifest);
|
|
116
86
|
}
|
|
117
87
|
`;
|
|
118
|
-
return {
|
|
119
|
-
'index.ts': `export { ${factoryName} } from './template';\n`,
|
|
120
|
-
'manifest.ts': manifestSource,
|
|
121
|
-
'template.ts': templateSource,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/*** Add one stable import and definition to an existing category registry. */
|
|
126
|
-
function updateCategoryRegistry(source, definition) {
|
|
127
|
-
const importLine = `import { ${definition.factoryName} } from './${definition.templateId}';`;
|
|
128
|
-
if (source.includes(`id: '${definition.templateId}'`) || source.includes(importLine)) {
|
|
129
|
-
throw new Error(`Template is already registered: ${definition.templateId}`);
|
|
130
|
-
}
|
|
131
|
-
const exportMarker = '\nexport const ';
|
|
132
|
-
const exportIndex = source.indexOf(exportMarker);
|
|
133
|
-
if (exportIndex < 0) {
|
|
134
|
-
throw new Error('Category registry does not expose its canonical template array.');
|
|
135
|
-
}
|
|
136
|
-
const prefixLines = source.slice(0, exportIndex).trimEnd().split('\n');
|
|
137
|
-
const relativeImports = [
|
|
138
|
-
...prefixLines.filter((line) => /^import .* from '\.\//u.test(line)),
|
|
139
|
-
importLine,
|
|
140
|
-
].sort((left, right) => left.localeCompare(right));
|
|
141
|
-
const preservedPrefix = prefixLines.filter((line) => !/^import .* from '\.\//u.test(line));
|
|
142
|
-
const withImport = `${[...preservedPrefix, ...relativeImports].join('\n')}\n${source.slice(exportIndex + 1)}`;
|
|
143
|
-
const closeMarker = '] satisfies readonly CategoryStarterTemplateDefinition[];';
|
|
144
|
-
const closeIndex = withImport.indexOf(closeMarker);
|
|
145
|
-
if (closeIndex < 0) {
|
|
146
|
-
throw new Error('Category registry is missing its canonical definition-array terminator.');
|
|
147
|
-
}
|
|
148
|
-
const entry = ` {
|
|
149
|
-
id: '${escapeSingleQuoted(definition.templateId)}',
|
|
150
|
-
label: '${escapeSingleQuoted(definition.label)}',
|
|
151
|
-
description: '${escapeSingleQuoted(definition.description)}',
|
|
152
|
-
create: ${definition.factoryName},
|
|
153
|
-
},
|
|
154
|
-
`;
|
|
155
|
-
return `${withImport.slice(0, closeIndex)}${entry}${withImport.slice(closeIndex)}`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/*** Convert kebab-case identifiers to a PascalCase source symbol. */
|
|
159
|
-
function toPascalCase(value) {
|
|
160
|
-
return value
|
|
161
|
-
.split('-')
|
|
162
|
-
.map((segment) => segment[0].toUpperCase() + segment.slice(1))
|
|
163
|
-
.join('');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/*** Convert kebab-case identifiers to an uppercase constant name. */
|
|
167
|
-
function toConstantCase(value) {
|
|
168
|
-
return value.replaceAll('-', '_').toUpperCase();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/*** Escape content placed in generated single-quoted TypeScript strings. */
|
|
172
|
-
function escapeSingleQuoted(value) {
|
|
173
|
-
return value.replaceAll('\\', '\\\\').replaceAll("'", "\\'");
|
|
174
88
|
}
|
|
175
89
|
|
|
176
90
|
/*** Assert that a resolved output remains inside its declared owner directory. */
|
|
177
|
-
function assertInside(
|
|
178
|
-
const relativePath = relative(
|
|
179
|
-
if (
|
|
180
|
-
|
|
91
|
+
function assertInside(parentPath, childPath) {
|
|
92
|
+
const relativePath = relative(parentPath, childPath);
|
|
93
|
+
if (
|
|
94
|
+
relativePath === '' ||
|
|
95
|
+
relativePath === '..' ||
|
|
96
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
97
|
+
relativePath.startsWith('../')
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(`Template path escapes its owner directory: ${childPath}`);
|
|
181
100
|
}
|
|
182
101
|
}
|
|
183
102
|
|
|
184
|
-
/*** Return whether a filesystem path
|
|
185
|
-
async function pathExists(
|
|
103
|
+
/*** Return whether a filesystem path exists. */
|
|
104
|
+
async function pathExists(filePath) {
|
|
186
105
|
try {
|
|
187
|
-
await access(
|
|
106
|
+
await access(filePath);
|
|
188
107
|
return true;
|
|
189
|
-
} catch
|
|
190
|
-
|
|
191
|
-
throw error;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
192
110
|
}
|
|
193
111
|
}
|
|
194
112
|
|
|
@@ -209,8 +127,10 @@ function assertNonEmptyString(value, label) {
|
|
|
209
127
|
/*** Run deterministic Templates source scaffolding from one JSON input. */
|
|
210
128
|
async function main() {
|
|
211
129
|
const [inputPath] = process.argv.slice(2);
|
|
212
|
-
if (!inputPath)
|
|
213
|
-
|
|
130
|
+
if (!inputPath) {
|
|
131
|
+
throw new Error('Usage: scaffold-template.mjs <scaffold-input.json>');
|
|
132
|
+
}
|
|
133
|
+
const input = JSON.parse(await readFile(resolve(inputPath), 'utf8'));
|
|
214
134
|
console.log(JSON.stringify(await scaffoldTemplate(input), null, 2));
|
|
215
135
|
}
|
|
216
136
|
|
package/package.json
CHANGED