@port60/template-kit 0.8.0 → 0.10.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/README.md +10 -3
- package/bin/cli.mjs +26 -4
- package/package.json +1 -1
- package/src/commands/content.mjs +42 -0
- package/src/commands/create.mjs +10 -7
- package/src/commands/dev.mjs +134 -8
- package/src/commands/model.mjs +34 -0
- package/src/commands/packageCmd.mjs +3 -3
- package/src/commands/refresh.mjs +48 -0
- package/src/commands/upgrade.mjs +44 -0
- package/src/commands/validate.mjs +2 -3
- package/src/lib/agentsMd.mjs +78 -21
- package/src/vendor/contract/v1/behaviours.json +129 -0
- package/src/vendor/contract/v1/content-bounds.json +3 -3
- package/src/vendor/contract/v1/content-model.json +193 -0
- package/src/vendor/contract/v1/context.json +1985 -141
- package/src/vendor/contract/v1/dialect.json +3 -3
- package/src/vendor/contract/v1/fonts.json +1 -1
- package/src/vendor/contract/v1/imagery.json +4 -4
- package/src/vendor/contract/v1/islands.json +256 -110
- package/src/vendor/contract/v1/layout.json +25 -0
- package/src/vendor/contract/v1/manifest.schema.json +60 -14
- package/src/vendor/contract/v1/sections.json +330 -61
- package/src/vendor/contract/v1/tokens.json +1 -1
- package/src/vendor/contract/v1.lock.json +1 -1
- package/src/vendor/engine/content-footprint.mjs +79 -0
- package/src/vendor/validator/behaviors-runtime.js +2 -0
- package/src/vendor/validator/fixture-art.mjs +109 -0
- package/src/vendor/validator/model-reference.mjs +92 -0
- package/src/vendor/validator/platform-base.css +4250 -0
- package/src/vendor/validator/preview.mjs +497 -23
- package/src/vendor/validator/site-context.mjs +97 -0
- package/src/vendor/validator/validate.mjs +214 -14
- package/starter/assets/theme.css +54 -8
- package/starter/layout.liquid +15 -16
- package/starter/manifest.json +10 -5
- package/starter/sections/campaigns.liquid +23 -0
- package/starter/sections/homeHero.liquid +26 -11
- package/starter/sections/people.liquid +1 -1
- package/starter/sections/values.liquid +6 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@port60/template-kit)
|
|
4
4
|
[](https://github.com/Port60Labs/template-kit/actions/workflows/ci.yml)
|
|
5
5
|
|
|
6
|
-
The official toolkit for building [Port60](https://port60.com) site templates
|
|
6
|
+
The official toolkit for building [Port60](https://port60.com) site templates, scaffold from
|
|
7
7
|
the starter, preview locally against the platform contract, validate with the exact checks the
|
|
8
8
|
platform runs at upload, and package for review.
|
|
9
9
|
|
|
@@ -14,13 +14,17 @@ owns how a site looks.
|
|
|
14
14
|
## Quickstart
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx @port60/template-kit create my-template
|
|
17
|
+
npx @port60/template-kit create my-template # the folder name IS the template name
|
|
18
18
|
cd my-template && npm install
|
|
19
19
|
npm run dev # live preview at http://localhost:4400
|
|
20
20
|
npm run validate # the platform's conformance checks
|
|
21
21
|
npm run package # the uploadable <name>-<version>.zip
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Already building? `npx @port60/template-kit@latest upgrade` moves an existing template onto
|
|
25
|
+
the latest kit and contract, regenerates the agent briefing, and reports what (if anything) the
|
|
26
|
+
newer contract asks of you.
|
|
27
|
+
|
|
24
28
|
## Commands
|
|
25
29
|
|
|
26
30
|
| Command | What it does |
|
|
@@ -29,6 +33,9 @@ npm run package # the uploadable <name>-<version>.zip
|
|
|
29
33
|
| `dev [dir]` | Live preview over the contract's sample fixtures; validation re-runs on save. |
|
|
30
34
|
| `validate [dir] [--json]` | The exact checks the platform runs at upload. `--json` emits `{ok, errors, warnings, provenSupports}`. |
|
|
31
35
|
| `package [dir]` | Validate, then build the contract-shaped zip the studio accepts as-is. |
|
|
36
|
+
| `model [--json]` | The content model, in hand; `--json` for agents. |
|
|
37
|
+
| `content [dir]` | Eject the model's data as your editable copy; render it with `dev --content`. |
|
|
38
|
+
| `upgrade [dir]` | Latest kit + contract for an existing template; re-briefs and re-validates. |
|
|
32
39
|
|
|
33
40
|
## Building with an AI agent
|
|
34
41
|
|
|
@@ -41,7 +48,7 @@ See the [AI quickstart](https://developers.port60.com/guides/ai-quickstart/).
|
|
|
41
48
|
|
|
42
49
|
## The contract
|
|
43
50
|
|
|
44
|
-
The vendored contract in `src/vendor/contract` is the **Charity Platform contract v1
|
|
51
|
+
The vendored contract in `src/vendor/contract` is the **Charity Platform contract v1**, the
|
|
45
52
|
platform's first product surface. The dialect, validation rules and this toolchain are
|
|
46
53
|
platform-wide; future Port60 products ship their own contract packs for the same kit.
|
|
47
54
|
|
package/bin/cli.mjs
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @port60/template-kit
|
|
2
|
+
// @port60/template-kit, build Port60 site templates locally. AI-agent ready: `create` scaffolds
|
|
3
3
|
// an AGENTS.md-briefed project; `validate --json` is the machine feedback loop; `dev` is the
|
|
4
4
|
// human's live preview; `package` produces the uploadable artifact.
|
|
5
5
|
import { create } from '../src/commands/create.mjs';
|
|
6
6
|
import { validate } from '../src/commands/validate.mjs';
|
|
7
7
|
import { dev } from '../src/commands/dev.mjs';
|
|
8
8
|
import { packageCmd } from '../src/commands/packageCmd.mjs';
|
|
9
|
+
import { model } from '../src/commands/model.mjs';
|
|
10
|
+
import { content } from '../src/commands/content.mjs';
|
|
11
|
+
import { upgrade } from '../src/commands/upgrade.mjs';
|
|
12
|
+
import { refresh } from '../src/commands/refresh.mjs';
|
|
9
13
|
|
|
10
14
|
function parseArgs(argv) {
|
|
11
15
|
const args = { _: [] };
|
|
@@ -37,6 +41,18 @@ switch (command) {
|
|
|
37
41
|
case 'validate':
|
|
38
42
|
await validate(args);
|
|
39
43
|
break;
|
|
44
|
+
case 'model':
|
|
45
|
+
model(args);
|
|
46
|
+
break;
|
|
47
|
+
case 'content':
|
|
48
|
+
content(args);
|
|
49
|
+
break;
|
|
50
|
+
case 'upgrade':
|
|
51
|
+
upgrade(args);
|
|
52
|
+
break;
|
|
53
|
+
case 'refresh':
|
|
54
|
+
await refresh(args);
|
|
55
|
+
break;
|
|
40
56
|
case 'dev':
|
|
41
57
|
await dev(args);
|
|
42
58
|
break;
|
|
@@ -44,13 +60,19 @@ switch (command) {
|
|
|
44
60
|
await packageCmd(args);
|
|
45
61
|
break;
|
|
46
62
|
default:
|
|
47
|
-
console.log(`@port60/template-kit
|
|
63
|
+
console.log(`@port60/template-kit, build Port60 site templates locally
|
|
48
64
|
|
|
49
65
|
usage:
|
|
50
|
-
p60-template-kit create <dir>
|
|
51
|
-
|
|
66
|
+
p60-template-kit create <dir> scaffold a template (name = the folder;
|
|
67
|
+
--name/--label override the identity)
|
|
68
|
+
p60-template-kit dev [dir] [--port 4400] [--content my.json]
|
|
69
|
+
live preview; --content renders YOUR data
|
|
70
|
+
p60-template-kit model [--json] the content model, in hand (--json for agents)
|
|
71
|
+
p60-template-kit content [dir] [--out file] eject the model's data as your editable copy
|
|
52
72
|
p60-template-kit validate [dir] [--json] the platform's exact conformance checks
|
|
53
73
|
p60-template-kit package [dir] validate + build the uploadable zip
|
|
74
|
+
p60-template-kit upgrade [dir] move an existing template to the latest kit
|
|
75
|
+
and contract, re-brief, re-validate
|
|
54
76
|
|
|
55
77
|
AI agents: the scaffold's AGENTS.md is your briefing; iterate with \`validate --json\`.
|
|
56
78
|
Docs: https://developers.port60.com (agents: /llms-full.txt)`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@port60/template-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Build Port60 site templates locally: scaffold, live-preview, validate against the platform contract, and package for studio upload. AI-agent ready — every scaffold ships AGENTS.md and validate emits machine-readable JSON.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { buildSiteFixture, validatePreviewContent } from '../vendor/validator/site-context.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `content [dir] [--out file] [--force]`, EJECT the content model's data as your hard copy.
|
|
7
|
+
* Writes every overridable `site.content.*` collection, fully populated with the canonical
|
|
8
|
+
* organisation's data, as one editable JSON file. Replace the copy, swap the imagery for your
|
|
9
|
+
* own URLs, then hand it back:
|
|
10
|
+
*
|
|
11
|
+
* p60-template-kit content . → writes ./preview-content.json
|
|
12
|
+
* p60-template-kit dev . --content my.json → the preview renders YOUR data
|
|
13
|
+
*
|
|
14
|
+
* The shape stays the platform's (schema-checked on every read, a field that does not exist in
|
|
15
|
+
* production cannot exist in a preview); the data becomes yours. `about` is not ejected: it is
|
|
16
|
+
* your template's own section composition, derived from the manifest.
|
|
17
|
+
*/
|
|
18
|
+
export function content(args) {
|
|
19
|
+
const dir = resolve(args._[0] ?? '.');
|
|
20
|
+
const out = resolve(dir, typeof args.out === 'string' ? args.out : 'preview-content.json');
|
|
21
|
+
if (existsSync(out) && !args.force) {
|
|
22
|
+
console.error(`✗ ${out} already exists, pass --force to overwrite it.`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const site = buildSiteFixture(null);
|
|
26
|
+
const data = {
|
|
27
|
+
brand: site.brand,
|
|
28
|
+
...Object.fromEntries(Object.entries(site.content).filter(([name]) => name !== 'about'))
|
|
29
|
+
};
|
|
30
|
+
const problems = validatePreviewContent(data);
|
|
31
|
+
if (problems.length > 0) {
|
|
32
|
+
// The eject is generated FROM the model, so this firing means the kit itself is broken.
|
|
33
|
+
console.error('✗ internal error, the ejected content does not validate:');
|
|
34
|
+
for (const problem of problems) console.error(` - ${problem}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
writeFileSync(out, JSON.stringify(data, null, 2) + '\n');
|
|
38
|
+
console.log(`✓ content model data written to ${out}`);
|
|
39
|
+
console.log(' Edit the copy, swap imageUrl values for your own hosted images, then:');
|
|
40
|
+
console.log(` p60-template-kit dev ${args._[0] ?? '.'} --content ${typeof args.out === 'string' ? args.out : 'preview-content.json'}`);
|
|
41
|
+
console.log(' Shape is fixed (schema-checked every read); the data is yours. Never packaged.');
|
|
42
|
+
}
|
package/src/commands/create.mjs
CHANGED
|
@@ -3,8 +3,11 @@ import { resolve, join, basename } from 'node:path';
|
|
|
3
3
|
import { agentsMd } from '../lib/agentsMd.mjs';
|
|
4
4
|
|
|
5
5
|
const NAME_PATTERN = /^[a-z][a-z0-9-]{1,48}[a-z0-9]$/;
|
|
6
|
+
const KIT_PACKAGE = JSON.parse(
|
|
7
|
+
readFileSync(resolve(import.meta.dirname, '../../package.json'), 'utf8')
|
|
8
|
+
);
|
|
6
9
|
|
|
7
|
-
/** `create <dir> [--name x] [--label "X"]
|
|
10
|
+
/** `create <dir> [--name x] [--label "X"]`, a working, validating template from the starter,
|
|
8
11
|
* briefed for AI agents (AGENTS.md + CLAUDE.md) and wired with the kit's npm scripts. */
|
|
9
12
|
export function create(args) {
|
|
10
13
|
const dir = args._[0];
|
|
@@ -32,7 +35,7 @@ export function create(args) {
|
|
|
32
35
|
manifest.name = name;
|
|
33
36
|
manifest.version = '0.1.0';
|
|
34
37
|
manifest.label = label;
|
|
35
|
-
manifest.description = `${label}
|
|
38
|
+
manifest.description = `${label}, a Port60 site template.`;
|
|
36
39
|
manifest.changelog = 'First cut from the starter.';
|
|
37
40
|
writeFileSync(join(target, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
38
41
|
|
|
@@ -50,18 +53,18 @@ export function create(args) {
|
|
|
50
53
|
package: 'p60-template-kit package .'
|
|
51
54
|
},
|
|
52
55
|
devDependencies: {
|
|
53
|
-
'@port60/template-kit':
|
|
56
|
+
'@port60/template-kit': `^${KIT_PACKAGE.version}`
|
|
54
57
|
}
|
|
55
58
|
}, null, 2) + '\n');
|
|
56
59
|
writeFileSync(join(target, 'README.md'), `# ${label}
|
|
57
60
|
|
|
58
61
|
A Port60 site template. Start with \`npm install\`, then:
|
|
59
62
|
|
|
60
|
-
- \`npm run dev
|
|
61
|
-
- \`npm run validate
|
|
62
|
-
- \`npm run package
|
|
63
|
+
- \`npm run dev\`, live preview at http://localhost:4400
|
|
64
|
+
- \`npm run validate\`, conformance against the platform contract
|
|
65
|
+
- \`npm run package\`, the uploadable zip
|
|
63
66
|
|
|
64
|
-
**Working with an AI agent?** Point it at this directory
|
|
67
|
+
**Working with an AI agent?** Point it at this directory, \`AGENTS.md\` (and \`CLAUDE.md\`)
|
|
65
68
|
brief it on the contract, the rules and the validate loop.
|
|
66
69
|
|
|
67
70
|
Docs: https://developers.port60.com
|
package/src/commands/dev.mjs
CHANGED
|
@@ -1,24 +1,139 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import { watch } from 'node:fs';
|
|
2
|
+
import { watch, readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { renderStudioPreview } from '../vendor/validator/preview.mjs';
|
|
5
5
|
import { validateArtifact } from '../vendor/validator/validate.mjs';
|
|
6
|
+
import { applyPreviewContent, buildSiteFixture, validatePreviewContent } from '../vendor/validator/site-context.mjs';
|
|
7
|
+
import { renderModelReferenceHtml } from '../vendor/validator/model-reference.mjs';
|
|
6
8
|
import { loadArtifactDir } from '../lib/artifactFiles.mjs';
|
|
9
|
+
import { join } from 'node:path';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* The author's optional preview data (content model v1): preview-content.json beside the
|
|
13
|
+
* manifest replaces site.content collections wholesale for THIS preview, data is free, shape is
|
|
14
|
+
* fixed (schema-validated every read; problems land in the terminal and the canonical fixtures
|
|
15
|
+
* render instead). Hot like everything else: re-read on every request, never packaged, refused
|
|
16
|
+
* by the upload intake.
|
|
17
|
+
*/
|
|
18
|
+
function loadPreviewContent(dir, explicitPath) {
|
|
19
|
+
const path = explicitPath ?? join(dir, 'preview-content.json');
|
|
20
|
+
let raw;
|
|
21
|
+
try {
|
|
22
|
+
raw = readFileSync(path, 'utf8');
|
|
23
|
+
} catch {
|
|
24
|
+
if (explicitPath) console.error(`✗ --content file not found: ${explicitPath}, using the canonical fixtures`);
|
|
25
|
+
return null; // no file, the canonical fixtures render
|
|
26
|
+
}
|
|
27
|
+
let json;
|
|
28
|
+
try {
|
|
29
|
+
json = JSON.parse(raw);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error(`✗ preview-content.json is not valid JSON (${e.message}), using the canonical fixtures`);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const problems = validatePreviewContent(json);
|
|
35
|
+
if (problems.length > 0) {
|
|
36
|
+
console.error('✗ preview-content.json ignored, the shape is fixed even though the data is yours:');
|
|
37
|
+
for (const problem of problems) console.error(` - ${problem}`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return json;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The origins of every absolute URL in the override, the hosts the dev CSP must admit. */
|
|
44
|
+
function imageOriginsOf(json) {
|
|
45
|
+
const origins = new Set();
|
|
46
|
+
const walk = (value) => {
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
value.forEach(walk);
|
|
49
|
+
} else if (value && typeof value === 'object') {
|
|
50
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
51
|
+
// Imagery fields only (imageUrl, logoUrl, …), a directions link is a URL too, and the
|
|
52
|
+
// CSP should admit exactly the hosts that will be ASKED for pixels.
|
|
53
|
+
if (/Url$/.test(key) && typeof inner === 'string' && /^https?:\/\//.test(inner)) {
|
|
54
|
+
try {
|
|
55
|
+
origins.add(new URL(inner).origin);
|
|
56
|
+
} catch {
|
|
57
|
+
// not a URL after all, nothing to admit
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
walk(inner);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
walk(json);
|
|
66
|
+
return [...origins];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `dev <dir> [--port 4400]`, the local preview: your template over the contract's kind fixtures,
|
|
71
|
+
* the SAME render the studio and reviewers see (islands as fixture-hydrated skeletons,
|
|
72
|
+
* network-dead CSP). ONE addition over the studio render: the platform's own behaviour runtime is
|
|
73
|
+
* inlined, so data-p60-* carousels, reveals and tabs run for real locally, the only script the
|
|
74
|
+
* document can execute. Files are re-read on every request, so a browser refresh is the hot
|
|
75
|
+
* reload; file changes also re-run validation into the terminal, the human watches the page,
|
|
76
|
+
* the agent watches the JSON.
|
|
13
77
|
*/
|
|
14
78
|
export async function dev(args) {
|
|
15
79
|
const dir = resolve(args._[0] ?? '.');
|
|
16
80
|
const port = Number(args.port ?? 4400);
|
|
81
|
+
// The dev-richer half of the imagery split: point P60_FIXTURE_IMAGES at the platform's fixture
|
|
82
|
+
// imagery base and the preview loads photographic fixtures from that ONE origin instead of the
|
|
83
|
+
// sealed inline-SVG art. Unset (the default), the preview stays fully network-dead.
|
|
84
|
+
const fixtureImageBase = process.env.P60_FIXTURE_IMAGES || null;
|
|
85
|
+
// `--content my-org.json` points the preview at YOUR data (an ejected, edited copy of the
|
|
86
|
+
// content model, see the `content` command). Without it, preview-content.json beside the
|
|
87
|
+
// manifest is picked up by convention. Hot either way: re-read on every request.
|
|
88
|
+
// Relative --content paths resolve against the TEMPLATE dir (matching the eject hint);
|
|
89
|
+
// absolute paths pass through untouched.
|
|
90
|
+
const contentPath = typeof args.content === 'string' ? resolve(dir, args.content) : null;
|
|
91
|
+
let behaviorsRuntime = null;
|
|
92
|
+
try {
|
|
93
|
+
behaviorsRuntime = readFileSync(
|
|
94
|
+
resolve(import.meta.dirname, '../vendor/validator/behaviors-runtime.js'), 'utf8');
|
|
95
|
+
} catch {
|
|
96
|
+
// An older vendored copy without the runtime, the preview degrades to the CSS approximation.
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The preview is ROUTED: nav links land on real surfaces, so an author sees every platform
|
|
100
|
+
// page wearing their chrome, their own page template where they ship one, the platform's
|
|
101
|
+
// fixture skeleton where the page is platform-owned (ticket purchase, donate, campaigns).
|
|
102
|
+
const surfaceFor = (rawUrl) => {
|
|
103
|
+
const url = new URL(rawUrl, 'http://preview.local');
|
|
104
|
+
const path = url.pathname.replace(/\/+$/, '') || '/';
|
|
105
|
+
if (path === '/events') return url.searchParams.has('event') ? 'event' : 'events';
|
|
106
|
+
if (path === '/services') return url.searchParams.has('service') ? 'service' : 'services';
|
|
107
|
+
if (path === '/donate') return 'donate';
|
|
108
|
+
if (path === '/articles' || path === '/articles/all') return 'articles';
|
|
109
|
+
if (path.startsWith('/articles/')) return 'article';
|
|
110
|
+
if (path === '/campaigns') return 'campaigns';
|
|
111
|
+
if (path.startsWith('/campaigns/')) return 'campaign';
|
|
112
|
+
if (path === '/courses') return 'course';
|
|
113
|
+
return 'home';
|
|
114
|
+
};
|
|
17
115
|
|
|
18
116
|
const server = createServer(async (req, res) => {
|
|
19
117
|
try {
|
|
20
118
|
const files = loadArtifactDir(dir);
|
|
21
|
-
const
|
|
119
|
+
const url = new URL(req.url ?? '/', 'http://preview.local');
|
|
120
|
+
if (url.pathname === '/model') {
|
|
121
|
+
// The model, where the developer lives: the registry beside the LIVE data this preview
|
|
122
|
+
// renders (preview-content.json overlay included).
|
|
123
|
+
const manifest = JSON.parse(files['manifest.json'] ?? '{}');
|
|
124
|
+
const site = applyPreviewContent(buildSiteFixture(manifest), loadPreviewContent(dir, contentPath));
|
|
125
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
126
|
+
res.end(renderModelReferenceHtml(site));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const previewContent = loadPreviewContent(dir, contentPath);
|
|
130
|
+
const html = await renderStudioPreview(files, {
|
|
131
|
+
behaviorsRuntime,
|
|
132
|
+
fixtureImageBase,
|
|
133
|
+
previewContent,
|
|
134
|
+
contentImageOrigins: imageOriginsOf(previewContent),
|
|
135
|
+
surface: surfaceFor(req.url ?? '/')
|
|
136
|
+
});
|
|
22
137
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
23
138
|
res.end(html);
|
|
24
139
|
} catch (e) {
|
|
@@ -29,12 +144,23 @@ export async function dev(args) {
|
|
|
29
144
|
<h1>The template didn't render</h1>
|
|
30
145
|
<p><code>${String(e instanceof Error ? e.message : e).replace(/</g, '<')}</code></p>
|
|
31
146
|
${errors.length ? `<h2>Validation says</h2><ul>${errors.map((x) => `<li>${String(x).replace(/</g, '<')}</li>`).join('')}</ul>` : ''}
|
|
32
|
-
<p>Fix and refresh
|
|
147
|
+
<p>Fix and refresh, files are re-read on every request.</p>`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// A taken port is the most likely first-run failure (a forgotten dev server from another
|
|
152
|
+
// template), and a raw EADDRINUSE stack reads like the kit is broken. Name the fix instead.
|
|
153
|
+
server.on('error', (e) => {
|
|
154
|
+
if (e.code === 'EADDRINUSE') {
|
|
155
|
+
console.error(`✗ port ${port} is already in use, another preview is probably still running.`);
|
|
156
|
+
console.error(` Stop it, or start this one elsewhere: p60-template-kit dev ${args._[0] ?? '.'} --port ${port + 1}`);
|
|
157
|
+
process.exit(1);
|
|
33
158
|
}
|
|
159
|
+
throw e;
|
|
34
160
|
});
|
|
35
161
|
|
|
36
162
|
server.listen(port, () => {
|
|
37
|
-
console.log(`✓ preview at http://localhost:${port}
|
|
163
|
+
console.log(`✓ preview at http://localhost:${port}, refresh after edits`);
|
|
38
164
|
console.log(' watching for changes; validation runs on save:');
|
|
39
165
|
});
|
|
40
166
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { contentModel } from '../vendor/engine/content-footprint.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `model [--json]`, the content model, in hand. Prints every `site.content.*` collection with
|
|
5
|
+
* its bound, its onward link and its fields; `--json` emits the registry verbatim for agents
|
|
6
|
+
* (the same file the validator and the production renderer enforce, so it cannot drift).
|
|
7
|
+
* The dev server serves the same reference with live example data at /model.
|
|
8
|
+
*/
|
|
9
|
+
export function model(args) {
|
|
10
|
+
if (args.json) {
|
|
11
|
+
console.log(JSON.stringify(contentModel, null, 2));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.log(`site.content, content model ${contentModel.version}`);
|
|
15
|
+
console.log('site also carries: ' + contentModel.siblings.keys.map((k) => `site.${k}`).join(', '));
|
|
16
|
+
console.log('');
|
|
17
|
+
for (const [name, collection] of Object.entries(contentModel.collections)) {
|
|
18
|
+
const more = collection.moreHref ? ` · more at ${collection.moreHref}` : '';
|
|
19
|
+
console.log(`site.content.${name} (bounded at ${collection.cap}${more}, since ${collection.since})`);
|
|
20
|
+
for (const [field, spec] of Object.entries(collection.item)) {
|
|
21
|
+
const notes = [
|
|
22
|
+
spec.nullable ? 'nullable' : null,
|
|
23
|
+
spec.enumOpen ? `open enum${spec.enumOpen.length ? `: ${spec.enumOpen.join('|')}` : ''}` : null,
|
|
24
|
+
spec.max ? `max ${spec.max}` : null
|
|
25
|
+
].filter(Boolean).join(', ');
|
|
26
|
+
console.log(` ${field}: ${spec.type}${notes ? ` (${notes})` : ''}`);
|
|
27
|
+
}
|
|
28
|
+
console.log('');
|
|
29
|
+
}
|
|
30
|
+
console.log('Rules: collections are bounded (link onward via moreHref); enums are open (branch and');
|
|
31
|
+
console.log('fall back); nullable fields need a branch; the model only grows. Your footprint and');
|
|
32
|
+
console.log('minimum model version are computed at publish, you never declare them. Live examples:');
|
|
33
|
+
console.log('the /model page on your dev preview.');
|
|
34
|
+
}
|
|
@@ -5,16 +5,16 @@ import { loadArtifactDir } from '../lib/artifactFiles.mjs';
|
|
|
5
5
|
import { buildZip } from '../lib/zip.mjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* `package <dir
|
|
8
|
+
* `package <dir>`, validate first (the platform will run the identical checks, so failing here
|
|
9
9
|
* saves the round trip), then zip EXACTLY the contract-shaped file set into
|
|
10
|
-
* `<name>-<version>.zip
|
|
10
|
+
* `<name>-<version>.zip`, the artifact the studio's upload lane accepts as-is.
|
|
11
11
|
*/
|
|
12
12
|
export async function packageCmd(args) {
|
|
13
13
|
const dir = resolve(args._[0] ?? '.');
|
|
14
14
|
const files = loadArtifactDir(dir);
|
|
15
15
|
const { errors, manifest } = await validateArtifact(files);
|
|
16
16
|
if (errors.length > 0) {
|
|
17
|
-
console.error(`✗ not packaging
|
|
17
|
+
console.error(`✗ not packaging, ${errors.length} validation error${errors.length === 1 ? '' : 's'}:`);
|
|
18
18
|
for (const e of errors) console.error(` - ${e}`);
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { agentsMd } from '../lib/agentsMd.mjs';
|
|
4
|
+
import { loadArtifactDir } from '../lib/artifactFiles.mjs';
|
|
5
|
+
import { validateArtifact } from '../vendor/validator/validate.mjs';
|
|
6
|
+
import { contentModel } from '../vendor/engine/content-footprint.mjs';
|
|
7
|
+
import dialect from '../vendor/contract/v1/dialect.json' with { type: 'json' };
|
|
8
|
+
|
|
9
|
+
const KIT_PACKAGE = JSON.parse(
|
|
10
|
+
readFileSync(resolve(import.meta.dirname, '../../package.json'), 'utf8')
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `refresh [dir]`, regenerate the GENERATED files against the installed kit and report where the
|
|
15
|
+
* template stands. Rewrites AGENTS.md and CLAUDE.md (they are the kit's briefing, generated at
|
|
16
|
+
* create time and stale the moment the contract moves, your own notes belong in README.md),
|
|
17
|
+
* prints the versions in force, and runs the full conformance check so an upgrade immediately
|
|
18
|
+
* shows what, if anything, the newer contract asks of you. Template sources are never touched.
|
|
19
|
+
*/
|
|
20
|
+
export async function refresh(args) {
|
|
21
|
+
const dir = resolve(args._[0] ?? '.');
|
|
22
|
+
const files = loadArtifactDir(dir);
|
|
23
|
+
if (!files['manifest.json']) {
|
|
24
|
+
console.error(`✗ ${dir} does not contain a template (no manifest.json).`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const manifest = JSON.parse(files['manifest.json']);
|
|
28
|
+
|
|
29
|
+
const briefing = agentsMd(manifest.name);
|
|
30
|
+
writeFileSync(join(dir, 'AGENTS.md'), briefing);
|
|
31
|
+
writeFileSync(join(dir, 'CLAUDE.md'), briefing);
|
|
32
|
+
|
|
33
|
+
console.log(`✓ kit ${KIT_PACKAGE.version} · contract ${dialect.format} · content model ${contentModel.version}`);
|
|
34
|
+
console.log('✓ AGENTS.md and CLAUDE.md regenerated for this contract');
|
|
35
|
+
|
|
36
|
+
const { errors, warnings, contentFootprint, minContentVersion } = await validateArtifact(files);
|
|
37
|
+
if (contentFootprint?.length) {
|
|
38
|
+
console.log(` content footprint: ${contentFootprint.join(', ')}`
|
|
39
|
+
+ (minContentVersion ? ` (minimum model ${minContentVersion})` : ''));
|
|
40
|
+
}
|
|
41
|
+
if (errors.length === 0) {
|
|
42
|
+
console.log(`✓ ${manifest.name}@${manifest.version} conforms${warnings.length ? ` (${warnings.length} warning${warnings.length === 1 ? '' : 's'})` : ''}, nothing to change.`);
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`✗ the current contract asks ${errors.length} thing${errors.length === 1 ? '' : 's'} of this template:`);
|
|
45
|
+
for (const e of errors) console.log(` - ${e}`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `upgrade [dir]`, bring an EXISTING template onto the latest kit and contract. The scaffold
|
|
7
|
+
* pins the kit as a devDependency and every script runs the local copy, so the whole enforcement
|
|
8
|
+
* surface (contract, content model, validator, preview, behaviour runtime) upgrades with one
|
|
9
|
+
* install; this command does that install and then hands over to the FRESHLY INSTALLED kit's
|
|
10
|
+
* `refresh` to regenerate the agent briefing and report what the new contract thinks of your
|
|
11
|
+
* template. Your template sources are never touched, only the generated briefing files.
|
|
12
|
+
*
|
|
13
|
+
* Run it as `npx @port60/template-kit@latest upgrade` (or `p60-template-kit upgrade` from an
|
|
14
|
+
* installed copy, the delegation makes either safe).
|
|
15
|
+
*/
|
|
16
|
+
export function upgrade(args) {
|
|
17
|
+
const dir = resolve(args._[0] ?? '.');
|
|
18
|
+
if (!existsSync(join(dir, 'manifest.json'))) {
|
|
19
|
+
console.error(`✗ ${dir} does not contain a template (no manifest.json).`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
if (!existsSync(join(dir, 'package.json'))) {
|
|
23
|
+
console.error(`✗ ${dir} has no package.json, scaffolds created by this kit pin it as a devDependency.`);
|
|
24
|
+
console.error(' Add it yourself: npm init -y && npm install --save-dev @port60/template-kit@latest');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('▸ installing the latest kit…');
|
|
29
|
+
const install = spawnSync('npm', ['install', '--save-dev', '@port60/template-kit@latest'],
|
|
30
|
+
{ cwd: dir, stdio: 'inherit' });
|
|
31
|
+
if (install.status !== 0) {
|
|
32
|
+
console.error('✗ npm install failed, nothing else was changed.');
|
|
33
|
+
process.exit(install.status ?? 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The refresh must run on the NEW version (this process may be an older one).
|
|
37
|
+
const localCli = join(dir, 'node_modules', '@port60', 'template-kit', 'bin', 'cli.mjs');
|
|
38
|
+
const refresh = spawnSync(process.execPath, [localCli, 'refresh', dir], { stdio: 'inherit' });
|
|
39
|
+
if (refresh.status !== 0) {
|
|
40
|
+
// An older kit without `refresh` (or a refresh failure): the install still happened.
|
|
41
|
+
console.log('✓ kit upgraded. This version has no refresh step, run `npm run validate` to see');
|
|
42
|
+
console.log(' what the new contract thinks of your template.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -3,9 +3,8 @@ import { validateArtifact } from '../vendor/validator/validate.mjs';
|
|
|
3
3
|
import { loadArtifactDir } from '../lib/artifactFiles.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* `validate <dir> [--json]
|
|
7
|
-
* **supports as an OUTPUT**. On a clean pass the manifest's supports block IS the proven set
|
|
8
|
-
* the validator's two-way honesty checks (declared ⇒ renders the fixture, renders ⇒ declared)
|
|
6
|
+
* `validate <dir> [--json]`, the exact checks the platform runs at upload, plus the T3 promise:
|
|
7
|
+
* **supports as an OUTPUT**. On a clean pass the manifest's supports block IS the proven set, * the validator's two-way honesty checks (declared ⇒ renders the fixture, renders ⇒ declared)
|
|
9
8
|
* are what turn a declaration into proof. `--json` is the AI agent's feedback loop: iterate
|
|
10
9
|
* until {ok: true}.
|
|
11
10
|
*/
|