ferst-core 0.9.0 → 0.9.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.
@@ -108,18 +108,47 @@ export function deploymentBranch() {
108
108
  return 'main';
109
109
  }
110
110
 
111
+ /** Ferst's managed GitHub org: its sites are on the Basic plan. */
112
+ const MANAGED_OWNER = 'myai4webdev';
113
+
114
+ /** The Pages collection's description on each plan. */
115
+ const PAGES_DESCRIPTION = {
116
+ basic: "See each page and request a change — our team makes it and sends it back for you to check. You don't edit the page layout here.",
117
+ free: "You're on the free plan: you look after this site yourself and change its pages in your own repository. Asking the Ferst team to make changes for you comes with the Basic plan.",
118
+ };
119
+
120
+ /**
121
+ * The site's plan, for what its CMS offers. Requests (the Ferst team making a change for the
122
+ * site's people) come with the Basic plan: a managed site. A self-managed site, the client's
123
+ * own repo, is on the free plan and makes its own changes. The plan the platform records in
124
+ * `ferst-site.json` wins; otherwise the repo's owner decides. Unknown keeps the requests field:
125
+ * the platform refuses a free-plan site's request anyway.
126
+ * @param {string} repo owner/name
127
+ * @param {{ plan?: string }} [site] the parsed ferst-site.json
128
+ * @returns {'basic' | 'free'}
129
+ */
130
+ export function planFor(repo, site = {}) {
131
+ if (site && (site.plan === 'basic' || site.plan === 'free')) return site.plan;
132
+ const owner = String(repo ?? '').split('/')[0].trim().toLowerCase();
133
+ if (!owner) return 'basic';
134
+ return owner === MANAGED_OWNER ? 'basic' : 'free';
135
+ }
136
+
111
137
  /**
112
- * Fill the config template's placeholders. Pure — unit-testable.
138
+ * Fill the config template's placeholders. Pure — unit-testable. On the free plan the
139
+ * `# ferst:requests` … `# /ferst:requests` block (the pages' "Requested changes") is left out.
113
140
  * @param {string} tpl the template contents
114
- * @param {{ repo: string, base: string, branch: string }} vars
141
+ * @param {{ repo: string, base: string, branch: string, plan?: 'basic' | 'free' }} vars
115
142
  * @returns {string}
116
143
  */
