@natjswenson/press 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -5,6 +5,52 @@ All notable changes to the **press** skill are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
6
6
  this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.0] - 2026-08-01
9
+
10
+ ### Added
11
+
12
+ - **`version-badge` emitter** — a generated `press:version` line for a
13
+ consumer's README. Until now a repo's press version appeared only in a CI pin
14
+ and a comment marker, neither of which anyone reads when landing on the repo.
15
+ - Emitters now receive a small run context (`{ version }`), kept separate from
16
+ target `params` because it is a property of the invocation, not the config.
17
+
18
+ ### Changed
19
+
20
+ - **Propagation now covers every long-lived branch, not just `dev`.** Landing on
21
+ the integration branch alone left `main` — the branch that actually ships —
22
+ stale until someone remembered to promote, and made the next promotion carry
23
+ an unrelated brand diff. Branch names are suffixed per base so the two pushes
24
+ cannot collide.
25
+
26
+ ### Fixed
27
+
28
+ - **Target selection could match the wrong repository.** Selection was by file
29
+ presence alone, and `README.md` exists everywhere — so a README target
30
+ selected inside *any* checkout and would have been compared against the wrong
31
+ file. Selection now checks the checkout's own `origin` remote against the
32
+ target's `github`/`repo`, falling back to presence only when there is no
33
+ remote.
34
+ - Goldens pin the **shape** of each emitter's output using a fixed placeholder
35
+ version. Using the real release would rewrite every golden on every release
36
+ and turn a drift detector into noise.
37
+
38
+ ## [0.6.1] - 2026-08-01
39
+
40
+ ### Fixed
41
+
42
+ - **Every propagate push failed on missing git credentials, and the job still
43
+ reported success.** `gh` authenticates its own API calls from `GH_TOKEN`, so
44
+ cloning and PR listing worked, but a plain `git push` has no credential helper
45
+ and fails with `could not read Username for 'https://github.com'`. All four
46
+ consumers failed this way on the 0.6.0 release. `gh auth setup-git` now wires
47
+ gh in as git's credential helper.
48
+ - **A failed fan-out now fails the job.** Failures were a warning, on the
49
+ reasoning that the release had already happened and the weekly run would
50
+ retry. That was wrong: a fan-out that silently does nothing is worse than one
51
+ that never ran, because the green tick claims the brand reached everywhere
52
+ when it reached nowhere.
53
+
8
54
  ## [0.6.0] - 2026-08-01
9
55
 
10
56
  ### Changed
@@ -248,6 +294,8 @@ source of truth and a CI drift gate.
248
294
  rasterised cards, whose eyebrow legitimately runs at `.16em`. Scoped rather
249
295
  than waived, and caught by linting the real shipped corpus.
250
296
 
