Skip to content

Commit 9b878ae

Browse files
committed
fix: protect nit private state
1 parent 2cd83bc commit 9b878ae

7 files changed

Lines changed: 181 additions & 31 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
node_modules/
22
dist/
3+
.nit/
34
.DS_Store
45
*.log
56
CLAUDE.md

SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name: nit
33
description: Git for agent identity — one identity, any apps
44
metadata:
5-
version: 0.6.32
5+
version: 0.6.33
66
---
77

88
# nit — Git for Agent Identity

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@newtype-ai/nit",
3-
"version": "0.6.32",
3+
"version": "0.6.33",
44
"description": "Version control for agent cards",
55
"type": "module",
66
"bin": {

src/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ export type { MachineIdRuntime } from './fingerprint.js';
175175

176176
const NIT_DIR = '.nit';
177177
const CARD_FILE = 'agent-card.json';
178+
const GITIGNORE_FILE = '.gitignore';
179+
const NIT_GITIGNORE_ENTRY = '.nit/';
178180
const DEFAULT_API_BASE = 'https://api.newtype-ai.org';
179181
const CURRENT_PROTOCOL_VERSION = '0.3.0';
180182
const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
@@ -301,6 +303,31 @@ async function writeWorkingCard(
301303
await fs.writeFile(cardPath, JSON.stringify(card, null, 2) + '\n', 'utf-8');
302304
}
303305

306+
function gitignoreCoversNit(raw: string): boolean {
307+
return raw.split(/\r?\n/).some((line) => {
308+
const trimmed = line.trim();
309+
if (!trimmed || trimmed.startsWith('#')) return false;
310+
return trimmed === '.nit' || trimmed === '.nit/' || trimmed === '/.nit' || trimmed === '/.nit/';
311+
});
312+
}
313+
314+
async function ensureNitGitignored(projDir: string): Promise<void> {
315+
const gitignorePath = join(projDir, GITIGNORE_FILE);
316+
let raw = '';
317+
try {
318+
raw = await fs.readFile(gitignorePath, 'utf-8');
319+
} catch (err) {
320+
const code = (err as NodeJS.ErrnoException).code;
321+
if (code !== 'ENOENT') throw err;
322+
await fs.writeFile(gitignorePath, `${NIT_GITIGNORE_ENTRY}\n`, 'utf-8');
323+
return;
324+
}
325+
326+
if (gitignoreCoversNit(raw)) return;
327+
const separator = raw.length === 0 || raw.endsWith('\n') ? '' : '\n';
328+
await fs.writeFile(gitignorePath, `${raw}${separator}${NIT_GITIGNORE_ENTRY}\n`, 'utf-8');
329+
}
330+
304331
/**
305332
* Get the card stored at a specific commit.
306333
*/
@@ -431,6 +458,8 @@ export async function init(options?: {
431458
discoveredSkills = await discoverProjectSkills(projDir);
432459
}
433460

461+
await ensureNitGitignored(projDir);
462+
434463
// Create directory structure
435464
await fs.mkdir(join(nitDir, 'objects'), { recursive: true });
436465
await fs.mkdir(join(nitDir, 'refs', 'heads'), { recursive: true });

tests/local-regression.test.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ test('init fails without overwriting malformed existing agent-card.json', () =>
394394
assert.match(result.stderr, /Invalid agent-card\.json/);
395395
assert.equal(readFileSync(cardPath, 'utf8'), '{bad json');
396396
assert.equal(existsSync(join(cwd, '.nit')), false);
397+
assert.equal(existsSync(join(cwd, '.gitignore')), false);
397398
});
398399

399400
test('init fails before creating .nit when existing card lacks required fields', () => {
@@ -405,6 +406,7 @@ test('init fails before creating .nit when existing card lacks required fields',
405406
assert.notEqual(result.status, 0);
406407
assert.match(result.stderr, /missing "name"/);
407408
assert.equal(existsSync(join(cwd, '.nit')), false);
409+
assert.equal(existsSync(join(cwd, '.gitignore')), false);
408410
});
409411

410412
test('init rejects malformed skill entries before creating .nit', () => {
@@ -420,6 +422,37 @@ test('init rejects malformed skill entries before creating .nit', () => {
420422
assert.notEqual(result.status, 0);
421423
assert.match(result.stderr, /skills\[0\] must be a JSON object/);
422424
assert.equal(existsSync(join(cwd, '.nit')), false);
425+
assert.equal(existsSync(join(cwd, '.gitignore')), false);
426+
});
427+
428+
test('init creates .gitignore entry for private nit state', () => {
429+
const cwd = workspace('nit-init-gitignore-');
430+
initWorkspace(cwd);
431+
432+
const gitignore = readFileSync(join(cwd, '.gitignore'), 'utf8');
433+
assert.match(gitignore, /^\.nit\/$/m);
434+
});
435+
436+
test('init appends .nit to existing .gitignore exactly once', () => {
437+
const cwd = workspace('nit-init-gitignore-existing-');
438+
writeFileSync(join(cwd, '.gitignore'), 'node_modules/\n.DS_Store', 'utf8');
439+
440+
initWorkspace(cwd);
441+
442+
const gitignore = readFileSync(join(cwd, '.gitignore'), 'utf8');
443+
assert.match(gitignore, /^node_modules\//);
444+
assert.match(gitignore, /\.DS_Store\n\.nit\/\n$/);
445+
assert.equal((gitignore.match(/^\.nit\/$/gm) ?? []).length, 1);
446+
});
447+
448+
test('init respects existing .nit ignore variants without duplicating', () => {
449+
const cwd = workspace('nit-init-gitignore-existing-nit-');
450+
writeFileSync(join(cwd, '.gitignore'), '# local\n/.nit/\n', 'utf8');
451+
452+
initWorkspace(cwd);
453+
454+
const gitignore = readFileSync(join(cwd, '.gitignore'), 'utf8');
455+
assert.equal(gitignore, '# local\n/.nit/\n');
423456
});
424457

425458
test('checkout fails closed when working card is malformed', () => {

0 commit comments

Comments
 (0)