sneakoscope 0.7.74 → 0.7.78

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.
@@ -1,22 +1,38 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
- import { ensureDir, exists, nowIso, readJson, runProcess, writeJsonAtomic, writeTextAtomic } from './fsx.mjs';
3
+ import { exists, nowIso, readJson, runProcess, writeJsonAtomic, writeTextAtomic } from './fsx.mjs';
4
4
 
5
5
  const VERSION_HOOK_MARKER = 'Sneakoscope Codex Version Guard';
6
6
  const VERSION_STATE_FILE = 'sks-version-state.json';
7
7
  const DEFAULT_BUMP = 'patch';
8
8
 
9
9
  export async function installVersionGitHook(root, commandPrefix = 'sks') {
10
+ void root;
11
+ void commandPrefix;
12
+ return {
13
+ ok: false,
14
+ installed: false,
15
+ reason: 'pre_commit_hooks_unsupported',
16
+ message: 'SKS no longer installs Git pre-commit hooks. Use `sks versioning bump` and release checks explicitly.'
17
+ };
18
+ }
19
+
20
+ export async function disableVersionGitHook(root) {
21
+ await setVersionPolicyEnabled(root, false);
10
22
  const git = await gitPaths(root);
11
- if (!git.ok) return { ok: true, installed: false, reason: git.reason || 'not_git' };
12
- const hookPath = git.hook_path;
13
- const block = versionHookBlock(commandPrefix);
14
- const current = await readFileMaybe(hookPath);
15
- const next = mergeShellBlock(current, VERSION_HOOK_MARKER, block);
16
- await ensureDir(path.dirname(hookPath));
17
- await writeTextAtomic(hookPath, next);
18
- await fsp.chmod(hookPath, 0o755).catch(() => {});
19
- return { ok: true, installed: true, hook_path: hookPath };
23
+ if (!git.ok) return { ok: true, disabled: true, hook_removed: false, reason: git.reason || 'not_git' };
24
+ const current = await readFileMaybe(git.hook_path);
25
+ if (!current.includes(`BEGIN ${VERSION_HOOK_MARKER}`)) {
26
+ return { ok: true, disabled: true, hook_removed: false, hook_path: git.hook_path, reason: 'managed_hook_not_installed' };
27
+ }
28
+ const next = removeShellBlock(current, VERSION_HOOK_MARKER);
29
+ if (next.trim() === '#!/bin/sh' || next.trim() === '#!/usr/bin/env sh' || !next.trim()) {
30
+ await fsp.rm(git.hook_path, { force: true });
31
+ return { ok: true, disabled: true, hook_removed: true, hook_path: git.hook_path };
32
+ }
33
+ await writeTextAtomic(git.hook_path, next);
34
+ await fsp.chmod(git.hook_path, 0o755).catch(() => {});
35
+ return { ok: true, disabled: true, hook_removed: true, hook_path: git.hook_path };
20
36
  }
21
37
 
22
38
  export async function versioningStatus(root) {
@@ -40,7 +56,7 @@ export async function versioningStatus(root) {
40
56
  state_path: path.join(git.common_dir, VERSION_STATE_FILE),
41
57
  last_version: state.last_version || null,
42
58
  runtime_drift: runtimeDrift,
43
- reason: version ? null : 'package_json_version_missing'
59
+ reason: !policy.enabled ? 'disabled_by_policy' : (version ? null : 'package_json_version_missing')
44
60
  };
45
61
  }
46
62
 
