@njeybe/nativ 2.0.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.
Files changed (140) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +334 -0
  4. package/bin/cli.js +31 -0
  5. package/dist/commands/bench.d.ts +8 -0
  6. package/dist/commands/bench.js +52 -0
  7. package/dist/commands/db.d.ts +22 -0
  8. package/dist/commands/db.js +415 -0
  9. package/dist/commands/doctor.d.ts +38 -0
  10. package/dist/commands/doctor.js +169 -0
  11. package/dist/commands/hook.d.ts +31 -0
  12. package/dist/commands/hook.js +105 -0
  13. package/dist/commands/init.d.ts +3 -0
  14. package/dist/commands/init.js +144 -0
  15. package/dist/commands/mcp.d.ts +5 -0
  16. package/dist/commands/mcp.js +16 -0
  17. package/dist/commands/setup.d.ts +17 -0
  18. package/dist/commands/setup.js +51 -0
  19. package/dist/commands/status.d.ts +5 -0
  20. package/dist/commands/status.js +75 -0
  21. package/dist/commands/task.d.ts +83 -0
  22. package/dist/commands/task.js +807 -0
  23. package/dist/commands/test-gen.d.ts +14 -0
  24. package/dist/commands/test-gen.js +52 -0
  25. package/dist/commands/triage.d.ts +70 -0
  26. package/dist/commands/triage.js +343 -0
  27. package/dist/commands/update.d.ts +14 -0
  28. package/dist/commands/update.js +112 -0
  29. package/dist/commands/validate.d.ts +1 -0
  30. package/dist/commands/validate.js +89 -0
  31. package/dist/commands/verify.d.ts +8 -0
  32. package/dist/commands/verify.js +65 -0
  33. package/dist/commands/worktree.d.ts +47 -0
  34. package/dist/commands/worktree.js +396 -0
  35. package/dist/core/benchmark.d.ts +57 -0
  36. package/dist/core/benchmark.js +772 -0
  37. package/dist/core/enforcement.d.ts +80 -0
  38. package/dist/core/enforcement.js +271 -0
  39. package/dist/core/legacy-templates.d.ts +24 -0
  40. package/dist/core/legacy-templates.js +60 -0
  41. package/dist/core/lock-manager.d.ts +54 -0
  42. package/dist/core/lock-manager.js +260 -0
  43. package/dist/core/root-resolver.d.ts +32 -0
  44. package/dist/core/root-resolver.js +149 -0
  45. package/dist/core/setup-assets.d.ts +123 -0
  46. package/dist/core/setup-assets.js +438 -0
  47. package/dist/core/telemetry.d.ts +156 -0
  48. package/dist/core/telemetry.js +416 -0
  49. package/dist/core/test-generator-types.d.ts +227 -0
  50. package/dist/core/test-generator-types.js +179 -0
  51. package/dist/core/test-generator.d.ts +34 -0
  52. package/dist/core/test-generator.js +1690 -0
  53. package/dist/core/tier1-liaison.d.ts +139 -0
  54. package/dist/core/tier1-liaison.js +320 -0
  55. package/dist/core/verifier.d.ts +52 -0
  56. package/dist/core/verifier.js +249 -0
  57. package/dist/core/version.d.ts +5 -0
  58. package/dist/core/version.js +14 -0
  59. package/dist/core/worktree-merge.d.ts +30 -0
  60. package/dist/core/worktree-merge.js +98 -0
  61. package/dist/db/data-engine.d.ts +46 -0
  62. package/dist/db/data-engine.js +802 -0
  63. package/dist/db/diff.d.ts +39 -0
  64. package/dist/db/diff.js +420 -0
  65. package/dist/db/env-parser.d.ts +86 -0
  66. package/dist/db/env-parser.js +549 -0
  67. package/dist/db/introspector.d.ts +21 -0
  68. package/dist/db/introspector.js +835 -0
  69. package/dist/db/types.d.ts +217 -0
  70. package/dist/db/types.js +9 -0
  71. package/dist/governor/circuit-breaker.d.ts +100 -0
  72. package/dist/governor/circuit-breaker.js +231 -0
  73. package/dist/governor/evaluator.d.ts +67 -0
  74. package/dist/governor/evaluator.js +851 -0
  75. package/dist/governor/index.d.ts +6 -0
  76. package/dist/governor/index.js +6 -0
  77. package/dist/governor/rules/api-rules.d.ts +2 -0
  78. package/dist/governor/rules/api-rules.js +140 -0
  79. package/dist/governor/rules/db-rules.d.ts +2 -0
  80. package/dist/governor/rules/db-rules.js +180 -0
  81. package/dist/governor/store.d.ts +33 -0
  82. package/dist/governor/store.js +84 -0
  83. package/dist/governor/types.d.ts +64 -0
  84. package/dist/governor/types.js +1 -0
  85. package/dist/index.d.ts +3 -0
  86. package/dist/index.js +358 -0
  87. package/dist/mcp/server.d.ts +3 -0
  88. package/dist/mcp/server.js +309 -0
  89. package/dist/providers/claude-api.d.ts +5 -0
  90. package/dist/providers/claude-api.js +75 -0
  91. package/dist/providers/claude-cli.d.ts +13 -0
  92. package/dist/providers/claude-cli.js +134 -0
  93. package/dist/providers/gemini.d.ts +8 -0
  94. package/dist/providers/gemini.js +78 -0
  95. package/dist/providers/index.d.ts +5 -0
  96. package/dist/providers/index.js +5 -0
  97. package/dist/providers/registry.d.ts +38 -0
  98. package/dist/providers/registry.js +121 -0
  99. package/dist/providers/types.d.ts +74 -0
  100. package/dist/providers/types.js +25 -0
  101. package/dist/runner/agent-supervisor.d.ts +363 -0
  102. package/dist/runner/agent-supervisor.js +1672 -0
  103. package/dist/scanner/context-builder.d.ts +2 -0
  104. package/dist/scanner/context-builder.js +29 -0
  105. package/dist/scanner/detector.d.ts +2 -0
  106. package/dist/scanner/detector.js +760 -0
  107. package/dist/scanner/types.d.ts +80 -0
  108. package/dist/scanner/types.js +50 -0
  109. package/dist/server/studio-server.d.ts +39 -0
  110. package/dist/server/studio-server.js +1931 -0
  111. package/dist/server/studio-ui.d.ts +15 -0
  112. package/dist/server/studio-ui.js +5321 -0
  113. package/package.json +70 -0
  114. package/plugin/.claude-plugin/plugin.json +18 -0
  115. package/plugin/.mcp.json +10 -0
  116. package/plugin/agents/architect.md +28 -0
  117. package/plugin/agents/verifier.md +28 -0
  118. package/plugin/agents/worker.md +26 -0
  119. package/plugin/hooks/hooks.json +28 -0
  120. package/plugin/skills/nativ/SKILL.md +75 -0
  121. package/templates/.claudeignore +45 -0
  122. package/templates/AGENTS.md +67 -0
  123. package/templates/CLAUDE.md +32 -0
  124. package/templates/GEMINI.md +66 -0
  125. package/templates/claude-agents/architect.md +28 -0
  126. package/templates/claude-agents/verifier.md +28 -0
  127. package/templates/claude-agents/worker.md +26 -0
  128. package/templates/dot-ai/api_contracts.json +66 -0
  129. package/templates/dot-ai/context.md +38 -0
  130. package/templates/dot-ai/db_schema.json +39 -0
  131. package/templates/dot-ai/master_plan.json +28 -0
  132. package/templates/dot-ai/subagents/backend.md +11 -0
  133. package/templates/dot-ai/subagents/database.md +10 -0
  134. package/templates/dot-ai/subagents/db-migration.md +11 -0
  135. package/templates/dot-ai/subagents/devops-agent.md +11 -0
  136. package/templates/dot-ai/subagents/flutter-developer.md +11 -0
  137. package/templates/dot-ai/subagents/frontend.md +126 -0
  138. package/templates/dot-ai/subagents/qa-tester.md +13 -0
  139. package/templates/dot-ai/subagents/security-auditor.md +11 -0
  140. package/templates/dot-ai/ui_specs.md +78 -0
