create-substrat 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-substrat",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Scaffold a Substrat vertical — `npm create substrat <dir>`.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -19,8 +19,13 @@
19
19
  * This file lives in `.substrat/` — the tool-neutral home, next to `playbook.md` —
20
20
  * not in `.claude/`. `.claude/settings.json` is a three-line adapter that runs it,
21
21
  * and any other client that grows a session hook binds the same way. It also
22
- * means the plugin distribution (#753) can ship this script unchanged rather than
23
- * forking it.
22
+ * means the plugin distribution (#753) ships this script unchanged rather than
23
+ * forking it: `plugin/substrat/scripts/session-start.mjs` is emitted from this
24
+ * file byte-for-byte, and `pnpm lint:plugin --check` fails if the two diverge.
25
+ *
26
+ * The plugin copy is what reaches a project scaffolded before this hook existed.
27
+ * When a project owns its own copy, the plugin's stays silent — see
28
+ * `isPluginCopy()` below — so a scaffolded project announces itself once.
24
29
  *
25
30
  * ## Deliberately silent, and deliberately offline
26
31
  *
@@ -33,8 +38,9 @@
33
38
  *
34
39
  * Opt out by creating `.substrat/no-session-context`.
35
40
  */
36
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
37
- import { dirname, join } from 'node:path';
41
+ import { existsSync, readFileSync, realpathSync, writeFileSync, mkdirSync } from 'node:fs';
42
+ import { dirname, join, relative, isAbsolute } from 'node:path';
43
+ import { fileURLToPath } from 'node:url';
38
44
 
39
45
  const DOCS = 'https://substrat.net';
40
46
  const KERNEL = '@substrat-run/kernel';
@@ -42,6 +48,33 @@ const KERNEL = '@substrat-run/kernel';
42
48
  /** Where the project is. Claude Code sets this; fall back to the cwd it ran us in. */
43
49
  const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
44
50
 
51
+ /** Where this script is. The two copies differ only in where they sit on disk. */
52
+ const self = fileURLToPath(import.meta.url);
53
+
54
+ /** Symlinks resolved, so a project reached through one is not mistaken for elsewhere. */
55
+ function real(path) {
56
+ try {
57
+ return realpathSync(path);
58
+ } catch {
59
+ return path;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * True when we are the plugin's copy rather than the project's own.
65
+ *
66
+ * Asked positionally instead of by a flag, because the two copies must stay
67
+ * byte-identical for `lint:plugin` to have anything to check: a copy that is
68
+ * invoked differently is a copy that can be edited differently. Both sides are
69
+ * resolved first — on macOS a project under `/tmp` is handed to us as
70
+ * `/private/tmp`, and an unresolved compare would read the project's own copy as
71
+ * foreign and silence the very hook that should speak.
72
+ */
73
+ function isPluginCopy() {
74
+ const inside = relative(real(root), real(self));
75
+ return inside.startsWith('..') || isAbsolute(inside);
76
+ }
77
+
45
78
  /** The marker: which kernel version this project last announced. */
46
79
  const MARKER = join(root, '.substrat', '.docs-pin');
47
80
  const OPT_OUT = join(root, '.substrat', 'no-session-context');
@@ -67,6 +100,11 @@ function main() {
67
100
  // file to invent, and nothing to keep in sync.
68
101
  if (!pkg?.substrat) silent();
69
102
 
103
+ // The project's own copy wins. It ships beside the playbook it points at, so it
104
+ // is the one that matches what is actually checked out here; the plugin's exists
105
+ // for projects scaffolded before this hook did, and must not double-announce.
106
+ if (isPluginCopy() && existsSync(join(root, '.substrat', 'hooks', 'session-start.mjs'))) silent();
107
+
70
108
  if (existsSync(OPT_OUT)) silent();
71
109
 
72
110
  // What is actually installed beats what package.json asked for: a caret range on