117
- export function fillConfig(tpl, { repo, base, branch }) {
118
- const out = tpl
144
+ export function fillConfig(tpl, { repo, base, branch, plan = 'basic' }) {
145
+ const forPlan = plan === 'free' ? tpl.replace(/^[ \t]*# ferst:requests\b[\s\S]*?^[ \t]*# \/ferst:requests[^\n]*\n/m, '') : tpl;
146
+ const out = forPlan
119
147
  .split('__REPO__').join(repo)
120
148
  .split('__API_ROOT__').join(`${base}/git`)
121
149
  .split('__BASE_URL__').join(base)
122
- .split('__BRANCH__').join(branch);
150
+ .split('__BRANCH__').join(branch)
151
+ .split('__PAGES_DESCRIPTION__').join(PAGES_DESCRIPTION[plan] ?? PAGES_DESCRIPTION.basic);
123
152
  const leftover = out.match(/__[A-Z][A-Z_]*__/);
124
153
  if (leftover) {
125
154
  throw new Error(`ferst-cms-config: unfilled placeholder ${leftover[0]} — add it to the filler.`);
@@ -147,9 +176,17 @@ function main() {
147
176
  process.exit(1);
148
177
  }
149
178
 
179
+ let site = {};
180
+ try {
181
+ site = JSON.parse(readFileSync(resolve(process.cwd(), 'ferst-site.json'), 'utf8')) ?? {};
182
+ } catch {
183
+ /* no ferst-site.json: the repo's owner decides the plan */
184
+ }
185
+ const plan = planFor(repo, site);
186
+
150
187
  let out;
151
188
  try {
152
- out = fillConfig(tpl, { repo, base, branch });
189
+ out = fillConfig(tpl, { repo, base, branch, plan });
153
190
  } catch (e) {
154
191
  console.error(String(e.message ?? e));
155
192
  process.exit(1);
@@ -157,7 +194,7 @@ function main() {
157
194
 
158
195
  mkdirSync(dirname(outPath), { recursive: true });
159
196
  writeFileSync(outPath, out);
160
- console.log(`ferst-cms-config: wrote ${outPath} (repo=${repo}, branch="${branch}", base=${base})`);
197
+ console.log(`ferst-cms-config: wrote ${outPath} (repo=${repo}, branch="${branch}", base=${base}, plan=${plan})`);
161
198
  }
162
199
 
163
200
  // Run when invoked directly as a bin — including via npm's node_modules/.bin symlink.
@@ -142,6 +142,26 @@ export function coreRangeFor(version) {
142
142
  return `^${m[1]}.${m[2]}.0`;
143
143
  }
144
144
 
145
+ /** Whether `b` is a newer plain x.y.z version than `a`; anything unparsable is not newer. */
146
+ export function isNewerVersion(a, b) {
147
+ const parse = (v) => String(v ?? '').trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)$/)?.slice(1).map(Number);
148
+ const [x, y] = [parse(a), parse(b)];
149
+ if (!x || !y) return false;
150
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return y[i] > x[i];
151
+ return false;
152
+ }
153
+
154
+ /**
155
+ * The dry run's line about the engine. A site's scheduled run decides from the dry run
156
+ * whether to update, so a newer ferst-core has to read as a change that WOULD apply, like
157
+ * the scaffold changes above it; otherwise an engine-only release never reaches the site.
158
+ */
159
+ export function coreUpdateNote(installed, latest) {
160
+ if (!latest) return `ferst-core is ${installed ?? '(not installed)'}; the latest version is unknown`;
161
+ if (!installed) return `ferst-core ${latest} WOULD be installed (none installed yet)`;
162
+ return isNewerVersion(installed, latest) ? `ferst-core ${installed} → ${latest} WOULD be installed` : `ferst-core is current (${installed})`;
163
+ }
164
+
145
165
  /** Read the `version` out of a package.json's text. Throws if absent. */
146
166
  export function extractVersion(pkgJsonText) {
147
167
  let v;
@@ -451,8 +471,7 @@ function main() {
451
471
  console.log('ferst-update: --no-core — skipping the ferst-core npm bump.');
452
472
  } else if (dryRun) {
453
473
  dryLatest = (tryExec('npm view ferst-core version', cwd) || '').trim() || null;
454
- const current = readInstalledCoreVersion(cwd);
455
- console.log(`ferst-update: ferst-core installed=${current ?? '(none)'} latest=${dryLatest || '(unknown)'}`);
474
+ console.log(`ferst-update: --dry-run — ${coreUpdateNote(readInstalledCoreVersion(cwd), dryLatest)}`);
456
475
  } else {
457
476
  const before = readInstalledCoreVersion(cwd);
458
477
  console.log('ferst-update: npm install ferst-core@latest --save ...');
@@ -177,6 +177,16 @@ collections:
177
177
  - { name: provider, label: Provider name, widget: string, default: Google Calendar }
178
178
  - { name: weekStart, label: Week starts on, widget: select, default: monday, options: [{ label: "Monday", value: monday }, { label: "Sunday", value: sunday }] }
179
179
 
180
+ - name: posts-settings
181
+ label: News
182
+ file: src/content/postsSettings/index.json
183
+ icon: newspaper
184
+ fields:
185
+ - { name: title, label: Heading, widget: string, default: News }
186
+ - { name: intro, label: Intro, widget: text, required: false }
187
+ - { name: perPage, label: Posts per page, widget: number, default: 12, value_type: int, min: 1, max: 48, hint: "How many posts each news page shows. 12 fills rows of one, two, three or four evenly." }
188
+ - { name: excludeFromIndex, label: Tags kept off the main news page, widget: list, required: false, hint: "Tag names (for example newsletter) whose posts appear only on their own tag page." }
189
+
180
190
  - name: posts
181
191
  label: Posts
182
192
  label_singular: Post
@@ -224,7 +234,7 @@ collections:
224
234
  - name: pages
225
235
  label: Pages
226
236
  label_singular: Page
227
- description: "See each page and request a change — our team makes it and sends it back for you to check. You don't edit the page layout here."
237
+ description: "__PAGES_DESCRIPTION__"
228
238
  folder: src/content/pages
229
239
  extension: json
230
240
  format: json
@@ -234,6 +244,7 @@ collections:
234
244
  identifier_field: title
235
245
  fields:
236
246
  - { name: title, label: Page title, widget: string }
247
+ # ferst:requests (the Basic plan only: bin/cms-config.mjs leaves it out on the free plan)
237
248
  - name: requests
238
249
  label: Requested changes
239
250
  label_singular: Request
@@ -248,6 +259,7 @@ collections:
248
259
  - { name: issue, widget: hidden, required: false }
249
260
  - { name: by, widget: hidden, required: false }
250
261
  - { name: at, widget: hidden, required: false }
262
+ # /ferst:requests
251
263
  - { name: description, label: SEO description, widget: hidden, required: false }
252
264
  - { name: heroImage, widget: hidden, required: false }
253
265
  - { name: align, widget: hidden, required: false }
package/content/posts.ts CHANGED
@@ -43,8 +43,8 @@ export const postsSettingsSchema = z.object({
43
43
  title: z.string().default('News'),
44
44
  /** Optional intro copy for the index. */
45
45
  intro: z.string().default(''),
46
- /** Posts per page in the paginated index. */
47
- perPage: z.number().int().min(1).default(9),
46
+ /** Posts per page in the paginated index. 12 fills rows of 1, 2, 3 or 4 tiles evenly. */
47
+ perPage: z.number().int().min(1).default(12),
48
48
  /** Tag slugs excluded from the MAIN index (e.g. a `newsletter` tag that has its
49
49
  * own view). Kept generic — no capability knows what "newsletter" means. */
50
50
  excludeFromIndex: z.array(z.string()).default([]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ferst-core",
3
- "version": "0.9.0",
3
+ "version": "0.9.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",