@@ -0,0 +1,260 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import lockfile from 'proper-lockfile';
4
+ import pc from 'picocolors';
5
+ export const DEFAULT_LOCK_OPTIONS = {
6
+ stale: 10000, // 10 seconds stale timeout prevents deadlocks if a process crashes
7
+ retries: {
8
+ retries: 40,
9
+ factor: 1.2,
10
+ minTimeout: 20,
11
+ maxTimeout: 500,
12
+ },
13
+ realpath: false, // Prevents symlink/junction resolution mismatch in git worktrees
14
+ };
15
+ /**
16
+ * Executes an operation holding an advisory lock on a given file.
17
+ * Automatically releases the lock when the operation finishes (even on error).
18
+ */
19
+ export async function withFileLock(filePath, fn, options = {}) {
20
+ const mergedOptions = { ...DEFAULT_LOCK_OPTIONS, ...options };
21
+ // proper-lockfile requires the target file to exist
22
+ if (!fs.existsSync(filePath)) {
23
+ const dir = path.dirname(filePath);
24
+ if (!fs.existsSync(dir)) {
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ }
27
+ fs.writeFileSync(filePath, '', 'utf8');
28
+ }
29
+ const release = await lockfile.lock(filePath, mergedOptions);
30
+ try {
31
+ return await fn();
32
+ }
33
+ finally {
34
+ try {
35
+ await release();
36
+ }
37
+ catch {
38
+ // Best-effort release (in case lock was already freed or invalidated)
39
+ }
40
+ }
41
+ }
42
+ /**
43
+ * Loads a master plan from disk without acquiring an advisory lock.
44
+ * Includes automatic transient retries to handle atomic file swap collisions (e.g. Windows EBUSY).
45
+ * Returns null if the file does not exist or fails to parse.
46
+ */
47
+ export function loadPlan(planPath, options = {}) {
48
+ if (!fs.existsSync(planPath)) {
49
+ if (!options.silent) {
50
+ console.error(pc.red(`\n✖ No .ai/master_plan.json found at: ${planPath}`));
51
+ console.log(pc.yellow('Run `nativ init` first to scaffold the workflow.\n'));
52
+ }
53
+ return null;
54
+ }
55
+ const maxAttempts = options.retries ?? 10;
56
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
57
+ try {
58
+ const raw = fs.readFileSync(planPath, 'utf8');
59
+ return JSON.parse(raw);
60
+ }
61
+ catch (err) {
62
+ if (attempt === maxAttempts - 1) {
63
+ if (!options.silent) {
64
+ console.error(pc.red(`\n✖ Failed to parse .ai/master_plan.json: ${err.message}\n`));
65
+ }
66
+ return null;
67
+ }
68
+ // Brief synchronous pause to allow concurrent rename to finalize
69
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+ /**
75
+ * Atomically writes the plan using a temp file rename with Windows EPERM/EBUSY resilience.
76
+ * Creates a unique temp file to prevent rename collisions across threads/processes.
77
+ */
78
+ export function savePlan(planPath, plan) {
79
+ const tmpPath = `${planPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
80
+ try {
81
+ plan.lastUpdated = new Date().toISOString();
82
+ fs.writeFileSync(tmpPath, JSON.stringify(plan, null, 2) + '\n', 'utf8');
83
+ // Windows NTFS atomic rename retry loop
84
+ let renamed = false;
85
+ for (let attempt = 0; attempt < 10; attempt++) {
86
+ try {
87
+ fs.renameSync(tmpPath, planPath);
88
+ renamed = true;
89
+ break;
90
+ }
91
+ catch (err) {
92
+ if (err.code === 'EPERM' || err.code === 'EBUSY') {
93
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
94
+ }
95
+ else {
96
+ throw err;
97
+ }
98
+ }
99
+ }
100
+ if (!renamed) {
101
+ // Fallback: direct atomic copyFileSync + unlinkSync under advisory lock
102
+ fs.copyFileSync(tmpPath, planPath);
103
+ try {
104
+ fs.unlinkSync(tmpPath);
105
+ }
106
+ catch {
107
+ // Best-effort cleanup of temp file
108
+ }
109
+ }
110
+ return true;
111
+ }
112
+ catch (err) {
113
+ fs.rmSync(tmpPath, { force: true });
114
+ console.error(pc.red(`\n✖ Failed to save .ai/master_plan.json: ${err.message}\n`));
115
+ return false;
116
+ }
117
+ }
118
+ /**
119
+ * Safely executes a mutation on master_plan.json under an OS-level advisory lock.
120
+ * Ensures an atomic Read-Modify-Write cycle across concurrent agent processes.
121
+ */
122
+ export async function withPlanLock(planPath, mutator, options = {}) {
123
+ if (!fs.existsSync(planPath)) {
124
+ console.error(pc.red(`\n✖ No .ai/master_plan.json found at: ${planPath}`));
125
+ console.log(pc.yellow('Run `nativ init` first to scaffold the workflow.\n'));
126
+ return null;
127
+ }
128
+ return withFileLock(planPath, async () => {
129
+ const plan = loadPlan(planPath);
130
+ if (!plan) {
131
+ console.error(pc.red(`\n✖ Failed to parse .ai/master_plan.json at: ${planPath}\n`));
132
+ return null;
133
+ }
134
+ let aborted = false;
135
+ const ctx = {
136
+ abort: () => {
137
+ aborted = true;
138
+ },
139
+ isAborted: () => aborted,
140
+ };
141
+ const result = await mutator(plan, ctx);
142
+ if (!aborted) {
143
+ savePlan(planPath, plan);
144
+ }
145
+ return result;
146
+ }, options);
147
+ }
148
+ /**
149
+ * Synchronous counterpart of withFileLock for callers that cannot await (the governor runs inside
150
+ * synchronous verdicts). Uses the same `<file>.lock` directory, so it excludes async holders too.
151
+ */
152
+ export function withFileLockSync(filePath, fn, options = {}) {
153
+ const { retries: _ignored, ...syncOptions } = { ...DEFAULT_LOCK_OPTIONS, ...options };
154
+ if (!fs.existsSync(filePath)) {
155
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
156
+ fs.writeFileSync(filePath, '', 'utf8');
157
+ }
158
+ let release = null;
159
+ // ~5s of contention before giving up: parallel agents trip the breaker at the same moment.
160
+ const maxAttempts = 200;
161
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
162
+ try {
163
+ release = lockfile.lockSync(filePath, syncOptions);
164
+ break;
165
+ }
166
+ catch (err) {
167
+ if (attempt === maxAttempts - 1 || err.code !== 'ELOCKED')
168
+ throw err;
169
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
170
+ }
171
+ }
172
+ try {
173
+ return fn();
174
+ }
175
+ finally {
176
+ try {
177
+ release?.();
178
+ }
179
+ catch {
180
+ // Best-effort release
181
+ }
182
+ }
183
+ }
184
+ /**
185
+ * Writes JSON through a unique temp file and a rename, so readers never observe a half-written
186
+ * file. Retries the rename on Windows sharing violations, like savePlan().
187
+ */
188
+ export function writeJsonAtomicSync(filePath, data) {
189
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
190
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
191
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
192
+ try {
193
+ for (let attempt = 0;; attempt++) {
194
+ try {
195
+ fs.renameSync(tmpPath, filePath);
196
+ return;
197
+ }
198
+ catch (err) {
199
+ if (attempt >= 10 || (err.code !== 'EPERM' && err.code !== 'EBUSY'))
200
+ throw err;
201
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
202
+ }
203
+ }
204
+ }
205
+ catch (err) {
206
+ fs.rmSync(tmpPath, { force: true });
207
+ throw err;
208
+ }
209
+ }
210
+ /**
211
+ * Synchronous variant of withPlanLock using proper-lockfile lockSync.
212
+ */
213
+ export function withPlanLockSync(planPath, mutator, options = {}) {
214
+ if (!fs.existsSync(planPath)) {
215
+ return null;
216
+ }
217
+ const { retries: _ignored, ...syncOptions } = { ...DEFAULT_LOCK_OPTIONS, ...options };
218
+ let release = null;
219
+ const maxAttempts = 15;
220
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
221
+ try {
222
+ release = lockfile.lockSync(planPath, syncOptions);
223
+ break;
224
+ }
225
+ catch (err) {
226
+ if (attempt === maxAttempts - 1 || err.code !== 'ELOCKED') {
227
+ throw err;
228
+ }
229
+ // Brief synchronous pause
230
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
231
+ }
232
+ }
233
+ if (!release)
234
+ return null;
235
+ try {
236
+ const plan = loadPlan(planPath, { silent: true });
237
+ if (!plan)
238
+ return null;
239
+ let aborted = false;
240
+ const ctx = {
241
+ abort: () => {
242
+ aborted = true;
243
+ },
244
+ isAborted: () => aborted,
245
+ };
246
+ const result = mutator(plan, ctx);
247
+ if (!aborted) {
248
+ savePlan(planPath, plan);
249
+ }
250
+ return result;
251
+ }
252
+ finally {
253
+ try {
254
+ release();
255
+ }
256
+ catch {
257
+ // Best-effort release
258
+ }
259
+ }
260
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resolves the primary project root containing the .ai/ contract specifications.
3
+ * When running inside an isolated Git worktree (.worktrees/task-*), this safely
4
+ * climbs back to the parent repository root so contracts, master plans, and
5
+ * schemas remain accessible to sub-agents.
6
+ */
7
+ export declare function resolveProjectRoot(startDirArg?: string): string;
8
+ /**
9
+ * Creates a directory junction (Windows) or symlink (Unix) linking .ai/
10
+ * from the main project root into the newly created worktree so path-relative
11
+ * agent tools continue to work seamlessly.
12
+ */
13
+ export declare function linkWorktreeAiDirectory(worktreeDir: string, rootDir: string): boolean;
14
+ /**
15
+ * Safely unlinks the .ai junction/symlink in a worktree directory before
16
+ * Git or filesystem deletion touches it. On Windows, recursive deletion of an untracked
17
+ * junction by tools like `git worktree remove --force` can traverse into the junction and
18
+ * delete original files in the project root. This guarantees only the link itself is removed.
19
+ */
20
+ export declare function safeUnlinkWorktreeAiDirectory(worktreeDir: string): boolean;
21
+ /**
22
+ * Creates a directory junction (Windows) or symlink (Unix) linking node_modules/
23
+ * from the main project root into the newly created worktree so sub-agents have
24
+ * access to project dependencies without running npm install in each worktree.
25
+ */
26
+ export declare function linkWorktreeNodeModules(worktreeDir: string, rootDir: string): boolean;
27
+ /**
28
+ * Safely unlinks the node_modules junction/symlink in a worktree directory before
29
+ * Git or filesystem deletion touches it, ensuring original node_modules in the
30
+ * project root are never deleted.
31
+ */
32
+ export declare function safeUnlinkWorktreeNodeModules(worktreeDir: string): boolean;
@@ -0,0 +1,149 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Resolves the primary project root containing the .ai/ contract specifications.
5
+ * When running inside an isolated Git worktree (.worktrees/task-*), this safely
6
+ * climbs back to the parent repository root so contracts, master plans, and
7
+ * schemas remain accessible to sub-agents.
8
+ */
9
+ export function resolveProjectRoot(startDirArg) {
10
+ let current = path.resolve(startDirArg || process.cwd());
11
+ // 1. Direct hit: .ai exists in the current directory
12
+ if (fs.existsSync(path.join(current, '.ai'))) {
13
+ return current;
14
+ }
15
+ // 2. Worktree directory convention (.worktrees/task-*)
16
+ const worktreeIdx = current.indexOf(path.join('.worktrees'));
17
+ if (worktreeIdx !== -1) {
18
+ const candidate = current.substring(0, worktreeIdx).replace(/[\\/]$/, '');
19
+ if (fs.existsSync(path.join(candidate, '.ai'))) {
20
+ return candidate;
21
+ }
22
+ }
23
+ // 3. Git worktree file inspection: in worktrees, .git is a file containing "gitdir: <path>"
24
+ const gitPath = path.join(current, '.git');
25
+ if (fs.existsSync(gitPath)) {
26
+ try {
27
+ const stat = fs.lstatSync(gitPath);
28
+ if (stat.isFile()) {
29
+ const content = fs.readFileSync(gitPath, 'utf8').trim();
30
+ const match = content.match(/^gitdir:\s*(.+)$/i);
31
+ if (match) {
32
+ let gitDir = match[1].trim();
33
+ if (!path.isAbsolute(gitDir)) {
34
+ gitDir = path.resolve(current, gitDir);
35
+ }
36
+ // gitDir points to /path/to/repo/.git/worktrees/task-xxx
37
+ // Main repo is 2 levels up from the worktrees metadata folder
38
+ const mainRepo = path.resolve(gitDir, '..', '..');
39
+ if (fs.existsSync(path.join(mainRepo, '.ai'))) {
40
+ return mainRepo;
41
+ }
42
+ }
43
+ }
44
+ }
45
+ catch {
46
+ // Fall through to traversal
47
+ }
48
+ }
49
+ // 4. Upward traversal fallback (climb until we find .ai or filesystem root)
50
+ let prev = '';
51
+ while (current && current !== prev) {
52
+ if (fs.existsSync(path.join(current, '.ai'))) {
53
+ return current;
54
+ }
55
+ prev = current;
56
+ current = path.dirname(current);
57
+ }
58
+ return path.resolve(startDirArg || process.cwd());
59
+ }
60
+ /**
61
+ * Creates a directory junction (Windows) or symlink (Unix) linking .ai/
62
+ * from the main project root into the newly created worktree so path-relative
63
+ * agent tools continue to work seamlessly.
64
+ */
65
+ export function linkWorktreeAiDirectory(worktreeDir, rootDir) {
66
+ const rootAi = path.join(rootDir, '.ai');
67
+ const targetAi = path.join(worktreeDir, '.ai');
68
+ if (!fs.existsSync(rootAi) || fs.existsSync(targetAi)) {
69
+ return false;
70
+ }
71
+ try {
72
+ const isWindows = process.platform === 'win32';
73
+ fs.symlinkSync(rootAi, targetAi, isWindows ? 'junction' : 'dir');
74
+ return true;
75
+ }
76
+ catch {
77
+ // If symlinks fail (restricted permissions), we can copy or rely on resolveProjectRoot
78
+ return false;
79
+ }
80
+ }
81
+ /**
82
+ * Safely unlinks the .ai junction/symlink in a worktree directory before
83
+ * Git or filesystem deletion touches it. On Windows, recursive deletion of an untracked
84
+ * junction by tools like `git worktree remove --force` can traverse into the junction and
85
+ * delete original files in the project root. This guarantees only the link itself is removed.
86
+ */
87
+ export function safeUnlinkWorktreeAiDirectory(worktreeDir) {
88
+ const targetAi = path.join(worktreeDir, '.ai');
89
+ try {
90
+ const stat = fs.lstatSync(targetAi);
91
+ if (stat.isSymbolicLink() || (process.platform === 'win32' && stat.isDirectory())) {
92
+ try {
93
+ fs.unlinkSync(targetAi);
94
+ }
95
+ catch {
96
+ fs.rmdirSync(targetAi);
97
+ }
98
+ return true;
99
+ }
100
+ }
101
+ catch {
102
+ // If it doesn't exist or isn't a link, ignore
103
+ }
104
+ return false;
105
+ }
106
+ /**
107
+ * Creates a directory junction (Windows) or symlink (Unix) linking node_modules/
108
+ * from the main project root into the newly created worktree so sub-agents have
109
+ * access to project dependencies without running npm install in each worktree.
110
+ */
111
+ export function linkWorktreeNodeModules(worktreeDir, rootDir) {
112
+ const rootModules = path.join(rootDir, 'node_modules');
113
+ const targetModules = path.join(worktreeDir, 'node_modules');
114
+ if (!fs.existsSync(rootModules) || fs.existsSync(targetModules)) {
115
+ return false;
116
+ }
117
+ try {
118
+ const isWindows = process.platform === 'win32';
119
+ fs.symlinkSync(rootModules, targetModules, isWindows ? 'junction' : 'dir');
120
+ return true;
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ }
126
+ /**
127
+ * Safely unlinks the node_modules junction/symlink in a worktree directory before
128
+ * Git or filesystem deletion touches it, ensuring original node_modules in the
129
+ * project root are never deleted.
130
+ */
131
+ export function safeUnlinkWorktreeNodeModules(worktreeDir) {
132
+ const targetModules = path.join(worktreeDir, 'node_modules');
133
+ try {
134
+ const stat = fs.lstatSync(targetModules);
135
+ if (stat.isSymbolicLink() || (process.platform === 'win32' && stat.isDirectory())) {
136
+ try {
137
+ fs.unlinkSync(targetModules);
138
+ }
139
+ catch {
140
+ fs.rmdirSync(targetModules);
141
+ }
142
+ return true;
143
+ }
144
+ }
145
+ catch {
146
+ // If it doesn't exist or isn't a link, ignore
147
+ }
148
+ return false;
149
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * nativ · Single source for every file `nativ setup` generates.
3
+ *
4
+ * `planSetup` computes what each asset should contain and whether that differs from disk; `applySetup`
5
+ * writes the differences; `nativ doctor` reuses the same plan to detect drift. Everything merges into
6
+ * existing files: user-authored settings, servers and agent files are never overwritten.
7
+ */
8
+ import { type EnforcementMode } from './enforcement.js';
9
+ export declare const MCP_SERVER_NAME = "nativ";
10
+ export declare const AGENT_FILES: readonly ["architect.md", "worker.md", "verifier.md"];
11
+ export type AssetAction = 'created' | 'updated' | 'unchanged' | 'skipped';
12
+ export interface AssetChange {
13
+ /** Path relative to the project root, forward slashes. */
14
+ path: string;
15
+ action: AssetAction;
16
+ detail?: string;
17
+ }
18
+ export interface CliInvocation {
19
+ command: string;
20
+ prefixArgs: string[];
21
+ /** False when the command embeds an absolute path, which will not work on another machine. */
22
+ portable: boolean;
23
+ }
24
+ export interface SetupOptions {
25
+ /** How Claude Code should invoke nativ, e.g. `npx -y @njeybe/nativ`. Defaults to `nativ` when it is on PATH. */
26
+ command?: string;
27
+ enforcement?: EnforcementMode;
28
+ /** Also overwrite agent files that were edited by hand or are not nativ's. */
29
+ force?: boolean;
30
+ hasBinary?: (name: string) => boolean;
31
+ templatesDir?: string;
32
+ }
33
+ export interface SetupPlan {
34
+ root: string;
35
+ invocation: CliInvocation;
36
+ changes: AssetChange[];
37
+ warnings: string[];
38
+ /** Full new content per changed path, keyed by the same relative path. */
39
+ writes: Map<string, string>;
40
+ }
41
+ export declare function defaultHasBinary(name: string): boolean;
42
+ export declare function resolveCliInvocation(options?: {
43
+ command?: string;
44
+ hasBinary?: (name: string) => boolean;
45
+ }): CliInvocation;
46
+ /** The invocation as a shell string, e.g. `npx -y @njeybe/nativ`, quoting parts that need it. */
47
+ export declare function cliString(invocation: CliInvocation): string;
48
+ export interface DesiredClaudeConfig {
49
+ cli: string;
50
+ mcpServer: {
51
+ command: string;
52
+ args: string[];
53
+ };
54
+ allow: string[];
55
+ deny: string[];
56
+ ask: string[];
57
+ preToolUse: {
58
+ matcher: string;
59
+ command: string;
60
+ timeout: number;
61
+ };
62
+ sessionStart: {
63
+ matcher: string;
64
+ command: string;
65
+ timeout: number;
66
+ };
67
+ }
68
+ export declare function buildDesiredConfig(invocation: CliInvocation): DesiredClaudeConfig;
69
+ type Json = Record<string, unknown>;
70
+ export interface MergeResult {
71
+ merged: Json;
72
+ changes: string[];
73
+ warnings: string[];
74
+ }
75
+ export declare function mergeClaudeSettings(existing: Json, desired: DesiredClaudeConfig): MergeResult;
76
+ export declare function mergeMcpConfig(existing: Json, desired: DesiredClaudeConfig): {
77
+ merged: Json;
78
+ changed: boolean;
79
+ };
80
+ /** CRLF and a leading BOM to plain LF. */
81
+ export declare const toLf: (text: string) => string;
82
+ /** Appends the marker that lets a later run tell "nativ wrote this, untouched" from "a person edited this". */
83
+ export declare function stampManaged(content: string): string;
84
+ export type ManagedState = 'pristine' | 'modified' | 'unmanaged';
85
+ export declare function managedState(content: string): ManagedState;
86
+ export interface TemplateFilePlan {
87
+ action: AssetAction;
88
+ detail?: string;
89
+ /** The stamped content to write; absent when nothing should change. */
90
+ content?: string;
91
+ }
92
+ /**
93
+ * Decides what to do with a file derived from a template (a directive or a role guide).
94
+ * Missing files are created; files nativ wrote and nobody edited (marker intact, or matching a template nativ
95
+ * shipped before markers existed) follow the template; anything edited by hand is kept unless `force` is set.
96
+ * `legacyKey` names the file in LEGACY_TEMPLATE_HASHES.
97
+ */
98
+ export declare function planTemplateFile(existing: string | null, template: string, legacyKey: string, force?: boolean): TemplateFilePlan;
99
+ export declare function getTemplatesDir(): string;
100
+ /**
101
+ * The command already registered for the nativ MCP server in `.mcp.json`, when it has the shape setup writes
102
+ * (`<command> <args...> mcp`). Reusing it keeps hooks and permissions in step with what the project chose, so a
103
+ * plain `nativ setup` or `nativ doctor --fix` never silently swaps a deliberate `--command` for auto-detection.
104
+ */
105
+ export declare function configuredInvocation(root: string): CliInvocation | null;
106
+ /** Computes what `nativ setup` would do, without touching disk. */
107
+ export declare function planSetup(rootArg: string, options?: SetupOptions): SetupPlan;
108
+ export interface SetupResult {
109
+ root: string;
110
+ dryRun: boolean;
111
+ invocation: CliInvocation;
112
+ changes: AssetChange[];
113
+ warnings: string[];
114
+ }
115
+ /** Writes the planned differences. Idempotent: applying twice changes nothing the second time. */
116
+ export declare function applySetup(rootArg: string, options?: SetupOptions & {
117
+ dryRun?: boolean;
118
+ }): SetupResult;
119
+ /** A few lines of orientation for a new Claude Code session, or null when this is not a nativ project. */
120
+ export declare function buildSessionContext(cwd: string, env?: NodeJS.ProcessEnv): string | null;
121
+ /** `nativ hook context`: prints the SessionStart hook output. Never throws and always prints valid JSON or nothing. */
122
+ export declare function runSessionContext(targetDirArg?: string, write?: (text: string) => void): void;
123
+ export {};