ferst-core 0.6.1 → 0.6.2

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.
@@ -46,6 +46,8 @@ export function isDirectInvocation(argv1) {
46
46
  export const DEFAULT_UPSTREAM = 'https://github.com/MyAI4WebDev/ferst-client-template.git';
47
47
  /** The ownership manifest a seeded client repo carries at its root. */
48
48
  export const MANIFEST = 'ferst-template.json';
49
+ /** The per-site provenance/config file a client repo carries at its root (gitBase, etc.). */
50
+ export const SITE_META = 'ferst-site.json';
49
51
 
50
52
  // ── pure helpers (unit-tested; no shell, no fs writes) ───────────────────────────
51
53
 
@@ -155,6 +157,33 @@ export function tail(text, n = 40) {
155
157
  return lines.slice(-n).join('\n');
156
158
  }
157
159
 
160
+ /**
161
+ * Build the provenance "drift stamp" for a completed update: WHICH template commit and
162
+ * core version this repo landed on, and when. Pure — the write is the caller's job.
163
+ * @param {{ upstream?: string, commit?: string|null, coreVersion?: string|null, now?: Date|string }} p
164
+ * @returns {{ templateUpstream: string|null, templateCommit: string|null, coreVersion: string|null, at: string }}
165
+ */
166
+ export function buildProvenance({ upstream, commit, coreVersion, now = new Date() } = {}) {
167
+ return {
168
+ templateUpstream: upstream ?? null,
169
+ templateCommit: commit ?? null,
170
+ coreVersion: coreVersion ?? null,
171
+ at: typeof now === 'string' ? now : now.toISOString(),
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Merge a provenance stamp into an existing ferst-site.json object under `lastUpdate`,
177
+ * preserving every other field (gitBase, productionBranch, unknown keys) and overwriting
178
+ * any prior `lastUpdate`. Pass null (no ferst-site.json yet) → a fresh object with just
179
+ * the stamp. Pure — never mutates the input.
180
+ * @returns {object} the object to write back
181
+ */
182
+ export function mergeSiteMeta(existing, provenance) {
183
+ const base = existing && typeof existing === 'object' && !Array.isArray(existing) ? existing : {};
184
+ return { ...base, lastUpdate: provenance };
185
+ }
186
+
158
187
  // ── thin side-effecting shell (kept out of the pure helpers above) ────────────────
159
188
 
160
189
  const exec = (cmd, cwd) => execSync(cmd, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
@@ -181,6 +210,16 @@ function readInstalledCoreVersion(cwd) {
181
210
  }
182
211
  }
183
212
 
213
+ /** The repo's ferst-site.json parsed to an object, or null when absent/unreadable/non-object. */
214
+ function readSiteMeta(cwd) {
215
+ try {
216
+ const parsed = JSON.parse(readFileSync(resolve(cwd, SITE_META), 'utf8'));
217
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
184
223
  /** Ensure an `upstream` remote points at `url` (add, or set-url if it already exists). */
185
224
  function ensureUpstream(url, cwd) {
186
225
  const remotes = (tryGit('remote', cwd) || '').split(/\r?\n/).map((s) => s.trim());
@@ -240,6 +279,8 @@ function main() {
240
279
  }
241
280
  const branch = parseDefaultBranch(tryGit('ls-remote --symref upstream HEAD', cwd)) || 'main';
242
281
  console.log(`ferst-update: upstream default branch = ${branch}`);
282
+ // The exact template commit this run syncs from (for the provenance stamp / #18 ledger).
283
+ const templateCommit = (tryGit(`rev-parse upstream/${branch}`, cwd) || '').trim() || null;
243
284
 
244
285
  let scaffoldCount = 0; // files the scaffold sync touched (counted BEFORE the npm churn)
245
286
  if (dryRun) {
@@ -263,12 +304,13 @@ function main() {
263
304
 
264
305
  // 3. Engine (ferst-core) update.
265
306
  let coreBump = null; // { before, after } once bumped
307
+ let dryLatest = null; // dry-run only: the version `@latest` WOULD install (for the stamp preview)
266
308
  if (noCore) {
267
309
  console.log('ferst-update: --no-core — skipping the ferst-core npm bump.');
268
310
  } else if (dryRun) {
269
- const latest = (tryExec('npm view ferst-core version', cwd) || '').trim();
311
+ dryLatest = (tryExec('npm view ferst-core version', cwd) || '').trim() || null;
270
312
  const current = readInstalledCoreVersion(cwd);
271
- console.log(`ferst-update: ferst-core installed=${current ?? '(none)'} latest=${latest || '(unknown)'}`);
313
+ console.log(`ferst-update: ferst-core installed=${current ?? '(none)'} latest=${dryLatest || '(unknown)'}`);
272
314
  } else {
273
315
  const before = readInstalledCoreVersion(cwd);
274
316
  console.log('ferst-update: npm install ferst-core@latest --save ...');
@@ -302,6 +344,9 @@ function main() {
302
344
  // 4. Verify — a failed @latest bump is a real incompatibility to surface, so on
303
345
  // failure we print the tail and exit non-zero WITHOUT reverting.
304
346
  if (dryRun) {
347
+ const wouldCommit = templateCommit ? templateCommit.slice(0, 7) : '(unknown)';
348
+ const wouldCore = (noCore ? readInstalledCoreVersion(cwd) : dryLatest) ?? '(unknown)';
349
+ console.log(`ferst-update: --dry-run — WOULD stamp ${SITE_META}: template ${wouldCommit}, core ${wouldCore} (no write).`);
305
350
  console.log('ferst-update: --dry-run complete (no writes, no verify).');
306
351
  return;
307
352
  }
@@ -315,12 +360,25 @@ function main() {
315
360
  }
316
361
  }
317
362
 
318
- // 5. Summary.
363
+ // 5. Provenance drift stamp — record WHICH template commit + core version this repo
364
+ // landed on (only after a fully-successful update), so the repo and the future fleet
365
+ // drift ledger (#18) always know its state. MERGE: preserve gitBase/productionBranch
366
+ // and any other existing ferst-site.json fields.
367
+ const coreVersion = coreBump?.after ?? readInstalledCoreVersion(cwd);
368
+ const provenance = buildProvenance({ upstream: upstream.url, commit: templateCommit, coreVersion });
369
+ writeFileSync(
370
+ resolve(cwd, SITE_META),
371
+ `${JSON.stringify(mergeSiteMeta(readSiteMeta(cwd), provenance), null, 2)}\n`,
372
+ );
373
+ const stampSha = templateCommit ? templateCommit.slice(0, 7) : '(unknown)';
374
+
375
+ // 6. Summary.
319
376
  const coreLine = coreBump
320
377
  ? `ferst-core ${coreBump.before ?? '(none)'} → ${coreBump.after}`
321
378
  : 'ferst-core unchanged (--no-core)';
322
379
  console.log(
323
- `ferst-update: DONE — scaffold files updated: ${scaffoldCount}; ${coreLine}; build + check:thin green.`,
380
+ `ferst-update: DONE — scaffold files updated: ${scaffoldCount}; ${coreLine}; ` +
381
+ `stamped ${SITE_META}: template ${stampSha}, core ${coreVersion ?? '(unknown)'}; build + check:thin green.`,
324
382
  );
325
383
  }
326
384
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ferst-core",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "description": "Ferst Core — the shared, brand-agnostic client-site system: Astro components, layouts, content schemas, the layered config resolver, and a neutral styling architecture that every client overrides.",
6
6
  "license": "BUSL-1.1",