@nera-static/nera 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/package.json +1 -1
- package/src/agent-files.js +83 -0
- package/src/commands/update.js +52 -3
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @nera-static/nera
|
|
2
2
|
|
|
3
|
+
[](https://github.com/seebaermichi/nera-cli/actions/workflows/test.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@nera-static/nera)
|
|
5
|
+
|
|
3
6
|
The Nera CLI — scaffold, build, preview and update a [Nera](https://github.com/seebaermichi/nera)
|
|
4
7
|
static site with a single command. It runs over the [`@nera-static/core`](https://github.com/seebaermichi/nera)
|
|
5
8
|
engine and is the one package a Nera site depends on.
|
|
@@ -28,7 +31,7 @@ package is named after the folder (`My Bakery` → `my-bakery`).
|
|
|
28
31
|
| `nera build` | Render `pages/` → `public/`. With `--check`, then run `nera check` on the result — one command for CI. |
|
|
29
32
|
| `nera dev` | Build, serve `public/`, and rebuild on change with live reload. Watches `pages/`, `config/`, `theme/` and a local theme (`theme: ./themes/<name>`). |
|
|
30
33
|
| `nera serve` | Serve the already-built `public/` folder. |
|
|
31
|
-
| `nera update` | Update the site's Nera packages. On a legacy cloned site, `nera update --migrate` converts it to the thin model. |
|
|
34
|
+
| `nera update` | Update the site's Nera packages, then create a missing `AGENTS.md`/`CLAUDE.md` and refresh the Nera part of `AGENTS.md` (see [Instructions for AI assistants](#instructions-for-ai-assistants)). On a legacy cloned site, `nera update --migrate` converts it to the thin model. |
|
|
32
35
|
| `nera validate` | Check the site's sources (layouts, includes, YAML) before publishing; exits non-zero on any error. |
|
|
33
36
|
| `nera check` | Check the built `public/` for accessibility, privacy and legal-notice problems (see below). Build first. |
|
|
34
37
|
|
|
@@ -56,6 +59,22 @@ the marker line `<!-- nera:site-notes … -->`; keep that line in place. The
|
|
|
56
59
|
part above it is Nera's and may be replaced by a later Nera version; your
|
|
57
60
|
notes below it are yours.
|
|
58
61
|
|
|
62
|
+
`nera update` keeps both files current, with no extra flag. After the
|
|
63
|
+
packages are updated (and after `nera update --migrate`) it:
|
|
64
|
+
|
|
65
|
+
- creates `AGENTS.md` and `CLAUDE.md` when they are missing, so a site made
|
|
66
|
+
before 1.6.0 gets them too;
|
|
67
|
+
- replaces only the part of `AGENTS.md` above the first line starting with
|
|
68
|
+
`<!-- nera:site-notes`, taking the new text from the installed
|
|
69
|
+
`@nera-static/nera`'s template; the marker line and everything below it
|
|
70
|
+
stay byte for byte as they are;
|
|
71
|
+
- leaves an `AGENTS.md` without that line alone and prints a hint: copy the
|
|
72
|
+
marker line from the template directly above your own notes to opt in;
|
|
73
|
+
- never touches an existing `CLAUDE.md`.
|
|
74
|
+
|
|
75
|
+
A file is written only when its content changes, so running `nera update`
|
|
76
|
+
again changes nothing.
|
|
77
|
+
|
|
59
78
|
## Starting from a theme
|
|
60
79
|
|
|
61
80
|
```bash
|
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
// Start of the ownership line in AGENTS.md. Everything above the first line
|
|
5
|
+
// that starts with it is Nera's and is refreshed by `nera update`; that line
|
|
6
|
+
// and everything below it belong to the site owner and are kept byte for byte.
|
|
7
|
+
export const SITE_NOTES_MARKER = '<!-- nera:site-notes'
|
|
8
|
+
|
|
9
|
+
// Offset of the first line that starts with SITE_NOTES_MARKER, or -1.
|
|
10
|
+
// Only a match at a line start counts, so the marker quoted inside a line
|
|
11
|
+
// never moves the split.
|
|
12
|
+
function markerOffset(text) {
|
|
13
|
+
if (text.startsWith(SITE_NOTES_MARKER)) return 0
|
|
14
|
+
const at = text.indexOf(`\n${SITE_NOTES_MARKER}`)
|
|
15
|
+
return at === -1 ? -1 : at + 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readIfExists(file) {
|
|
19
|
+
try {
|
|
20
|
+
return await fs.readFile(file, 'utf-8')
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.code === 'ENOENT') return null
|
|
23
|
+
throw err
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The site's AGENTS.md with its Nera part replaced by the template's. The
|
|
28
|
+
// template's line endings follow the site's, so a CRLF checkout stays CRLF.
|
|
29
|
+
// null when the site file has no marker: then none of it is Nera's to touch.
|
|
30
|
+
function spliceAgents(siteText, templateHead) {
|
|
31
|
+
const at = markerOffset(siteText)
|
|
32
|
+
if (at === -1) return null
|
|
33
|
+
const eol = siteText.slice(0, at).includes('\r\n') ? '\r\n' : '\n'
|
|
34
|
+
return templateHead.replace(/\r?\n/g, eol) + siteText.slice(at)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Bring a site's AGENTS.md and CLAUDE.md in line with the scaffold template
|
|
38
|
+
// in templateDir: create what is missing, refresh the Nera part of an
|
|
39
|
+
// AGENTS.md that has the marker, leave a marker-less AGENTS.md and any
|
|
40
|
+
// existing CLAUDE.md alone. Writes only on change, so a repeated run is a
|
|
41
|
+
// no-op. Returns one { file, action } per file, action being `created`,
|
|
42
|
+
// `updated`, `unchanged` or `skipped`; printing is the caller's job.
|
|
43
|
+
export async function syncAgentFiles({ cwd = process.cwd(), templateDir }) {
|
|
44
|
+
const template = await fs.readFile(path.join(templateDir, 'AGENTS.md'), 'utf-8')
|
|
45
|
+
const templateAt = markerOffset(template)
|
|
46
|
+
if (templateAt === -1) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`The template's AGENTS.md (${templateDir}) has no ` +
|
|
49
|
+
`\`${SITE_NOTES_MARKER}\` line. This is a packaging bug in ` +
|
|
50
|
+
'@nera-static/nera, not a problem with your site.'
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const agentsPath = path.join(cwd, 'AGENTS.md')
|
|
55
|
+
const agents = await readIfExists(agentsPath)
|
|
56
|
+
let agentsAction
|
|
57
|
+
if (agents === null) {
|
|
58
|
+
await fs.writeFile(agentsPath, template)
|
|
59
|
+
agentsAction = 'created'
|
|
60
|
+
} else {
|
|
61
|
+
const next = spliceAgents(agents, template.slice(0, templateAt))
|
|
62
|
+
if (next === null) {
|
|
63
|
+
agentsAction = 'skipped'
|
|
64
|
+
} else if (next === agents) {
|
|
65
|
+
agentsAction = 'unchanged'
|
|
66
|
+
} else {
|
|
67
|
+
await fs.writeFile(agentsPath, next)
|
|
68
|
+
agentsAction = 'updated'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const claudePath = path.join(cwd, 'CLAUDE.md')
|
|
73
|
+
let claudeAction = 'unchanged'
|
|
74
|
+
if ((await readIfExists(claudePath)) === null) {
|
|
75
|
+
await fs.copyFile(path.join(templateDir, 'CLAUDE.md'), claudePath)
|
|
76
|
+
claudeAction = 'created'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
{ file: 'AGENTS.md', action: agentsAction },
|
|
81
|
+
{ file: 'CLAUDE.md', action: claudeAction },
|
|
82
|
+
]
|
|
83
|
+
}
|
package/src/commands/update.js
CHANGED
|
@@ -2,6 +2,8 @@ import { execFileSync } from 'child_process'
|
|
|
2
2
|
import fs from 'fs/promises'
|
|
3
3
|
import fssync from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
|
+
import { syncAgentFiles, SITE_NOTES_MARKER } from '../agent-files.js'
|
|
6
|
+
import { templateDir as packageTemplateDir } from '../scaffold.js'
|
|
5
7
|
|
|
6
8
|
const defaultRunNpm = (npmArgs, cwd) =>
|
|
7
9
|
execFileSync('npm', npmArgs, { cwd, stdio: 'inherit' })
|
|
@@ -45,7 +47,46 @@ function printLegacyGuidance() {
|
|
|
45
47
|
console.log('\n👉 Run `nera update --migrate` to do it.')
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
// The scaffold template whose AGENTS.md / CLAUDE.md the site is synced to:
|
|
51
|
+
// the one in the site's installed @nera-static/nera (read after npm has
|
|
52
|
+
// updated it), so the instructions match the Nera version the site runs even
|
|
53
|
+
// when a global or older `nera` performs the update. Falls back to the
|
|
54
|
+
// running package's template when nothing is installed there or the installed
|
|
55
|
+
// version predates 1.6.0, which shipped no AGENTS.md.
|
|
56
|
+
export function agentTemplateDir(cwd, fallbackDir) {
|
|
57
|
+
const installed = path.join(cwd, 'node_modules', '@nera-static', 'nera', 'template')
|
|
58
|
+
return fssync.existsSync(path.join(installed, 'AGENTS.md')) ? installed : fallbackDir
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const AGENT_FILE_MESSAGES = {
|
|
62
|
+
created: (file) => ` ✓ Created ${file}`,
|
|
63
|
+
updated: (file) => ` ✓ Refreshed the Nera part of ${file}`,
|
|
64
|
+
// Neutral on purpose: an existing CLAUDE.md is kept without comparing it.
|
|
65
|
+
unchanged: (file) => ` ✓ ${file} unchanged`,
|
|
66
|
+
skipped: (file) => ` ⚠️ Left ${file} alone: it has no \`${SITE_NOTES_MARKER}\` line`,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function syncAndReportAgentFiles(cwd, fallbackDir) {
|
|
70
|
+
const dir = agentTemplateDir(cwd, fallbackDir)
|
|
71
|
+
const results = await syncAgentFiles({ cwd, templateDir: dir })
|
|
72
|
+
for (const { file, action } of results) {
|
|
73
|
+
console.log(AGENT_FILE_MESSAGES[action](file))
|
|
74
|
+
}
|
|
75
|
+
if (results.some(({ action }) => action === 'skipped')) {
|
|
76
|
+
// Relative inside the site, absolute for a global or npx-cached CLI,
|
|
77
|
+
// where a relative path would be a long run of `../`.
|
|
78
|
+
const template = path.join(dir, 'AGENTS.md')
|
|
79
|
+
const relative = path.relative(cwd, template)
|
|
80
|
+
const source = relative.startsWith('..') ? template : relative
|
|
81
|
+
console.log(
|
|
82
|
+
` To let \`nera update\` keep Nera's instructions current, copy the \`${SITE_NOTES_MARKER}\`` +
|
|
83
|
+
` line from ${source} directly above your own notes. Everything above that` +
|
|
84
|
+
' line is then Nera\'s and is replaced on update; the line and everything below it stay yours.'
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function migrateLegacy(cwd, pkg, runNpm, templateDir) {
|
|
49
90
|
console.log('🔧 Migrating this cloned site to the thin @nera-static/nera model...')
|
|
50
91
|
|
|
51
92
|
pkg.dependencies = {
|
|
@@ -89,6 +130,7 @@ async function migrateLegacy(cwd, pkg, runNpm) {
|
|
|
89
130
|
|
|
90
131
|
console.log('📦 Installing...')
|
|
91
132
|
runNpm(['install'], cwd)
|
|
133
|
+
await syncAndReportAgentFiles(cwd, templateDir)
|
|
92
134
|
|
|
93
135
|
console.log('✅ Migrated. Run `nera dev` to start.')
|
|
94
136
|
if (movedPlugins) {
|
|
@@ -100,7 +142,13 @@ async function migrateLegacy(cwd, pkg, runNpm) {
|
|
|
100
142
|
}
|
|
101
143
|
|
|
102
144
|
export async function runUpdate(args = [], options = {}) {
|
|
103
|
-
|
|
145
|
+
// templateDir: the fallback scaffold template for AGENTS.md / CLAUDE.md
|
|
146
|
+
// (see agentTemplateDir); injectable so tests need no installed package.
|
|
147
|
+
const {
|
|
148
|
+
cwd = process.cwd(),
|
|
149
|
+
runNpm = defaultRunNpm,
|
|
150
|
+
templateDir = packageTemplateDir(),
|
|
151
|
+
} = options
|
|
104
152
|
|
|
105
153
|
const pkg = JSON.parse(
|
|
106
154
|
await fs.readFile(path.join(cwd, 'package.json'), 'utf-8')
|
|
@@ -109,7 +157,7 @@ export async function runUpdate(args = [], options = {}) {
|
|
|
109
157
|
|
|
110
158
|
if (kind === 'legacy') {
|
|
111
159
|
if (args.includes('--migrate') || args.includes('--yes')) {
|
|
112
|
-
await migrateLegacy(cwd, pkg, runNpm)
|
|
160
|
+
await migrateLegacy(cwd, pkg, runNpm, templateDir)
|
|
113
161
|
} else {
|
|
114
162
|
printLegacyGuidance()
|
|
115
163
|
}
|
|
@@ -126,5 +174,6 @@ export async function runUpdate(args = [], options = {}) {
|
|
|
126
174
|
const targets = neraDeps(pkg)
|
|
127
175
|
console.log(`🔄 Updating Nera packages: ${targets.join(', ')}`)
|
|
128
176
|
runNpm(['update', ...targets], cwd)
|
|
177
|
+
await syncAndReportAgentFiles(cwd, templateDir)
|
|
129
178
|
console.log('✅ Up to date.')
|
|
130
179
|
}
|