297
+ [0.7.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.7.0
298
+ [0.6.1]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.6.1
251
299
  [0.6.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.6.0
252
300
  [0.5.1]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.5.1
253
301
  [0.5.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.5.0
package/SKILL.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: press
3
3
  description: The one brand system for everything produced in Claude — design tokens, the visual laws, the run-presentation contract, and the universal voice core. Use when composing or restyling any artifact (report, résumé, card, cover, PDF, HTML page, chart), when asked about brand colors, fonts, the accent law or "the PRESS look", when adding a new skill that renders anything, or when a brand value needs to change everywhere at once. Also handles "check the brand is in sync", "why do my colors differ", and onboarding a new consumer repo.
4
4
  user_invocable: true
5
- version: 0.6.0
5
+ version: 0.7.0
6
6
  ---
7
7
 
8
8
  # /press — the brand system
@@ -121,8 +121,14 @@ is that the PR says which kind it is:
121
121
  | **Brand values moved** | `press vX — BRAND VALUES CHANGED` | read the diff as a design change |
122
122
  | **Version only** | `adopt press vX` | one line; nothing renders differently |
123
123
 
124
- `.github/workflows/press-propagate.yml` runs this on every release and weekly,
125
- opening a PR in each consumer repo whose bytes actually moved.
124
+ `.github/workflows/press-propagate.yml` runs this on every release and weekly.
125
+ It propagates to **every long-lived branch** a consumer has `dev` *and* the
126
+ default branch — because landing on `dev` alone leaves the branch that actually
127
+ ships stale until someone remembers to promote, and makes the next promotion
128
+ carry an unrelated brand diff.
129
+
130
+ Each consumer's README carries a generated `press:version` line, so the release
131
+ a repo is on is visible without opening a workflow file.
126
132
 
127
133
  ## Changing a brand value
128
134
 
package/bin/press.js CHANGED
@@ -86,7 +86,7 @@ function cmdEmit({ tokens, root, values }) {
86
86
  const rows = [];
87
87
  for (const target of targets) {
88
88
  const path = targetPath(target, root);
89
- const body = emitBody(tokens, target.emitter, target.params ?? {});
89
+ const body = emitBody(tokens, target.emitter, target.params ?? {}, { version: VERSION });
90
90
  const before = readFileSync(path, 'utf8');
91
91
  const has = findRegion(before, target.region, target.syntax);
92
92
 
package/lib/check.mjs CHANGED
@@ -37,7 +37,8 @@ export function checkTarget(target, root, tokens, version) {
37
37
  }
38
38
  if (!found) return { target, path, status: 'missing' };
39
39
 
40
- const expected = emitBody(tokens, target.emitter, target.params ?? {}).replace(/\s+$/, '');
40
+ const expected = emitBody(tokens, target.emitter, target.params ?? {}, { version })
41
+ .replace(/\s+$/, '');
41
42
  const actual = found.body.replace(/\s+$/, '');
42
43
  if (expected === actual) {
43
44
  return { target, path, status: 'ok', writtenBy: found.version };
package/lib/emit.mjs CHANGED
@@ -292,23 +292,49 @@ function pythonConsts(tokens, params) {
292
292
  return out.join('\n');
293
293
  }
294
294
 
295
+
296
+ /**
297
+ * A short "this repo is on press vX" note for a consumer's README.
298
+ *
299
+ * The version is the point: without it, the only places a repo's press version
300
+ * appears are a CI pin and a comment marker, neither of which anyone reads. A
301
+ * reader landing on the repo should be able to see which brand release it is on
302
+ * without opening a workflow file.
303
+ */
304
+ function versionBadge(tokens, params, ctx) {
305
+ const version = ctx.version ?? '0.0.0';
306
+ const repo = params.repo_url ?? 'https://github.com/natejswenson/claude-skills/tree/main/skills/press';
307
+ const what = params.what ?? 'Design tokens, brand laws and run presentation';
308
+ return [
309
+ `**Brand:** PRESS v${version} — ${what} are generated from the`,
310
+ `[\`press\`](${repo}) skill, not maintained here. The token blocks marked`,
311
+ '`press:tokens` are generated; edit `press/brand/tokens.json` and re-emit instead.',
312
+ ].join('\n');
313
+ }
314
+
295
315
  export const EMITTERS = {
296
316
  'python-theme': pythonTheme,
297
317
  'python-consts': pythonConsts,
318
+ 'version-badge': versionBadge,
298
319
  'css-vars': cssVars,
299
320
  'md-palette': mdPalette,
300
321
  'markdown-block': markdownBlock,
301
322
  json: jsonTokens,
302
323
  };
303
324
 
304
- export function emitBody(tokens, emitter, params = {}) {
325
+ /**
326
+ * @param ctx Run context available to emitters — currently `{ version }`.
327
+ * Kept separate from `params` because it is a property of the
328
+ * invocation, not of the target's configuration.
329
+ */
330
+ export function emitBody(tokens, emitter, params = {}, ctx = {}) {
305
331
  const fn = EMITTERS[emitter];
306
332
  if (!fn) {
307
333
  throw new EmitError(
308
334
  `unknown emitter "${emitter}" (expected one of: ${Object.keys(EMITTERS).join(', ')})`,
309
335
  );
310
336
  }
311
- return fn(tokens, params);
337
+ return fn(tokens, params, ctx);
312
338
  }
313
339
 
314
340
  // --------------------------------------------------------------------------
package/lib/propagate.mjs CHANGED
@@ -50,7 +50,7 @@ export function propagate({ tokens, targets, root, version, dryRun = false }) {
50
50
  regions.push({ id: target.id, path: target.path, status: 'missing' });
51
51
  continue;
52
52
  }
53
- const body = emitBody(tokens, target.emitter, target.params ?? {});
53
+ const body = emitBody(tokens, target.emitter, target.params ?? {}, { version });
54
54
  const after = spliceRegion(before, target.region, target.syntax, body, version);
55
55
  // Two different kinds of "changed", kept apart on purpose.
56
56
  //
package/lib/targets.mjs CHANGED
@@ -6,6 +6,7 @@
6
6
  * `check`, which is why `doctor` exists to show the whole registry rather than
7
7
  * only what resolved locally.
8
8
  */
9
+ import { execFileSync } from 'node:child_process';
9
10
  import { existsSync, readFileSync } from 'node:fs';
10
11
  import { fileURLToPath } from 'node:url';
11
12
  import { dirname, isAbsolute, join, resolve } from 'node:path';
@@ -43,6 +44,26 @@ export function repoRoot(start = process.cwd()) {
43
44
  export const targetPath = (target, root) =>
44
45
  isAbsolute(target.path) ? target.path : join(root, target.path);
45
46
 
47
+ /**
48
+ * The repository a checkout actually is, from its origin remote.
49
+ *
50
+ * File presence alone cannot identify a consumer: `README.md` exists in every
51
+ * repo, so a README target would select inside any checkout and be checked
52
+ * against the wrong file. Returns null when there is no remote (a temp dir in a
53
+ * test), in which case callers fall back to presence.
54
+ */
55
+ export function repoIdentity(root) {
56
+ try {
57
+ const url = execFileSync('git', ['-C', root, 'config', '--get', 'remote.origin.url'], {
58
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
59
+ }).trim();
60
+ const m = /([^/:]+?)(?:\.git)?$/.exec(url);
61
+ return m ? m[1] : null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
46
67
  /**
47
68
  * Which targets this invocation is responsible for.
48
69
  *
@@ -63,5 +84,12 @@ export function selectTargets(targets, { root, ids }) {
63
84
  return found;
64
85
  });
65
86
  }
66
- return targets.filter((t) => existsSync(targetPath(t, root)));
87
+ const identity = repoIdentity(root);
88
+ return targets.filter((t) => {
89
+ if (!existsSync(targetPath(t, root))) return false;
90
+ // When the checkout names itself, trust that over a path that happens to
91
+ // exist — otherwise every repo's README matches every README target.
92
+ if (identity) return identity === (t.github ?? t.repo);
93
+ return true;
94
+ });
67
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@natjswenson/press",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "One brand system — tokens, laws, run presentation and voice core — generated into every consumer with a CI drift gate",
5
5
  "license": "MIT",
6
6
  "author": "Nate Swenson",
package/targets.json CHANGED
@@ -398,6 +398,51 @@
398
398
  "replaceFrom": "^# --- PRESS tokens",
399
399
  "replaceTo": "^\\}$"
400
400
  }
401
+ },
402
+ {
403
+ "id": "natejswenson.io-readme",
404
+ "repo": "natejswenson.io",
405
+ "path": "README.md",
406
+ "region": "version",
407
+ "syntax": "md",
408
+ "emitter": "version-badge",
409
+ "params": {
410
+ "what": "The site's colour, type and panel tokens"
411
+ }
412
+ },
413
+ {
414
+ "id": "budget-readme",
415
+ "repo": "budget",
416
+ "path": "README.md",
417
+ "region": "version",
418
+ "syntax": "md",
419
+ "emitter": "version-badge",
420
+ "params": {
421
+ "what": "The report's colour and type tokens"
422
+ },
423
+ "github": "local-budget"
424
+ },
425
+ {
426
+ "id": "local-fitness-readme",
427
+ "repo": "local-fitness",
428
+ "path": "README.md",
429
+ "region": "version",
430
+ "syntax": "md",
431
+ "emitter": "version-badge",
432
+ "params": {
433
+ "what": "The brief and report-card colour and type tokens"
434
+ }
435
+ },
436
+ {
437
+ "id": "natejswenson-readme",
438
+ "repo": "natejswenson",
439
+ "path": "README.md",
440
+ "region": "version",
441
+ "syntax": "md",
442
+ "emitter": "version-badge",
443
+ "params": {
444
+ "what": "The profile tiles' colour tokens and vendored faces"
445
+ }
401
446
  }
402
447
  ]
403
448
  }