@@ -190,11 +206,38 @@ export async function verifyProjectVersion(root, opts = {}) {
190
206
  async function versionPolicy(root) {
191
207
  const policy = await readJson(path.join(root, '.sneakoscope', 'policy.json'), {});
192
208
  return {
193
- enabled: policy.versioning?.enabled !== false,
209
+ enabled: policy.versioning?.enabled === true,
194
210
  bump: policy.versioning?.bump || DEFAULT_BUMP
195
211
  };
196
212
  }
197
213
 
214
+ async function setVersionPolicyEnabled(root, enabled) {
215
+ const policyPath = path.join(root, '.sneakoscope', 'policy.json');
216
+ const policy = await readJson(policyPath, {});
217
+ await writeJsonAtomic(policyPath, {
218
+ ...policy,
219
+ git: {
220
+ ...(policy.git || {}),
221
+ versioning: {
222
+ ...(policy.git?.versioning || {}),
223
+ enabled: Boolean(enabled),
224
+ bump: policy.git?.versioning?.bump || policy.versioning?.bump || DEFAULT_BUMP,
225
+ lock: 'git-common-dir/sks-version.lock',
226
+ state: 'git-common-dir/sks-version-state.json'
227
+ }
228
+ },
229
+ versioning: {
230
+ ...(policy.versioning || {}),
231
+ enabled: Boolean(enabled),
232
+ bump: policy.versioning?.bump || DEFAULT_BUMP,
233
+ trigger: 'manual',
234
+ lock_scope: 'git-common-dir',
235
+ managed_files: policy.versioning?.managed_files || ['package.json', 'package-lock.json', 'npm-shrinkwrap.json'],
236
+ collision_policy: policy.versioning?.collision_policy || 'explicit_bump_only'
237
+ }
238
+ });
239
+ }
240
+
198
241
  async function gitPaths(root) {
199
242
  const top = await git(root, ['rev-parse', '--show-toplevel']);
200
243
  if (top.code !== 0) return { ok: false, reason: 'not_git' };
@@ -287,7 +330,7 @@ async function syncChangelogVersionSection(root, version) {
287
330
  text = text.replace(/^#\s+Changelog\s*$/m, (title) => `${title}\n\n## [Unreleased]`);
288
331
  }
289
332
 
290
- const managedSection = `\n## [${version}] - ${date}\n\n### Fixed\n\n- Keep release metadata aligned after the automatic SKS version guard advances the package version.\n`;
333
+ const managedSection = `\n## [${version}] - ${date}\n\n### Fixed\n\n- Keep release metadata aligned after an explicit SKS version bump advances the package version.\n`;
291
334
  const next = text.replace(/^##\s+\[Unreleased\]\s*$/m, (heading) => `${heading}\n${managedSection}`);
292
335
  if (next === text) return { files: [], relative_files: [] };
293
336
  await writeTextAtomic(file, next);
@@ -341,26 +384,13 @@ function bumpSemver(v, bump = DEFAULT_BUMP) {
341
384
  return { major: v.major, minor: v.minor, patch: v.patch + 1 };
342
385
  }
343
386
 
344
- function versionHookBlock(commandPrefix) {
345
- return `# SKS keeps package versions unique across worker commits.\n${commandPrefix} versioning pre-commit\nstatus=$?\nif [ $status -ne 0 ]; then\n echo \"SKS versioning blocked commit. Run: sks versioning status\" >&2\n exit $status\nfi`;
346
- }
347
-
348
- function mergeShellBlock(current, marker, block) {
387
+ function removeShellBlock(current, marker) {
349
388
  const begin = `# BEGIN ${marker}`;
350
389
  const end = `# END ${marker}`;
351
- const managed = `${begin}\n${block.trim()}\n${end}\n`;
352
- if (!current.trim()) return `#!/bin/sh\n${managed}`;
353
- const withShebang = current.startsWith('#!') ? current : `#!/bin/sh\n${current}`;
354
- const beginIdx = withShebang.indexOf(begin);
355
- const endIdx = withShebang.indexOf(end);
356
- if (beginIdx >= 0 && endIdx >= beginIdx) {
357
- return `${withShebang.slice(0, beginIdx)}${managed}${withShebang.slice(endIdx + end.length).replace(/^\n/, '')}`;
358
- }
359
- const lines = withShebang.split('\n');
360
- if (lines[0]?.startsWith('#!')) {
361
- return `${lines[0]}\n${managed}${lines.slice(1).join('\n').replace(/^\n/, '')}`.replace(/\s*$/, '\n');
362
- }
363
- return `${managed}${withShebang.replace(/^\n/, '').replace(/\s*$/, '\n')}`;
390
+ const beginIdx = current.indexOf(begin);
391
+ const endIdx = current.indexOf(end);
392
+ if (beginIdx < 0 || endIdx < beginIdx) return current;
393
+ return `${current.slice(0, beginIdx)}${current.slice(endIdx + end.length).replace(/^\n/, '')}`.replace(/\s*$/, '\n');
364
394
  }
365
395
 
366
396
  async function readFileMaybe(file) {