@namewta/speculo 2.0.3 → 2.0.4

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": "@namewta/speculo",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Standard agent skills, commands and project methods with state-safe installation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -287,11 +287,11 @@ function repositoryRoot(path) {
287
287
  if (git(path, ['rev-parse', '--is-inside-work-tree'], { allowed: [0, 128] }).stdout.trim() !== 'true') {
288
288
  throw new SquashError('repository path is not a Git worktree');
289
289
  }
290
- return realpathSync(git(path, ['rev-parse', '--show-toplevel']).stdout.trim());
290
+ return realpathSync.native(git(path, ['rev-parse', '--show-toplevel']).stdout.trim());
291
291
  }
292
292
 
293
293
  function portable(root, path, externalIndex) {
294
- const rel = relative(realpathSync(root), realpathSync(path));
294
+ const rel = relative(realpathSync.native(root), realpathSync.native(path));
295
295
  if (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)) return rel ? rel.split(sep).join('/') : '.';
296
296
  return `<external-worktree:${externalIndex}>`;
297
297
  }
@@ -450,7 +450,7 @@ function inspectRemote(repo, request, oldHead) {
450
450
  function inspectRepository(projectRoot, request) {
451
451
  const repo = inside(projectRoot, request.path, `${request.id}.path`, { allowRoot: true });
452
452
  ensureDirectory(repo, `${request.id}.path`);
453
- if (repositoryRoot(repo) !== realpathSync(repo)) throw new SquashError(`${request.id}.path must be the repository toplevel`);
453
+ if (relative(repositoryRoot(repo), realpathSync.native(repo)) !== '') throw new SquashError(`${request.id}.path must be the repository toplevel`);
454
454
  validateRef(repo, request.branch, `${request.id}.branch`);
455
455
  git(repo, ['update-ref', '--stdin'], { input: 'start\nabort\n', env: { GIT_OPTIONAL_LOCKS: '0' } });
456
456
  git(repo, ['var', 'GIT_AUTHOR_IDENT'], { env: { GIT_OPTIONAL_LOCKS: '0' } });
@@ -765,7 +765,7 @@ function validateRunState(projectRoot, state) {
765
765
  safePath(repo.path, `${repo.id}.path`);
766
766
  const path = inside(projectRoot, repo.path, `${repo.id}.path`, { allowRoot: true, mustExist: true });
767
767
  ensureDirectory(path, `${repo.id}.path`);
768
- if (repositoryRoot(path) !== realpathSync(path)) throw new SquashError(`${repo.id}.path must remain a repository toplevel`);
768
+ if (relative(repositoryRoot(path), realpathSync.native(path)) !== '') throw new SquashError(`${repo.id}.path must remain a repository toplevel`);
769
769
  validateRef(path, repo.branch, `${repo.id}.branch`);
770
770
  validateOid(repo.start_sha, `${repo.id}.start_sha`);
771
771
  validateOid(repo.end_sha, `${repo.id}.end_sha`);
@@ -43,8 +43,10 @@ function workingFile(root, path) {
43
43
 
44
44
  function workingTree(options) {
45
45
  if (['head','commit','parent'].some(key => options[key] !== undefined)) throw new Error('review-scope: working-tree accepts base, not head/commit/parent');
46
- const root = realpathSync(resolve(options.repo));
47
- if (realpathSync(git(root, ['rev-parse','--show-toplevel']).trim()) !== root) throw new Error('review-scope: working-tree requires the actual project Git root');
46
+ // Native realpath expands Windows short names; relative also handles drive
47
+ // casing without admitting a nested directory that borrows its parent's Git.
48
+ const root = realpathSync.native(resolve(options.repo));
49
+ if (relative(root,realpathSync.native(git(root, ['rev-parse','--show-toplevel']).trim())) !== '') throw new Error('review-scope: working-tree requires the actual project Git root');
48
50
  const base = commit(root, options.base, 'base');
49
51
  const files = options.files?.map(portableFile);
50
52
  if (files && (!files.length || new Set(files).size !== files.length)) throw new Error('review-scope: empty or duplicate working-tree scope');
@@ -11,6 +11,7 @@ function fixture(t) {
11
11
  t.after(()=>rmSync(root,{recursive:true,force:true}));
12
12
  const git=(...args)=>execFileSync('git',['-C',root,...args],{encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim();
13
13
  git('init','-b','main');git('config','user.name','Fixture');git('config','user.email','test@example.invalid');
14
+ git('config','core.autocrlf','false');
14
15
  for(const path of ['one.txt','removed.txt','renamed.txt']) writeFileSync(join(root,path),'original\n');
15
16
  writeFileSync(join(root,'.gitignore'),'ignored/\n');git('add','.');git('-c','commit.gpgsign=false','commit','-m','base');
16
17
  const base=git('rev-parse','HEAD');
@@ -26,14 +27,14 @@ test('working-tree freezes staged, unstaged, added, deleted, renamed and mode ch
26
27
  const scope=f.scope();
27
28
  assert.equal(scope.outcome,'changes');assert.equal(scope.observed_head,f.base);assert.equal(scope.head_sha,undefined);
28
29
  assert.deepEqual(scope.changed_paths,['new-name.txt','new.bin','one.txt','removed.txt','renamed.txt']);
29
- assert.equal(scope.working_snapshot.find(x=>x.path==='one.txt').after.mode,'100755');
30
+ assert.equal(scope.working_snapshot.find(x=>x.path==='one.txt').after.mode,process.platform === 'win32' ? '100644' : '100755');
30
31
  assert.equal(scope.working_snapshot.find(x=>x.path==='removed.txt').after,null);
31
32
  assert.equal(scope.working_snapshot.find(x=>x.path==='new.bin').after.bytes,3);
32
33
  assert.deepEqual(f.scope(),scope);assert.equal(f.git('status','--porcelain'),status);assert.deepEqual(readFileSync(join(f.root,'.git/index')),before);
33
34
  writeFileSync(join(f.root,'new.bin'),Buffer.from([0,255,8]));assert.notEqual(f.scope().input_sha256,scope.input_sha256);
34
35
  });
35
36
  test('explicit scopes are literal, expose exclusions and retain deleted files',t=>{
36
- const f=fixture(t), path='literal [*] $(touch unsafe).txt';
37
+ const f=fixture(t), path='literal [abc] $(touch unsafe).txt';
37
38
  writeFileSync(join(f.root,path),'literal');rmSync(join(f.root,'removed.txt'));writeFileSync(join(f.root,'one.txt'),'changed');
38
39
  const scope=f.scope({files:[path,'removed.txt']});
39
40
  assert.deepEqual(scope.changed_paths,[path,'removed.txt']);assert.deepEqual(scope.excluded_paths,['one.txt']);
@@ -47,7 +48,7 @@ test('links are read as links and directory links never disclose outside bytes',
47
48
  t.after(()=>rmSync(outside,{recursive:true,force:true}));writeFileSync(join(outside,'secret'),'sensitive');
48
49
  symlinkSync(join(outside,'secret'),join(f.root,'link'));
49
50
  const scope=f.scope({files:['link']});assert.equal(scope.working_snapshot[0].after.kind,'symlink');
50
- symlinkSync(outside,join(f.root,'directory'));
51
+ symlinkSync(outside,join(f.root,'directory'),process.platform === 'win32' ? 'junction' : 'dir');
51
52
  assert.throws(()=>f.scope({files:['directory/secret']}),/symlink directory/);
52
53
  });
53
54
  test('unmerged index and borrowed parent repository are rejected',t=>{