@dalzoubi/dev-agents-sync 2.0.16 → 2.0.17

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.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Steering scaffold seeding.
3
+ *
4
+ * Steering files (`steering/decisions.md`, `glossary.md`, `architecture.md`) are
5
+ * a persistent, CONSUMER-OWNED memory layer. Agents read all three at session
6
+ * start and append to them; the toolkit seeds blank scaffolds ONCE and then never
7
+ * rewrites them.
8
+ *
9
+ * Invariants enforced here:
10
+ * - Seed-if-absent: a file is written only when it does not already exist. An
11
+ * existing file (edited by the consumer or a prior session) is left
12
+ * byte-identical — never overwritten.
13
+ * - No marker: seeded files carry NO `managed-by` marker, so they are
14
+ * consumer-owned and the lockfile never tracks them.
15
+ * - Atomic: a write failure surfaces a stable `ESTEER_WRITE` code and never
16
+ * leaves a half-written scaffold (each file is written whole or not at all).
17
+ * - --dry-run writes nothing.
18
+ *
19
+ * The scaffold bodies are inlined (not read from disk) so the published CLI is
20
+ * self-contained and does not depend on the canonical `.agents/steering/` tree
21
+ * at runtime. They are deliberately blank/minimal — a heading plus a one-line
22
+ * "what goes here" comment — and carry no marker and no consumer-specific
23
+ * language.
24
+ */
25
+
26
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
27
+ import path from 'node:path';
28
+
29
+ /** The three steering scaffold files and their blank/minimal seed bodies. */
30
+ export const STEERING_SCAFFOLDS = {
31
+ 'decisions.md':
32
+ '# Decisions\n\n' +
33
+ '<!-- What goes here: durable, dated decisions and their rationale, one line each ' +
34
+ '(YYYY-MM-DD — <decision>: <why>). Consumer-owned; agents append, never rewrite. -->\n',
35
+ 'glossary.md':
36
+ '# Glossary\n\n' +
37
+ '<!-- What goes here: project-specific terms and their definitions, one entry each. ' +
38
+ 'Consumer-owned; keep it the shared vocabulary every agent reads. -->\n',
39
+ 'architecture.md':
40
+ '# Architecture\n\n' +
41
+ '<!-- What goes here: load-bearing architecture notes, core patterns, and constraints ' +
42
+ 'agents should respect. Consumer-owned; keep it current. -->\n',
43
+ };
44
+
45
+ class SteeringWriteError extends Error {
46
+ constructor(message, { exitCode = 2, cause } = {}) {
47
+ super(message);
48
+ this.name = 'SteeringWriteError';
49
+ this.code = 'ESTEER_WRITE';
50
+ this.exitCode = exitCode;
51
+ if (cause) this.cause = cause;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Seeds the steering scaffold into <consumerCwd>/.claude/steering/, writing
57
+ * each scaffold file only when it is absent.
58
+ *
59
+ * @param {string} consumerCwd
60
+ * @param {{ dryRun?: boolean }} [opts]
61
+ * @returns {{ seeded: string[], present: string[], dryRun: boolean }}
62
+ * seeded — relative paths (steering/<name>) that were (or would be) written.
63
+ * present — relative paths left untouched because they already existed.
64
+ */
65
+ export function seedSteering(consumerCwd, opts = {}) {
66
+ const { dryRun = false } = opts;
67
+ const steeringDir = path.join(consumerCwd, '.claude', 'steering');
68
+
69
+ const seeded = [];
70
+ const present = [];
71
+
72
+ // First pass: decide what to write without touching the filesystem, so a
73
+ // dry-run is read-only and a real run knows the full plan up front.
74
+ const toWrite = [];
75
+ for (const [filename, content] of Object.entries(STEERING_SCAFFOLDS)) {
76
+ const absPath = path.join(steeringDir, filename);
77
+ const rel = `steering/${filename}`;
78
+ if (existsSync(absPath)) {
79
+ present.push(rel);
80
+ } else {
81
+ toWrite.push({ absPath, content, rel });
82
+ }
83
+ }
84
+
85
+ if (dryRun) {
86
+ for (const w of toWrite) seeded.push(w.rel);
87
+ return { seeded, present, dryRun: true };
88
+ }
89
+
90
+ // Write whole files only. A failure surfaces ESTEER_WRITE; because each file
91
+ // is written in a single writeFileSync, a failure never leaves a file
92
+ // half-written (the OS write is the unit of atomicity we rely on here).
93
+ try {
94
+ if (toWrite.length > 0) {
95
+ mkdirSync(steeringDir, { recursive: true });
96
+ }
97
+ for (const w of toWrite) {
98
+ writeFileSync(w.absPath, w.content, 'utf8');
99
+ seeded.push(w.rel);
100
+ }
101
+ } catch (err) {
102
+ throw new SteeringWriteError(
103
+ `ESTEER_WRITE: could not write ${path.join('.claude', 'steering')} — check directory permissions`,
104
+ { cause: err },
105
+ );
106
+ }
107
+
108
+ return { seeded, present, dryRun: false };
109
+ }
110
+
111
+ /** Human-readable summary line consistent with the existing CLI output style. */
112
+ export function steeringSummary(result) {
113
+ if (result.seeded.length > 0) {
114
+ return `steering/ seeded (${result.seeded.length} file${result.seeded.length === 1 ? '' : 's'})`;
115
+ }
116
+ return 'steering/ present — left untouched';
117
+ }
package/src/writer.mjs CHANGED
@@ -8,10 +8,22 @@
8
8
  * Rejects path traversal (`..`).
9
9
  */
10
10
 
11
- import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
11
+ import { mkdirSync, writeFileSync, existsSync, readFileSync, chmodSync } from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import { hasMarker, hasMarkerOnFirstLine } from './marker.mjs';
14
14
 
15
+ /**
16
+ * Managed hook scripts (`.claude/hooks/*.sh`) must be executable so Claude Code
17
+ * can run them. Other managed files are plain content and keep default perms.
18
+ */
19
+ function isExecutableHookKey(relKey) {
20
+ return (
21
+ typeof relKey === 'string' &&
22
+ /(^|\/)hooks\//.test(relKey.replace(/\\/g, '/')) &&
23
+ relKey.endsWith('.sh')
24
+ );
25
+ }
26
+
15
27
  const TARGET_PREFIX = {
16
28
  claude: '.claude',
17
29
  cursor: '.cursor',
@@ -144,6 +156,12 @@ export function writeManagedFile({ absPath, relKey, content, force }) {
144
156
  }
145
157
  mkdirSync(path.dirname(absPath), { recursive: true });
146
158
  writeFileSync(absPath, content, 'utf8');
159
+ // Hook scripts are executables — set the exec bit deterministically (0o755)
160
+ // so they are runnable immediately after install. chmod is a no-op on
161
+ // platforms without POSIX permissions, so this is safe everywhere.
162
+ if (isExecutableHookKey(relKey)) {
163
+ chmodSync(absPath, 0o755);
164
+ }
147
165
  }
148
166
 
149
167
  /** Returns true if the path under <root>/.claude or <root>/.cursor exists with content matching `expected`. */