diff --git a/.ai-devkit.json b/.ai-devkit.json index f10b5a05..51083048 100644 --- a/.ai-devkit.json +++ b/.ai-devkit.json @@ -9,7 +9,6 @@ "antigravity" ], "createdAt": "2025-12-28T13:35:45.251Z", - "updatedAt": "2026-06-14T11:53:00.073Z", "phases": [ "requirements", "design", diff --git a/e2e/cli.e2e.ts b/e2e/cli.e2e.ts index 56cb46e6..d93207fe 100644 --- a/e2e/cli.e2e.ts +++ b/e2e/cli.e2e.ts @@ -207,8 +207,7 @@ describe('memory commands', () => { memory: { path: '.ai-devkit/memory.db' }, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() + createdAt: new Date().toISOString() }); }); @@ -335,8 +334,7 @@ describe('install command', () => { version: '1.0.0', environments: ['claude'], phases: ['requirements', 'design'], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() + createdAt: new Date().toISOString() }); const result = run('install', { cwd: projectDir }); @@ -359,8 +357,7 @@ describe('install command', () => { skills: [ { registry: 'codeaholicguy/ai-devkit', name: 'dev-lifecycle' } ], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() + createdAt: new Date().toISOString() }); const result = run('install', { cwd: projectDir }); @@ -398,8 +395,7 @@ describe('skill command', () => { skills: [ { registry: 'codeaholicguy/ai-devkit', name: 'dev-lifecycle' } ], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() + createdAt: new Date().toISOString() }); // Create the skill directory so the remove command finds it @@ -427,8 +423,7 @@ describe('skill command', () => { { registry: 'codeaholicguy/ai-devkit', name: 'dev-lifecycle' }, { registry: 'codeaholicguy/ai-devkit', name: 'memory' } ], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() + createdAt: new Date().toISOString() }); const skillDir = join(projectDir, '.claude', 'skills', 'dev-lifecycle'); diff --git a/packages/cli/src/__tests__/lib/Config.test.ts b/packages/cli/src/__tests__/lib/Config.test.ts index dbb932a8..9abb6d46 100644 --- a/packages/cli/src/__tests__/lib/Config.test.ts +++ b/packages/cli/src/__tests__/lib/Config.test.ts @@ -94,7 +94,6 @@ describe('ConfigManager', () => { environments: ['cursor' as any], phases: ['requirements' as any], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -123,7 +122,6 @@ describe('ConfigManager', () => { environments: [], phases: [], createdAt: expect.any(String), - updatedAt: expect.any(String) }; (mockFs.writeJson as any).mockResolvedValue(undefined); @@ -136,17 +134,17 @@ describe('ConfigManager', () => { expectedConfig, { spaces: 2 } ); + expect(result).not.toHaveProperty('updatedAt'); }); }); describe('update', () => { - it('should update existing config and set updatedAt', async () => { + it('should update existing config without adding updatedAt', async () => { const existingConfig: DevKitConfig = { version: '1.0.0', environments: ['cursor'], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; const updates = { environments: ['cursor' as any, 'claude' as any] }; @@ -158,8 +156,47 @@ describe('ConfigManager', () => { const result = await configManager.update(updates); expect(result.environments).toEqual(['cursor', 'claude']); - expect(result.updatedAt).not.toBe(existingConfig.updatedAt); expect(result.createdAt).toBe(existingConfig.createdAt); + expect(result).not.toHaveProperty('updatedAt'); + }); + + it('should not rewrite config when updates are unchanged and updatedAt is absent', async () => { + const existingConfig: DevKitConfig = { + version: '1.0.0', + environments: ['cursor'], + phases: [], + skills: [{ registry: 'codeaholicguy/ai-devkit', name: 'debug' }], + createdAt: '2024-01-01T00:00:00.000Z', + }; + + (mockFs.pathExists as any).mockResolvedValue(true); + (mockFs.readJson as any).mockResolvedValue(existingConfig); + + const result = await configManager.update({ + environments: ['cursor'], + skills: [{ registry: 'codeaholicguy/ai-devkit', name: 'debug' }] + }); + + expect(result).toEqual(existingConfig); + expect(mockFs.writeJson).not.toHaveBeenCalled(); + }); + + it('should not rewrite config when only legacy updatedAt is present and updates are unchanged', async () => { + const existingConfig = { + version: '1.0.0', + environments: ['cursor'], + phases: [], + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z' + }; + + (mockFs.pathExists as any).mockResolvedValue(true); + (mockFs.readJson as any).mockResolvedValue(existingConfig); + + const result = await configManager.update({}); + + expect(result).toEqual(existingConfig); + expect(mockFs.writeJson).not.toHaveBeenCalled(); }); it('should throw error when config file not found', async () => { @@ -178,7 +215,6 @@ describe('ConfigManager', () => { environments: [], phases: ['requirements'], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -196,7 +232,6 @@ describe('ConfigManager', () => { environments: [], phases: ['requirements'], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -213,7 +248,6 @@ describe('ConfigManager', () => { version: '1.0.0', environments: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -233,7 +267,6 @@ describe('ConfigManager', () => { environments: [], phases: ['requirements', 'design'], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -250,7 +283,6 @@ describe('ConfigManager', () => { environments: [], phases: ['requirements'], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -274,7 +306,6 @@ describe('ConfigManager', () => { version: '1.0.0', environments: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -294,7 +325,6 @@ describe('ConfigManager', () => { environments: [], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -311,7 +341,6 @@ describe('ConfigManager', () => { environments: [], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -338,7 +367,6 @@ describe('ConfigManager', () => { environments: [], phases: ['requirements', 'deployment'], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -355,7 +383,6 @@ describe('ConfigManager', () => { environments: [], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -374,7 +401,6 @@ describe('ConfigManager', () => { environments: [], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -395,7 +421,6 @@ describe('ConfigManager', () => { environments: ['cursor', 'claude'], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -420,7 +445,6 @@ describe('ConfigManager', () => { environments: [], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -439,7 +463,6 @@ describe('ConfigManager', () => { environments: ['cursor'], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -460,7 +483,6 @@ describe('ConfigManager', () => { environments: ['cursor', 'claude'], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -477,7 +499,6 @@ describe('ConfigManager', () => { environments: ['cursor'], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -497,7 +518,6 @@ describe('ConfigManager', () => { phases: [], skills: [{ registry: 'codeaholicguy/ai-devkit', name: 'debug' }], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -523,7 +543,6 @@ describe('ConfigManager', () => { phases: [], skills: [{ registry: 'codeaholicguy/ai-devkit', name: 'debug' }], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -544,7 +563,6 @@ describe('ConfigManager', () => { environments: ['cursor'], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -574,7 +592,6 @@ describe('ConfigManager', () => { { registry: 'codeaholicguy/ai-devkit', name: 'memory' } ], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -598,7 +615,6 @@ describe('ConfigManager', () => { { registry: 'codeaholicguy/ai-devkit', name: 'dev-lifecycle' } ], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -620,7 +636,6 @@ describe('ConfigManager', () => { { registry: 'codeaholicguy/ai-devkit', name: 'memory' } ], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }; (mockFs.pathExists as any).mockResolvedValue(true); @@ -655,7 +670,6 @@ describe('ConfigManager', () => { 'invalid/value': false }, createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }); const registries = await configManager.getSkillRegistries(); @@ -673,7 +687,6 @@ describe('ConfigManager', () => { phases: [], skills: [{ registry: 'codeaholicguy/ai-devkit', name: 'debug' }], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }); const registries = await configManager.getSkillRegistries(); @@ -698,7 +711,6 @@ describe('ConfigManager', () => { environments: [], phases: [], createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }); const result = await configManager.getMemoryDbPath(); @@ -714,7 +726,6 @@ describe('ConfigManager', () => { phases: [], memory: { path: ' ' }, createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }); await expect(configManager.getMemoryDbPath()).resolves.toBeUndefined(); @@ -725,7 +736,6 @@ describe('ConfigManager', () => { phases: [], memory: { path: 42 }, createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }); await expect(configManager.getMemoryDbPath()).resolves.toBeUndefined(); @@ -739,7 +749,6 @@ describe('ConfigManager', () => { phases: [], memory: { path: '/custom/memory.db' }, createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }); const result = await configManager.getMemoryDbPath(); @@ -756,7 +765,6 @@ describe('ConfigManager', () => { phases: [], memory: { path: '.ai-devkit/project-memory.db' }, createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z' }); const result = await configManager.getMemoryDbPath(); diff --git a/packages/cli/src/lib/Config.ts b/packages/cli/src/lib/Config.ts index 75ec43a2..9e41b093 100644 --- a/packages/cli/src/lib/Config.ts +++ b/packages/cli/src/lib/Config.ts @@ -34,8 +34,7 @@ export class ConfigManager { version: packageJson.version, environments: [], phases: [], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() + createdAt: new Date().toISOString() }; await fs.writeJson(this.configPath, config, { spaces: 2 }); @@ -50,10 +49,13 @@ export class ConfigManager { const updated = { ...config, - ...updates, - updatedAt: new Date().toISOString() + ...updates }; + if (JSON.stringify(updated) === JSON.stringify(config)) { + return config; + } + await fs.writeJson(this.configPath, updated, { spaces: 2 }); return updated; } @@ -64,7 +66,7 @@ export class ConfigManager { throw new ConfigNotFoundError('Config file not found. Run ai-devkit init first.'); } - const phases = Array.isArray(config.phases) ? config.phases : []; + const phases = Array.isArray(config.phases) ? [...config.phases] : []; if (!phases.includes(phase)) { phases.push(phase); return this.update({ phases }); @@ -141,7 +143,7 @@ export class ConfigManager { throw new ConfigNotFoundError('Config file not found. Run ai-devkit init first.'); } - const installed = Array.isArray(config.skills) ? config.skills : []; + const installed = Array.isArray(config.skills) ? [...config.skills] : []; const exists = installed.some( entry => entry.registry === skill.registry && entry.name === skill.name diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 915e26af..05ab7482 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -34,7 +34,6 @@ export interface DevKitConfig { skills?: ConfigSkill[]; mcpServers?: Record; createdAt: string; - updatedAt: string; } export interface ConfigSkill { diff --git a/web/content/docs/11-configuration-file.md b/web/content/docs/11-configuration-file.md index 3a506fbb..46ffbc14 100644 --- a/web/content/docs/11-configuration-file.md +++ b/web/content/docs/11-configuration-file.md @@ -46,8 +46,7 @@ Use this page as a reference for fields inside `.ai-devkit.json`. In most cases, "args": ["-y", "@ai-devkit/memory"] } }, - "createdAt": "2025-12-28T13:35:45.251Z", - "updatedAt": "2026-04-18T10:00:00.000Z" + "createdAt": "2025-12-28T13:35:45.251Z" } ``` @@ -230,11 +229,11 @@ Every server definition requires a `transport` field set to `stdio`, `http`, or **Set by:** `ai-devkit init --template` or by editing `.ai-devkit.json` directly **Read by:** `ai-devkit install` -#### `createdAt` / `updatedAt` +#### `createdAt` - **Type:** `string` (ISO 8601 timestamp) -- **Set automatically** when the config is created or modified. -- You normally should not edit these fields manually. +- **Set automatically** when the config is created. +- You normally should not edit this field manually. ## Global Config (`~/.ai-devkit/.ai-devkit.json`)