@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.
Files changed (40) hide show
  1. package/README.md +10 -3
  2. package/bin/cli.mjs +26 -4
  3. package/package.json +1 -1
  4. package/src/commands/content.mjs +42 -0
  5. package/src/commands/create.mjs +10 -7
  6. package/src/commands/dev.mjs +134 -8
  7. package/src/commands/model.mjs +34 -0
  8. package/src/commands/packageCmd.mjs +3 -3
  9. package/src/commands/refresh.mjs +48 -0
  10. package/src/commands/upgrade.mjs +44 -0
  11. package/src/commands/validate.mjs +2 -3
  12. package/src/lib/agentsMd.mjs +78 -21
  13. package/src/vendor/contract/v1/behaviours.json +129 -0
  14. package/src/vendor/contract/v1/content-bounds.json +3 -3
  15. package/src/vendor/contract/v1/content-model.json +193 -0
  16. package/src/vendor/contract/v1/context.json +1985 -141
  17. package/src/vendor/contract/v1/dialect.json +3 -3
  18. package/src/vendor/contract/v1/fonts.json +1 -1
  19. package/src/vendor/contract/v1/imagery.json +4 -4
  20. package/src/vendor/contract/v1/islands.json +256 -110
  21. package/src/vendor/contract/v1/layout.json +25 -0
  22. package/src/vendor/contract/v1/manifest.schema.json +60 -14
  23. package/src/vendor/contract/v1/sections.json +330 -61
  24. package/src/vendor/contract/v1/tokens.json +1 -1
  25. package/src/vendor/contract/v1.lock.json +1 -1
  26. package/src/vendor/engine/content-footprint.mjs +79 -0
  27. package/src/vendor/validator/behaviors-runtime.js +2 -0
  28. package/src/vendor/validator/fixture-art.mjs +109 -0
  29. package/src/vendor/validator/model-reference.mjs +92 -0
  30. package/src/vendor/validator/platform-base.css +4250 -0
  31. package/src/vendor/validator/preview.mjs +497 -23
  32. package/src/vendor/validator/site-context.mjs +97 -0
  33. package/src/vendor/validator/validate.mjs +214 -14
  34. package/starter/assets/theme.css +54 -8
  35. package/starter/layout.liquid +15 -16
  36. package/starter/manifest.json +10 -5
  37. package/starter/sections/campaigns.liquid +23 -0
  38. package/starter/sections/homeHero.liquid +26 -11
  39. package/starter/sections/people.liquid +1 -1
  40. package/starter/sections/values.liquid +6 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/%40port60%2Ftemplate-kit)](https://www.npmjs.com/package/@port60/template-kit)
4
4
  [![CI](https://github.com/Port60Labs/template-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/Port60Labs/template-kit/actions/workflows/ci.yml)
5
5
 
6
- The official toolkit for building [Port60](https://port60.com) site templates — scaffold from
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 --name 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** — the
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 — build Port60 site templates locally. AI-agent ready: `create` scaffolds
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 — build Port60 site templates locally
63
+ console.log(`@port60/template-kit, build Port60 site templates locally
48
64
 
49
65
  usage:
50
- p60-template-kit create <dir> [--name my-template] [--label "My Template"]
51
- p60-template-kit dev [dir] [--port 4400] live preview over the contract fixtures
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.8.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
+ }
@@ -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"]` — a working, validating template from the starter,
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} — a Port60 site template.`;
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': '^0.1.0'
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\` — live preview at http://localhost:4400
61
- - \`npm run validate\` — conformance against the platform contract
62
- - \`npm run package\` — the uploadable zip
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 — \`AGENTS.md\` (and \`CLAUDE.md\`)
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
@@ -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
- * `dev <dir> [--port 4400]` — the local preview: your template over the contract's kind fixtures,
10
- * the SAME render the studio and reviewers see (islands as placeholders, network-dead CSP).
11
- * Files are re-read on every request, so a browser refresh is the hot reload; file changes also
12
- * re-run validation into the terminal — the human watches the page, the agent watches the JSON.
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 html = await renderStudioPreview(files);
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, '&lt;')}</code></p>
31
146
  ${errors.length ? `<h2>Validation says</h2><ul>${errors.map((x) => `<li>${String(x).replace(/</g, '&lt;')}</li>`).join('')}</ul>` : ''}
32
- <p>Fix and refresh — files are re-read on every request.</p>`);
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} — refresh after edits`);
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>` — validate first (the platform will run the identical checks, so failing here
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` — the artifact the studio's upload lane accepts as-is.
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 — ${errors.length} validation error${errors.length === 1 ? '' : 's'}:`);
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]` — 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 —
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
  */