aethel 1.0.0 → 1.2.0
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/CHANGELOG.md +19 -0
- package/README.md +45 -2
- package/docs/ARCHITECTURE.md +24 -0
- package/package.json +5 -3
- package/src/cli.js +33 -4
- package/src/core/compress.js +285 -0
- package/src/core/config.js +119 -0
- package/src/core/diff.js +146 -7
- package/src/core/pack-manifest.js +163 -0
- package/src/core/pack.js +355 -0
- package/src/core/snapshot.js +55 -9
package/src/core/diff.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isWorkspaceType } from "./drive-api.js";
|
|
2
2
|
import { loadIgnoreRules } from "./ignore.js";
|
|
3
|
+
import { loadPackManifest } from "./config.js";
|
|
3
4
|
|
|
4
5
|
export const ChangeType = Object.freeze({
|
|
5
6
|
REMOTE_ADDED: "remote_added",
|
|
@@ -9,6 +10,12 @@ export const ChangeType = Object.freeze({
|
|
|
9
10
|
LOCAL_MODIFIED: "local_modified",
|
|
10
11
|
LOCAL_DELETED: "local_deleted",
|
|
11
12
|
CONFLICT: "conflict",
|
|
13
|
+
// Pack-specific change types
|
|
14
|
+
PACK_LOCAL_MODIFIED: "pack_local_modified",
|
|
15
|
+
PACK_REMOTE_MODIFIED: "pack_remote_modified",
|
|
16
|
+
PACK_SYNCED: "pack_synced",
|
|
17
|
+
PACK_CONFLICT: "pack_conflict",
|
|
18
|
+
PACK_NEW: "pack_new",
|
|
12
19
|
});
|
|
13
20
|
|
|
14
21
|
const SHORT_STATUS = {
|
|
@@ -19,6 +26,11 @@ const SHORT_STATUS = {
|
|
|
19
26
|
[ChangeType.LOCAL_MODIFIED]: "ML",
|
|
20
27
|
[ChangeType.LOCAL_DELETED]: "-L",
|
|
21
28
|
[ChangeType.CONFLICT]: "!!",
|
|
29
|
+
[ChangeType.PACK_LOCAL_MODIFIED]: "PL",
|
|
30
|
+
[ChangeType.PACK_REMOTE_MODIFIED]: "PR",
|
|
31
|
+
[ChangeType.PACK_SYNCED]: "P=",
|
|
32
|
+
[ChangeType.PACK_CONFLICT]: "P!",
|
|
33
|
+
[ChangeType.PACK_NEW]: "P+",
|
|
22
34
|
};
|
|
23
35
|
|
|
24
36
|
const DESCRIPTION = {
|
|
@@ -29,6 +41,11 @@ const DESCRIPTION = {
|
|
|
29
41
|
[ChangeType.LOCAL_MODIFIED]: "modified locally",
|
|
30
42
|
[ChangeType.LOCAL_DELETED]: "deleted locally",
|
|
31
43
|
[ChangeType.CONFLICT]: "both sides changed",
|
|
44
|
+
[ChangeType.PACK_LOCAL_MODIFIED]: "pack changed locally",
|
|
45
|
+
[ChangeType.PACK_REMOTE_MODIFIED]: "pack changed on Drive",
|
|
46
|
+
[ChangeType.PACK_SYNCED]: "pack up to date",
|
|
47
|
+
[ChangeType.PACK_CONFLICT]: "pack conflict",
|
|
48
|
+
[ChangeType.PACK_NEW]: "new pack",
|
|
32
49
|
};
|
|
33
50
|
|
|
34
51
|
const SUGGESTED_ACTION = {
|
|
@@ -39,6 +56,11 @@ const SUGGESTED_ACTION = {
|
|
|
39
56
|
[ChangeType.LOCAL_MODIFIED]: "upload",
|
|
40
57
|
[ChangeType.LOCAL_DELETED]: "delete_remote",
|
|
41
58
|
[ChangeType.CONFLICT]: "conflict",
|
|
59
|
+
[ChangeType.PACK_LOCAL_MODIFIED]: "push_pack",
|
|
60
|
+
[ChangeType.PACK_REMOTE_MODIFIED]: "pull_pack",
|
|
61
|
+
[ChangeType.PACK_SYNCED]: "none",
|
|
62
|
+
[ChangeType.PACK_CONFLICT]: "resolve_pack",
|
|
63
|
+
[ChangeType.PACK_NEW]: "push_pack",
|
|
42
64
|
};
|
|
43
65
|
|
|
44
66
|
function createChange({
|
|
@@ -62,9 +84,10 @@ function createChange({
|
|
|
62
84
|
};
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
function buildDiffResult(changes) {
|
|
87
|
+
function buildDiffResult(changes, packChanges = []) {
|
|
66
88
|
return {
|
|
67
89
|
changes,
|
|
90
|
+
packChanges,
|
|
68
91
|
get remoteChanges() {
|
|
69
92
|
return this.changes.filter((change) =>
|
|
70
93
|
change.changeType.startsWith("remote")
|
|
@@ -80,8 +103,29 @@ function buildDiffResult(changes) {
|
|
|
80
103
|
(change) => change.changeType === ChangeType.CONFLICT
|
|
81
104
|
);
|
|
82
105
|
},
|
|
106
|
+
get packConflicts() {
|
|
107
|
+
return this.packChanges.filter(
|
|
108
|
+
(change) => change.changeType === ChangeType.PACK_CONFLICT
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
get pendingPackChanges() {
|
|
112
|
+
return this.packChanges.filter(
|
|
113
|
+
(change) =>
|
|
114
|
+
change.changeType === ChangeType.PACK_LOCAL_MODIFIED ||
|
|
115
|
+
change.changeType === ChangeType.PACK_REMOTE_MODIFIED ||
|
|
116
|
+
change.changeType === ChangeType.PACK_NEW
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
get syncedPacks() {
|
|
120
|
+
return this.packChanges.filter(
|
|
121
|
+
(change) => change.changeType === ChangeType.PACK_SYNCED
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
get hasPackChanges() {
|
|
125
|
+
return this.pendingPackChanges.length > 0 || this.packConflicts.length > 0;
|
|
126
|
+
},
|
|
83
127
|
get isClean() {
|
|
84
|
-
return this.changes.length === 0;
|
|
128
|
+
return this.changes.length === 0 && this.pendingPackChanges.length === 0;
|
|
85
129
|
},
|
|
86
130
|
};
|
|
87
131
|
}
|
|
@@ -154,6 +198,93 @@ function promoteConflicts(changes) {
|
|
|
154
198
|
return filtered;
|
|
155
199
|
}
|
|
156
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Compute pack-level changes by comparing local packedDirs against manifest.
|
|
203
|
+
* @param {string|null} root - Workspace root for loading manifest
|
|
204
|
+
* @param {object} packedDirs - Local packed directories from scanLocal
|
|
205
|
+
* @param {object|null} snapshot - Previous snapshot (may contain packedDirs)
|
|
206
|
+
* @returns {object[]} Array of pack change objects
|
|
207
|
+
*/
|
|
208
|
+
function computePackChanges(root, packedDirs, snapshot) {
|
|
209
|
+
if (!root || !packedDirs || Object.keys(packedDirs).length === 0) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const manifest = loadPackManifest(root);
|
|
214
|
+
const snapshotPackedDirs = snapshot?.packedDirs || {};
|
|
215
|
+
const changes = [];
|
|
216
|
+
|
|
217
|
+
for (const [packPath, packInfo] of Object.entries(packedDirs)) {
|
|
218
|
+
const manifestEntry = manifest.packs?.[packPath];
|
|
219
|
+
const snapshotEntry = snapshotPackedDirs[packPath];
|
|
220
|
+
const localTreeHash = packInfo.treeHash;
|
|
221
|
+
|
|
222
|
+
if (!manifestEntry) {
|
|
223
|
+
// Pack not in manifest = new pack
|
|
224
|
+
changes.push(
|
|
225
|
+
createChange({
|
|
226
|
+
changeType: ChangeType.PACK_NEW,
|
|
227
|
+
path: packPath,
|
|
228
|
+
localMeta: packInfo,
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const { localTreeHash: manifestLocalHash, remoteTreeHash: manifestRemoteHash } = manifestEntry;
|
|
235
|
+
|
|
236
|
+
// Check for local modification
|
|
237
|
+
const localChanged = localTreeHash !== manifestLocalHash;
|
|
238
|
+
// Check for remote modification (comparing against what we last synced)
|
|
239
|
+
const remoteChanged = manifestRemoteHash && manifestRemoteHash !== manifestLocalHash;
|
|
240
|
+
|
|
241
|
+
if (localChanged && remoteChanged) {
|
|
242
|
+
// Both sides changed = conflict
|
|
243
|
+
changes.push(
|
|
244
|
+
createChange({
|
|
245
|
+
changeType: ChangeType.PACK_CONFLICT,
|
|
246
|
+
path: packPath,
|
|
247
|
+
localMeta: { ...packInfo, treeHash: localTreeHash },
|
|
248
|
+
snapshotMeta: { treeHash: manifestLocalHash },
|
|
249
|
+
remoteMeta: { treeHash: manifestRemoteHash },
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
} else if (localChanged) {
|
|
253
|
+
// Only local changed
|
|
254
|
+
changes.push(
|
|
255
|
+
createChange({
|
|
256
|
+
changeType: ChangeType.PACK_LOCAL_MODIFIED,
|
|
257
|
+
path: packPath,
|
|
258
|
+
localMeta: { ...packInfo, treeHash: localTreeHash },
|
|
259
|
+
snapshotMeta: { treeHash: manifestLocalHash },
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
} else if (remoteChanged) {
|
|
263
|
+
// Only remote changed
|
|
264
|
+
changes.push(
|
|
265
|
+
createChange({
|
|
266
|
+
changeType: ChangeType.PACK_REMOTE_MODIFIED,
|
|
267
|
+
path: packPath,
|
|
268
|
+
localMeta: packInfo,
|
|
269
|
+
remoteMeta: { treeHash: manifestRemoteHash },
|
|
270
|
+
snapshotMeta: { treeHash: manifestLocalHash },
|
|
271
|
+
})
|
|
272
|
+
);
|
|
273
|
+
} else {
|
|
274
|
+
// No changes = synced
|
|
275
|
+
changes.push(
|
|
276
|
+
createChange({
|
|
277
|
+
changeType: ChangeType.PACK_SYNCED,
|
|
278
|
+
path: packPath,
|
|
279
|
+
localMeta: packInfo,
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return changes;
|
|
286
|
+
}
|
|
287
|
+
|
|
157
288
|
/**
|
|
158
289
|
* @param {object|null} snapshot
|
|
159
290
|
* @param {object[]} remoteFiles
|
|
@@ -182,6 +313,11 @@ export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIg
|
|
|
182
313
|
if (ignoreRules) {
|
|
183
314
|
remoteFiles = remoteFiles.filter((f) => !ignoreRules.ignores(f.path));
|
|
184
315
|
}
|
|
316
|
+
|
|
317
|
+
// Handle new localFiles format with .files and .packedDirs
|
|
318
|
+
const localFilesData = localFiles?.files ?? localFiles;
|
|
319
|
+
const packedDirs = localFiles?.packedDirs ?? {};
|
|
320
|
+
|
|
185
321
|
const changes = [];
|
|
186
322
|
const snapshotFiles = snapshot?.files || {};
|
|
187
323
|
const snapshotLocalFiles = snapshot?.localFiles || {};
|
|
@@ -189,13 +325,13 @@ export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIg
|
|
|
189
325
|
// Build sets of all folder paths that implicitly exist on each side
|
|
190
326
|
// (from parent directories of files), so we can skip redundant folder additions.
|
|
191
327
|
const remoteFolderPaths = collectFolderPaths(remoteFiles.map((f) => f.path));
|
|
192
|
-
const localFolderPaths = collectFolderPaths(Object.keys(
|
|
328
|
+
const localFolderPaths = collectFolderPaths(Object.keys(localFilesData));
|
|
193
329
|
|
|
194
330
|
// Also include explicit folder entries
|
|
195
331
|
for (const f of remoteFiles) {
|
|
196
332
|
if (f.isFolder) remoteFolderPaths.add(f.path);
|
|
197
333
|
}
|
|
198
|
-
for (const [p, meta] of Object.entries(
|
|
334
|
+
for (const [p, meta] of Object.entries(localFilesData)) {
|
|
199
335
|
if (meta.isFolder) localFolderPaths.add(p);
|
|
200
336
|
}
|
|
201
337
|
|
|
@@ -254,7 +390,7 @@ export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIg
|
|
|
254
390
|
}
|
|
255
391
|
}
|
|
256
392
|
|
|
257
|
-
for (const [relativePath, localMeta] of Object.entries(
|
|
393
|
+
for (const [relativePath, localMeta] of Object.entries(localFilesData)) {
|
|
258
394
|
const snapshotEntry = snapshotLocalFiles[relativePath];
|
|
259
395
|
|
|
260
396
|
if (!snapshotEntry) {
|
|
@@ -285,7 +421,7 @@ export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIg
|
|
|
285
421
|
}
|
|
286
422
|
|
|
287
423
|
for (const [relativePath, snapshotEntry] of Object.entries(snapshotLocalFiles)) {
|
|
288
|
-
if (!(relativePath in
|
|
424
|
+
if (!(relativePath in localFilesData)) {
|
|
289
425
|
// Skip folder deletion if the folder still implicitly exists locally
|
|
290
426
|
if (snapshotEntry.isFolder && localFolderPaths.has(relativePath)) {
|
|
291
427
|
continue;
|
|
@@ -300,5 +436,8 @@ export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIg
|
|
|
300
436
|
}
|
|
301
437
|
}
|
|
302
438
|
|
|
303
|
-
|
|
439
|
+
// Compute pack changes
|
|
440
|
+
const packChanges = computePackChanges(root, packedDirs, snapshot);
|
|
441
|
+
|
|
442
|
+
return buildDiffResult(promoteConflicts(changes), packChanges);
|
|
304
443
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack manifest CRUD operations.
|
|
3
|
+
* Manages the pack-manifest.json data structure.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
|
|
8
|
+
const MANIFEST_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a new empty manifest.
|
|
12
|
+
* @returns {{ version: number, packs: {} }}
|
|
13
|
+
*/
|
|
14
|
+
export function createManifest() {
|
|
15
|
+
return {
|
|
16
|
+
version: MANIFEST_VERSION,
|
|
17
|
+
packs: {},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get pack information for a specific path.
|
|
23
|
+
* @param {object} manifest - Manifest object
|
|
24
|
+
* @param {string} packPath - Directory path (e.g., "node_modules")
|
|
25
|
+
* @returns {object|null} Pack info or null if not found
|
|
26
|
+
*/
|
|
27
|
+
export function getPack(manifest, packPath) {
|
|
28
|
+
const normalized = normalizePath(packPath);
|
|
29
|
+
return manifest.packs[normalized] ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Set or update pack information.
|
|
34
|
+
* @param {object} manifest - Manifest object
|
|
35
|
+
* @param {string} packPath - Directory path
|
|
36
|
+
* @param {object} data - Pack data to set/merge
|
|
37
|
+
* @returns {object} Updated manifest
|
|
38
|
+
*/
|
|
39
|
+
export function setPack(manifest, packPath, data) {
|
|
40
|
+
const normalized = normalizePath(packPath);
|
|
41
|
+
const existing = manifest.packs[normalized] ?? {};
|
|
42
|
+
|
|
43
|
+
manifest.packs[normalized] = {
|
|
44
|
+
...existing,
|
|
45
|
+
...data,
|
|
46
|
+
// Always update lastModified when setting
|
|
47
|
+
lastModified: new Date().toISOString(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return manifest;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Remove a pack from the manifest.
|
|
55
|
+
* @param {object} manifest - Manifest object
|
|
56
|
+
* @param {string} packPath - Directory path
|
|
57
|
+
* @returns {object} Updated manifest
|
|
58
|
+
*/
|
|
59
|
+
export function removePack(manifest, packPath) {
|
|
60
|
+
const normalized = normalizePath(packPath);
|
|
61
|
+
delete manifest.packs[normalized];
|
|
62
|
+
return manifest;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* List all packs in the manifest.
|
|
67
|
+
* @param {object} manifest - Manifest object
|
|
68
|
+
* @returns {Array<{ path: string, info: object }>}
|
|
69
|
+
*/
|
|
70
|
+
export function listPacks(manifest) {
|
|
71
|
+
return Object.entries(manifest.packs).map(([path, info]) => ({
|
|
72
|
+
path,
|
|
73
|
+
info,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if a path is covered by any pack rule.
|
|
79
|
+
* @param {object} manifest - Manifest object
|
|
80
|
+
* @param {string} filePath - File path to check
|
|
81
|
+
* @returns {{ isPacked: boolean, packPath: string|null }}
|
|
82
|
+
*/
|
|
83
|
+
export function isPathPacked(manifest, filePath) {
|
|
84
|
+
const normalized = normalizePath(filePath);
|
|
85
|
+
|
|
86
|
+
for (const packPath of Object.keys(manifest.packs)) {
|
|
87
|
+
// Check if filePath starts with packPath
|
|
88
|
+
if (normalized === packPath || normalized.startsWith(packPath + "/")) {
|
|
89
|
+
return { isPacked: true, packPath };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { isPacked: false, packPath: null };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate manifest structure.
|
|
98
|
+
* @param {object} manifest - Manifest to validate
|
|
99
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
100
|
+
*/
|
|
101
|
+
export function validateManifest(manifest) {
|
|
102
|
+
const errors = [];
|
|
103
|
+
|
|
104
|
+
if (!manifest || typeof manifest !== "object") {
|
|
105
|
+
errors.push("Manifest must be an object");
|
|
106
|
+
return { valid: false, errors };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (typeof manifest.version !== "number") {
|
|
110
|
+
errors.push("Manifest version must be a number");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (manifest.version !== MANIFEST_VERSION) {
|
|
114
|
+
errors.push(`Unsupported manifest version: ${manifest.version} (expected ${MANIFEST_VERSION})`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!manifest.packs || typeof manifest.packs !== "object") {
|
|
118
|
+
errors.push("Manifest packs must be an object");
|
|
119
|
+
} else {
|
|
120
|
+
for (const [path, info] of Object.entries(manifest.packs)) {
|
|
121
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
122
|
+
errors.push(`Invalid pack path: ${path}`);
|
|
123
|
+
}
|
|
124
|
+
if (!info || typeof info !== "object") {
|
|
125
|
+
errors.push(`Pack info for "${path}" must be an object`);
|
|
126
|
+
} else {
|
|
127
|
+
if (!info.packId || typeof info.packId !== "string") {
|
|
128
|
+
errors.push(`Pack "${path}" missing valid packId`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { valid: errors.length === 0, errors };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate a unique pack ID.
|
|
139
|
+
* @param {string} packPath - Directory path
|
|
140
|
+
* @returns {string} Pack ID like "pack-node_modules-a1b2c3d4"
|
|
141
|
+
*/
|
|
142
|
+
export function generatePackId(packPath) {
|
|
143
|
+
const normalized = normalizePath(packPath);
|
|
144
|
+
// Sanitize path for ID: replace / with _ and remove special chars
|
|
145
|
+
const sanitized = normalized
|
|
146
|
+
.replace(/\//g, "_")
|
|
147
|
+
.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
148
|
+
const shortHash = crypto.randomBytes(4).toString("hex");
|
|
149
|
+
return `pack-${sanitized}-${shortHash}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Normalize a path for consistent manifest keys.
|
|
154
|
+
* @param {string} p - Path to normalize
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
function normalizePath(p) {
|
|
158
|
+
// Remove leading/trailing slashes and normalize to forward slashes
|
|
159
|
+
return p
|
|
160
|
+
.replace(/\\/g, "/")
|
|
161
|
+
.replace(/^\/+/, "")
|
|
162
|
+
.replace(/\/+$/, "");
|
|
163
|
+
}
|