antigravity-ai-kit 2.1.0 → 3.0.1
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/.agent/README.md +4 -4
- package/.agent/agents/README.md +16 -12
- package/.agent/agents/architect.md +1 -0
- package/.agent/agents/backend-specialist.md +11 -0
- package/.agent/agents/code-reviewer.md +1 -0
- package/.agent/agents/database-architect.md +11 -0
- package/.agent/agents/devops-engineer.md +11 -0
- package/.agent/agents/e2e-runner.md +1 -0
- package/.agent/agents/explorer-agent.md +11 -0
- package/.agent/agents/frontend-specialist.md +11 -0
- package/.agent/agents/mobile-developer.md +11 -0
- package/.agent/agents/performance-optimizer.md +11 -0
- package/.agent/agents/planner.md +1 -0
- package/.agent/agents/refactor-cleaner.md +1 -0
- package/.agent/agents/reliability-engineer.md +11 -0
- package/.agent/agents/security-reviewer.md +1 -0
- package/.agent/agents/sprint-orchestrator.md +10 -0
- package/.agent/agents/tdd-guide.md +1 -0
- package/.agent/commands/code-review.md +1 -0
- package/.agent/commands/debug.md +1 -0
- package/.agent/commands/deploy.md +1 -0
- package/.agent/commands/help.md +252 -31
- package/.agent/commands/plan.md +1 -0
- package/.agent/commands/status.md +1 -0
- package/.agent/commands/tdd.md +1 -0
- package/.agent/contexts/brainstorm.md +26 -0
- package/.agent/contexts/debug.md +28 -0
- package/.agent/contexts/implement.md +29 -0
- package/.agent/contexts/review.md +27 -0
- package/.agent/contexts/ship.md +28 -0
- package/.agent/engine/identity.json +13 -0
- package/.agent/engine/loading-rules.json +23 -1
- package/.agent/engine/marketplace-index.json +29 -0
- package/.agent/engine/reliability-config.json +14 -0
- package/.agent/engine/sdlc-map.json +44 -0
- package/.agent/engine/workflow-state.json +28 -2
- package/.agent/hooks/hooks.json +27 -25
- package/.agent/manifest.json +12 -4
- package/.agent/rules.md +2 -1
- package/.agent/skills/README.md +10 -5
- package/.agent/skills/i18n-localization/SKILL.md +191 -0
- package/.agent/skills/mcp-integration/SKILL.md +224 -0
- package/.agent/skills/parallel-agents/SKILL.md +1 -1
- package/.agent/skills/shell-conventions/SKILL.md +92 -0
- package/.agent/skills/ui-ux-pro-max/SKILL.md +557 -0
- package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.agent/templates/adr-template.md +32 -0
- package/.agent/templates/bug-report.md +37 -0
- package/.agent/templates/feature-request.md +32 -0
- package/.agent/workflows/README.md +92 -78
- package/.agent/workflows/brainstorm.md +154 -100
- package/.agent/workflows/create.md +142 -75
- package/.agent/workflows/debug.md +157 -98
- package/.agent/workflows/deploy.md +195 -144
- package/.agent/workflows/enhance.md +157 -65
- package/.agent/workflows/orchestrate.md +171 -114
- package/.agent/workflows/plan.md +147 -72
- package/.agent/workflows/preview.md +140 -83
- package/.agent/workflows/quality-gate.md +196 -0
- package/.agent/workflows/retrospective.md +197 -0
- package/.agent/workflows/review.md +188 -0
- package/.agent/workflows/status.md +142 -91
- package/.agent/workflows/test.md +168 -95
- package/.agent/workflows/ui-ux-pro-max.md +181 -127
- package/README.md +215 -78
- package/bin/ag-kit.js +344 -10
- package/lib/agent-registry.js +214 -0
- package/lib/agent-reputation.js +351 -0
- package/lib/cli-commands.js +235 -0
- package/lib/conflict-detector.js +245 -0
- package/lib/engineering-manager.js +354 -0
- package/lib/error-budget.js +294 -0
- package/lib/hook-system.js +252 -0
- package/lib/identity.js +245 -0
- package/lib/loading-engine.js +208 -0
- package/lib/marketplace.js +298 -0
- package/lib/plugin-system.js +604 -0
- package/lib/security-scanner.js +309 -0
- package/lib/self-healing.js +434 -0
- package/lib/session-manager.js +261 -0
- package/lib/skill-sandbox.js +244 -0
- package/lib/task-governance.js +523 -0
- package/lib/task-model.js +317 -0
- package/lib/updater.js +201 -0
- package/lib/verify.js +240 -0
- package/lib/workflow-engine.js +353 -0
- package/lib/workflow-persistence.js +160 -0
- package/package.json +7 -3
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity AI Kit — Task Data Model
|
|
3
|
+
*
|
|
4
|
+
* JSON file-backed task CRUD with status FSM tracking.
|
|
5
|
+
* Provides the data layer for task governance.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/task-model
|
|
8
|
+
* @author Emre Dursun
|
|
9
|
+
* @since v3.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
const AGENT_DIR = '.agent';
|
|
19
|
+
const ENGINE_DIR = 'engine';
|
|
20
|
+
const TASKS_FILE = 'tasks.json';
|
|
21
|
+
|
|
22
|
+
/** Valid task status values */
|
|
23
|
+
const VALID_STATUSES = ['open', 'in-progress', 'review', 'done', 'blocked'];
|
|
24
|
+
|
|
25
|
+
/** Valid status transitions */
|
|
26
|
+
const STATUS_TRANSITIONS = {
|
|
27
|
+
'open': ['in-progress', 'blocked'],
|
|
28
|
+
'in-progress': ['review', 'blocked', 'open'],
|
|
29
|
+
'review': ['done', 'in-progress'],
|
|
30
|
+
'done': ['open'],
|
|
31
|
+
'blocked': ['open', 'in-progress'],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Valid priority levels */
|
|
35
|
+
const VALID_PRIORITIES = ['critical', 'high', 'medium', 'low'];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {object} Task
|
|
39
|
+
* @property {string} id - Unique task ID
|
|
40
|
+
* @property {string} title - Task title
|
|
41
|
+
* @property {string | null} description - Optional description
|
|
42
|
+
* @property {string} status - Current status
|
|
43
|
+
* @property {string} priority - Priority level
|
|
44
|
+
* @property {string | null} assignee - Assigned agent or person
|
|
45
|
+
* @property {string} createdAt - ISO timestamp
|
|
46
|
+
* @property {string} updatedAt - ISO timestamp
|
|
47
|
+
* @property {string | null} completedAt - Completion timestamp
|
|
48
|
+
* @property {boolean} deleted - Soft delete flag
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolves the tasks file path.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} projectRoot - Root directory of the project
|
|
55
|
+
* @returns {string} Absolute path to tasks.json
|
|
56
|
+
*/
|
|
57
|
+
function resolveTasksPath(projectRoot) {
|
|
58
|
+
return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, TASKS_FILE);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Loads all tasks from disk.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} projectRoot - Root directory of the project
|
|
65
|
+
* @returns {Task[]}
|
|
66
|
+
*/
|
|
67
|
+
function loadTasks(projectRoot) {
|
|
68
|
+
const tasksPath = resolveTasksPath(projectRoot);
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(tasksPath)) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const data = JSON.parse(fs.readFileSync(tasksPath, 'utf-8'));
|
|
76
|
+
return data.tasks || [];
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Writes tasks to disk atomically.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} projectRoot - Root directory of the project
|
|
86
|
+
* @param {Task[]} tasks - Tasks array
|
|
87
|
+
* @returns {void}
|
|
88
|
+
*/
|
|
89
|
+
function writeTasks(projectRoot, tasks) {
|
|
90
|
+
const tasksPath = resolveTasksPath(projectRoot);
|
|
91
|
+
const tempPath = `${tasksPath}.tmp`;
|
|
92
|
+
const dir = path.dirname(tasksPath);
|
|
93
|
+
|
|
94
|
+
if (!fs.existsSync(dir)) {
|
|
95
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const data = {
|
|
99
|
+
schemaVersion: '1.0.0',
|
|
100
|
+
lastUpdated: new Date().toISOString(),
|
|
101
|
+
tasks,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
105
|
+
fs.renameSync(tempPath, tasksPath);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a new task.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} projectRoot - Root directory of the project
|
|
112
|
+
* @param {object} params - Task parameters
|
|
113
|
+
* @param {string} params.title - Task title
|
|
114
|
+
* @param {string} [params.description] - Optional description
|
|
115
|
+
* @param {string} [params.assignee] - Optional assignee
|
|
116
|
+
* @param {string} [params.priority] - Priority (default: 'medium')
|
|
117
|
+
* @returns {Task}
|
|
118
|
+
*/
|
|
119
|
+
function createTask(projectRoot, { title, description, assignee, priority }) {
|
|
120
|
+
const tasks = loadTasks(projectRoot);
|
|
121
|
+
const now = new Date().toISOString();
|
|
122
|
+
|
|
123
|
+
const taskPriority = priority && VALID_PRIORITIES.includes(priority) ? priority : 'medium';
|
|
124
|
+
|
|
125
|
+
/** @type {Task} */
|
|
126
|
+
const task = {
|
|
127
|
+
id: `TSK-${crypto.randomUUID().slice(0, 8).toUpperCase()}`,
|
|
128
|
+
title,
|
|
129
|
+
description: description || null,
|
|
130
|
+
status: 'open',
|
|
131
|
+
priority: taskPriority,
|
|
132
|
+
assignee: assignee || null,
|
|
133
|
+
createdAt: now,
|
|
134
|
+
updatedAt: now,
|
|
135
|
+
completedAt: null,
|
|
136
|
+
deleted: false,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
tasks.push(task);
|
|
140
|
+
writeTasks(projectRoot, tasks);
|
|
141
|
+
|
|
142
|
+
return task;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Retrieves a task by ID.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} projectRoot - Root directory of the project
|
|
149
|
+
* @param {string} taskId - Task ID
|
|
150
|
+
* @returns {Task | null}
|
|
151
|
+
*/
|
|
152
|
+
function getTask(projectRoot, taskId) {
|
|
153
|
+
const tasks = loadTasks(projectRoot);
|
|
154
|
+
return tasks.find((t) => t.id === taskId && !t.deleted) || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Lists tasks with optional filters.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} projectRoot - Root directory of the project
|
|
161
|
+
* @param {object} [filters] - Filter options
|
|
162
|
+
* @param {string} [filters.status] - Filter by status
|
|
163
|
+
* @param {string} [filters.priority] - Filter by priority
|
|
164
|
+
* @param {string} [filters.assignee] - Filter by assignee
|
|
165
|
+
* @returns {Task[]}
|
|
166
|
+
*/
|
|
167
|
+
function listTasks(projectRoot, filters = {}) {
|
|
168
|
+
let tasks = loadTasks(projectRoot).filter((t) => !t.deleted);
|
|
169
|
+
|
|
170
|
+
if (filters.status) {
|
|
171
|
+
tasks = tasks.filter((t) => t.status === filters.status);
|
|
172
|
+
}
|
|
173
|
+
if (filters.priority) {
|
|
174
|
+
tasks = tasks.filter((t) => t.priority === filters.priority);
|
|
175
|
+
}
|
|
176
|
+
if (filters.assignee) {
|
|
177
|
+
tasks = tasks.filter((t) => t.assignee === filters.assignee);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return tasks;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Updates a task's fields.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} projectRoot - Root directory of the project
|
|
187
|
+
* @param {string} taskId - Task ID
|
|
188
|
+
* @param {object} updates - Fields to update
|
|
189
|
+
* @returns {{ success: boolean, task: Task | null }}
|
|
190
|
+
*/
|
|
191
|
+
function updateTask(projectRoot, taskId, updates) {
|
|
192
|
+
const tasks = loadTasks(projectRoot);
|
|
193
|
+
const taskIndex = tasks.findIndex((t) => t.id === taskId && !t.deleted);
|
|
194
|
+
|
|
195
|
+
if (taskIndex === -1) {
|
|
196
|
+
return { success: false, task: null };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const allowedFields = ['title', 'description', 'assignee', 'priority'];
|
|
200
|
+
for (const field of allowedFields) {
|
|
201
|
+
if (updates[field] !== undefined) {
|
|
202
|
+
tasks[taskIndex][field] = updates[field];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
tasks[taskIndex].updatedAt = new Date().toISOString();
|
|
207
|
+
writeTasks(projectRoot, tasks);
|
|
208
|
+
|
|
209
|
+
return { success: true, task: tasks[taskIndex] };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Transitions a task to a new status following the FSM.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} projectRoot - Root directory of the project
|
|
216
|
+
* @param {string} taskId - Task ID
|
|
217
|
+
* @param {string} newStatus - Target status
|
|
218
|
+
* @returns {{ success: boolean, error?: string }}
|
|
219
|
+
*/
|
|
220
|
+
function transitionTask(projectRoot, taskId, newStatus) {
|
|
221
|
+
if (!VALID_STATUSES.includes(newStatus)) {
|
|
222
|
+
return { success: false, error: `Invalid status: ${newStatus}. Valid: ${VALID_STATUSES.join(', ')}` };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const tasks = loadTasks(projectRoot);
|
|
226
|
+
const taskIndex = tasks.findIndex((t) => t.id === taskId && !t.deleted);
|
|
227
|
+
|
|
228
|
+
if (taskIndex === -1) {
|
|
229
|
+
return { success: false, error: `Task not found: ${taskId}` };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const currentStatus = tasks[taskIndex].status;
|
|
233
|
+
const allowed = STATUS_TRANSITIONS[currentStatus] || [];
|
|
234
|
+
|
|
235
|
+
if (!allowed.includes(newStatus)) {
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
error: `Invalid transition: ${currentStatus} → ${newStatus}. Allowed: [${allowed.join(', ')}]`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
tasks[taskIndex].status = newStatus;
|
|
243
|
+
tasks[taskIndex].updatedAt = new Date().toISOString();
|
|
244
|
+
|
|
245
|
+
if (newStatus === 'done') {
|
|
246
|
+
tasks[taskIndex].completedAt = new Date().toISOString();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
writeTasks(projectRoot, tasks);
|
|
250
|
+
|
|
251
|
+
return { success: true };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Soft-deletes a task.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} projectRoot - Root directory of the project
|
|
258
|
+
* @param {string} taskId - Task ID
|
|
259
|
+
* @returns {{ success: boolean }}
|
|
260
|
+
*/
|
|
261
|
+
function deleteTask(projectRoot, taskId) {
|
|
262
|
+
const tasks = loadTasks(projectRoot);
|
|
263
|
+
const taskIndex = tasks.findIndex((t) => t.id === taskId);
|
|
264
|
+
|
|
265
|
+
if (taskIndex === -1) {
|
|
266
|
+
return { success: false };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
tasks[taskIndex].deleted = true;
|
|
270
|
+
tasks[taskIndex].updatedAt = new Date().toISOString();
|
|
271
|
+
writeTasks(projectRoot, tasks);
|
|
272
|
+
|
|
273
|
+
return { success: true };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Returns task metrics: counts by status, avg cycle time.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} projectRoot - Root directory of the project
|
|
280
|
+
* @returns {object} Task metrics
|
|
281
|
+
*/
|
|
282
|
+
function getTaskMetrics(projectRoot) {
|
|
283
|
+
const tasks = loadTasks(projectRoot).filter((t) => !t.deleted);
|
|
284
|
+
|
|
285
|
+
const counts = {};
|
|
286
|
+
for (const status of VALID_STATUSES) {
|
|
287
|
+
counts[status] = tasks.filter((t) => t.status === status).length;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Calculate average cycle time for completed tasks
|
|
291
|
+
const completedTasks = tasks.filter((t) => t.status === 'done' && t.completedAt);
|
|
292
|
+
let avgCycleTimeSeconds = 0;
|
|
293
|
+
|
|
294
|
+
if (completedTasks.length > 0) {
|
|
295
|
+
const totalCycleTime = completedTasks.reduce((sum, t) => {
|
|
296
|
+
return sum + (new Date(t.completedAt).getTime() - new Date(t.createdAt).getTime());
|
|
297
|
+
}, 0);
|
|
298
|
+
avgCycleTimeSeconds = Math.round(totalCycleTime / completedTasks.length / 1000);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
total: tasks.length,
|
|
303
|
+
counts,
|
|
304
|
+
avgCycleTimeSeconds,
|
|
305
|
+
completionRate: tasks.length > 0 ? Math.round((counts.done / tasks.length) * 100) : 0,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = {
|
|
310
|
+
createTask,
|
|
311
|
+
getTask,
|
|
312
|
+
listTasks,
|
|
313
|
+
updateTask,
|
|
314
|
+
transitionTask,
|
|
315
|
+
deleteTask,
|
|
316
|
+
getTaskMetrics,
|
|
317
|
+
};
|
package/lib/updater.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity AI Kit — CLI Updater
|
|
3
|
+
*
|
|
4
|
+
* Non-destructive update mechanism that merges new framework
|
|
5
|
+
* files into an existing .agent/ installation while preserving
|
|
6
|
+
* user customizations.
|
|
7
|
+
*
|
|
8
|
+
* @module lib/updater
|
|
9
|
+
* @author Emre Dursun
|
|
10
|
+
* @since v3.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
const AGENT_DIR = '.agent';
|
|
20
|
+
|
|
21
|
+
/** Files that should never be overwritten during updates */
|
|
22
|
+
const PRESERVED_FILES = new Set([
|
|
23
|
+
'session-context.md',
|
|
24
|
+
'session-state.json',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/** Directories whose contents should never be overwritten */
|
|
28
|
+
const PRESERVED_DIRS = new Set([
|
|
29
|
+
'decisions',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} UpdateReport
|
|
34
|
+
* @property {string[]} added - Files that were added (new in source)
|
|
35
|
+
* @property {string[]} updated - Files that were updated (hash mismatch)
|
|
36
|
+
* @property {string[]} skipped - Files that were preserved (user customizations)
|
|
37
|
+
* @property {string[]} unchanged - Files that are identical
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Computes a SHA-256 hash of a file's contents.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} filePath - Absolute path to the file
|
|
44
|
+
* @returns {string} Hex-encoded SHA-256 hash
|
|
45
|
+
*/
|
|
46
|
+
function fileHash(filePath) {
|
|
47
|
+
const content = fs.readFileSync(filePath);
|
|
48
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Recursively collects all file paths relative to a root directory.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} rootDir - Root directory to scan
|
|
55
|
+
* @param {string} [prefix=''] - Path prefix for recursion
|
|
56
|
+
* @returns {string[]} Array of relative paths
|
|
57
|
+
*/
|
|
58
|
+
function collectFiles(rootDir, prefix = '') {
|
|
59
|
+
/** @type {string[]} */
|
|
60
|
+
const files = [];
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(rootDir)) {
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
67
|
+
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
70
|
+
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
files.push(...collectFiles(path.join(rootDir, entry.name), relativePath));
|
|
73
|
+
} else {
|
|
74
|
+
files.push(relativePath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return files;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Determines if a relative file path is in the preserved set.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} relativePath - Relative path from .agent/ root
|
|
85
|
+
* @returns {boolean} True if the file should be preserved
|
|
86
|
+
*/
|
|
87
|
+
function isPreservedFile(relativePath) {
|
|
88
|
+
// Check exact filename matches
|
|
89
|
+
const basename = path.basename(relativePath);
|
|
90
|
+
if (PRESERVED_FILES.has(basename)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check if inside a preserved directory
|
|
95
|
+
const parts = relativePath.split(/[/\\]/);
|
|
96
|
+
for (const dir of PRESERVED_DIRS) {
|
|
97
|
+
if (parts.includes(dir)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generates a diff report comparing source and target .agent/ directories.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} sourceRoot - Root of the package (source .agent/ location)
|
|
109
|
+
* @param {string} targetRoot - Root of the project (target .agent/ location)
|
|
110
|
+
* @returns {UpdateReport}
|
|
111
|
+
*/
|
|
112
|
+
function generateDiff(sourceRoot, targetRoot) {
|
|
113
|
+
const sourceDir = path.join(sourceRoot, AGENT_DIR);
|
|
114
|
+
const targetDir = path.join(targetRoot, AGENT_DIR);
|
|
115
|
+
|
|
116
|
+
const sourceFiles = collectFiles(sourceDir);
|
|
117
|
+
const targetFiles = new Set(collectFiles(targetDir));
|
|
118
|
+
|
|
119
|
+
/** @type {UpdateReport} */
|
|
120
|
+
const report = {
|
|
121
|
+
added: [],
|
|
122
|
+
updated: [],
|
|
123
|
+
skipped: [],
|
|
124
|
+
unchanged: [],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
for (const relativeFile of sourceFiles) {
|
|
128
|
+
const sourcePath = path.join(sourceDir, relativeFile);
|
|
129
|
+
const targetPath = path.join(targetDir, relativeFile);
|
|
130
|
+
|
|
131
|
+
if (isPreservedFile(relativeFile) && targetFiles.has(relativeFile)) {
|
|
132
|
+
report.skipped.push(relativeFile);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!targetFiles.has(relativeFile)) {
|
|
137
|
+
report.added.push(relativeFile);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const sourceHash = fileHash(sourcePath);
|
|
142
|
+
const targetHash = fileHash(targetPath);
|
|
143
|
+
|
|
144
|
+
if (sourceHash !== targetHash) {
|
|
145
|
+
report.updated.push(relativeFile);
|
|
146
|
+
} else {
|
|
147
|
+
report.unchanged.push(relativeFile);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return report;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Applies a non-destructive update from source to target.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} sourceRoot - Root of the package
|
|
158
|
+
* @param {string} targetRoot - Root of the project
|
|
159
|
+
* @param {boolean} [dryRun=false] - If true, report only without modifying
|
|
160
|
+
* @returns {UpdateReport}
|
|
161
|
+
*/
|
|
162
|
+
function applyUpdate(sourceRoot, targetRoot, dryRun = false) {
|
|
163
|
+
const report = generateDiff(sourceRoot, targetRoot);
|
|
164
|
+
|
|
165
|
+
if (dryRun) {
|
|
166
|
+
return report;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const sourceDir = path.join(sourceRoot, AGENT_DIR);
|
|
170
|
+
const targetDir = path.join(targetRoot, AGENT_DIR);
|
|
171
|
+
|
|
172
|
+
// Copy new files
|
|
173
|
+
for (const relativeFile of report.added) {
|
|
174
|
+
const sourcePath = path.join(sourceDir, relativeFile);
|
|
175
|
+
const targetPath = path.join(targetDir, relativeFile);
|
|
176
|
+
const targetDirPath = path.dirname(targetPath);
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync(targetDirPath)) {
|
|
179
|
+
fs.mkdirSync(targetDirPath, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Update modified files
|
|
186
|
+
for (const relativeFile of report.updated) {
|
|
187
|
+
const sourcePath = path.join(sourceDir, relativeFile);
|
|
188
|
+
const targetPath = path.join(targetDir, relativeFile);
|
|
189
|
+
|
|
190
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return report;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
generateDiff,
|
|
198
|
+
applyUpdate,
|
|
199
|
+
isPreservedFile,
|
|
200
|
+
collectFiles,
|
|
201
|
+
};
|