pagan-artifact 0.3.2 → 0.3.3

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/setup/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
- * art - Modern version control.
3
- * Module: Setup (v0.3.2)
2
+ * Artifact - Modern version control.
3
+ * @author Benny Schmidt (https://github.com/bennyschmidt)
4
+ * @project https://github.com/bennyschmidt/artifact
5
+ * Module: Setup (v0.3.3)
4
6
  */
5
7
 
6
8
  const fs = require('fs');
@@ -8,15 +10,20 @@ const path = require('path');
8
10
 
9
11
  const pkg = require('../package.json');
10
12
  const shouldIgnore = require('../utils/shouldIgnore');
13
+ const { MAX_PART_CHARS } = require('../utils/constants');
11
14
 
12
15
  const ARTIFACT_HOST = pkg.artConfig.host || 'http://localhost:1337';
13
- const MAX_PART_CHARS = 32000000;
14
16
 
15
17
  /**
16
18
  * Internal helper to create the .art directory tree.
19
+ * @param {string} artDirectory - The path to the .art folder.
17
20
  */
18
21
 
19
- function ensureDirStructure(artDirectory) {
22
+ function ensureDirStructure (artDirectory) {
23
+ /**
24
+ * Define the core folder hierarchy required for local and remote tracking.
25
+ */
26
+
20
27
  const folders = [
21
28
  '',
22
29
  'root',
@@ -38,9 +45,15 @@ function ensureDirStructure(artDirectory) {
38
45
 
39
46
  /**
40
47
  * Initializes the local .art directory structure and indexes current files.
48
+ * @param {string} directoryPath - The root directory to initialize.
49
+ * @returns {string} Success message.
41
50
  */
42
51
 
43
52
  function init (directoryPath = process.cwd()) {
53
+ /**
54
+ * Check for an existing repository to prevent accidental overwrites.
55
+ */
56
+
44
57
  const artDirectory = path.join(directoryPath, '.art');
45
58
 
46
59
  if (fs.existsSync(artDirectory)) {
@@ -49,11 +62,15 @@ function init (directoryPath = process.cwd()) {
49
62
 
50
63
  ensureDirStructure(artDirectory);
51
64
 
65
+ /**
66
+ * Filter the working directory to find unignored files that are not internal.
67
+ */
68
+
52
69
  const files = fs.readdirSync(directoryPath, { recursive: true })
53
- .filter(f => {
54
- const isInternal = f === '.art' || f.startsWith('.art' + path.sep);
70
+ .filter(file => {
71
+ const isInternal = file === '.art' || file.startsWith('.art' + path.sep);
55
72
 
56
- return !isInternal && !shouldIgnore(f);
73
+ return !isInternal && !shouldIgnore(file);
57
74
  });
58
75
 
59
76
  const rootMasterManifest = { parts: [] };
@@ -61,8 +78,14 @@ function init (directoryPath = process.cwd()) {
61
78
  let currentPartFiles = [];
62
79
  let currentPartChars = 0;
63
80
 
81
+ /**
82
+ * Internal helper to save indexed file snapshots into manifest parts.
83
+ */
84
+
64
85
  const saveManifestPart = () => {
65
- if (currentPartFiles.length === 0) return;
86
+ if (currentPartFiles.length === 0) {
87
+ return;
88
+ }
66
89
 
67
90
  const partIndex = rootMasterManifest.parts.length;
68
91
  const partName = `manifest.part.${partIndex}.json`;
@@ -78,6 +101,10 @@ function init (directoryPath = process.cwd()) {
78
101
  currentPartChars = 0;
79
102
  };
80
103
 
104
+ /**
105
+ * Iterate through files and group their content into the root manifest.
106
+ */
107
+
81
108
  for (const file of files) {
82
109
  const fullPath = path.join(directoryPath, file);
83
110
 
@@ -99,6 +126,10 @@ function init (directoryPath = process.cwd()) {
99
126
 
100
127
  saveManifestPart();
101
128
 
129
+ /**
130
+ * Initialize manifest files for root and default branch histories.
131
+ */
132
+
102
133
  fs.writeFileSync(
103
134
  path.join(artDirectory, 'root/manifest.json'),
104
135
  JSON.stringify(rootMasterManifest, null, 2)
@@ -114,6 +145,10 @@ function init (directoryPath = process.cwd()) {
114
145
  JSON.stringify({ commits: [] }, null, 2)
115
146
  );
116
147
 
148
+ /**
149
+ * Create the primary art.json state file with default pointers.
150
+ */
151
+
117
152
  const artFile = {
118
153
  active: { branch: 'main', parent: null },
119
154
  remote: '',
@@ -130,11 +165,18 @@ function init (directoryPath = process.cwd()) {
130
165
 
131
166
  /**
132
167
  * Clone a repository and replay history.
168
+ * @param {string} repoSlug - The handle/repo identifier.
169
+ * @param {string} providedToken - Optional access token.
170
+ * @returns {Promise<string>} Success message.
133
171
  */
134
172
 
135
173
  async function clone (repoSlug, providedToken = null) {
174
+ /**
175
+ * Validate slug format and check destination availability.
176
+ */
177
+
136
178
  if (!repoSlug || !repoSlug.includes('/')) {
137
- throw new Error('A valid slug is required (e.g., handle/repo).');
179
+ throw new Error('A valid slug is required (handle/repo).');
138
180
  }
139
181
 
140
182
  const [handle, repo] = repoSlug.split('/');
@@ -145,6 +187,10 @@ async function clone (repoSlug, providedToken = null) {
145
187
  throw new Error(`Destination path "${targetPath}" already exists.`);
146
188
  }
147
189
 
190
+ /**
191
+ * Prepare target directory and enter the workspace.
192
+ */
193
+
148
194
  fs.mkdirSync(targetPath, { recursive: true });
149
195
 
150
196
  const artPath = path.join(targetPath, '.art');
@@ -153,44 +199,77 @@ async function clone (repoSlug, providedToken = null) {
153
199
  process.chdir(targetPath);
154
200
 
155
201
  try {
202
+ /**
203
+ * Set up local art.json with remote tracking info.
204
+ */
205
+
156
206
  const artJsonPath = path.join(artPath, 'art.json');
157
207
 
158
208
  const artJson = {
159
209
  active: { branch: 'main', parent: null },
160
210
  remote: `${ARTIFACT_HOST}/${handle}/${repo}`,
161
- configuration: { handle: '', personalAccessToken: providedToken || '' }
211
+ configuration: {
212
+ handle: '',
213
+ personalAccessToken: providedToken || ''
214
+ }
162
215
  };
163
216
 
164
217
  fs.writeFileSync(artJsonPath, JSON.stringify(artJson, null, 2));
165
218
 
166
219
  const token = artJson.configuration.personalAccessToken;
167
220
 
168
- const rootRes = await fetch(`${ARTIFACT_HOST}/manifest`, {
169
- method: 'POST',
170
- headers: { 'Content-Type': 'application/json' },
171
- body: JSON.stringify({
172
- type: 'root',
173
- handle,
174
- repo,
175
- branch: 'main',
176
- ...(token && { personalAccessToken: token })
177
- })
178
- });
221
+ /**
222
+ * Fetch the root manifest to reconstruct the project's base state.
223
+ */
224
+
225
+ const rootRes = await fetch(
226
+ `${ARTIFACT_HOST}/manifest`,
227
+ {
228
+ method: 'POST',
229
+ headers: { 'Content-Type': 'application/json' },
230
+ body: JSON.stringify({
231
+ type: 'root',
232
+ handle,
233
+ repo,
234
+ branch: 'main',
235
+
236
+ ...(token && { personalAccessToken: token })
237
+ })
238
+ }
239
+ );
179
240
 
180
241
  const masterManifest = await rootRes.json();
181
242
 
182
- fs.writeFileSync(path.join(artPath, 'root/manifest.json'), JSON.stringify(masterManifest, null, 2));
243
+ fs.writeFileSync(
244
+ path.join(artPath, 'root/manifest.json'),
245
+ JSON.stringify(masterManifest, null, 2)
246
+ );
247
+
248
+ /**
249
+ * Download root parts and write files to the working directory.
250
+ */
183
251
 
184
252
  for (const partName of masterManifest.parts) {
185
- const partRes = await fetch(`${ARTIFACT_HOST}/part`, {
186
- method: 'POST',
187
- headers: { 'Content-Type': 'application/json' },
188
- body: JSON.stringify({ handle, repo, partName, ...(token && { personalAccessToken: token }) })
189
- });
253
+ const partRes = await fetch(
254
+ `${ARTIFACT_HOST}/part`,
255
+ {
256
+ method: 'POST',
257
+ headers: { 'Content-Type': 'application/json' },
258
+ body: JSON.stringify({
259
+ handle,
260
+ repo,
261
+ partName,
262
+ ...(token && { personalAccessToken: token })
263
+ })
264
+ }
265
+ );
190
266
 
191
267
  const partData = await partRes.json();
192
268
 
193
- fs.writeFileSync(path.join(artPath, 'root', partName), JSON.stringify(partData, null, 2));
269
+ fs.writeFileSync(
270
+ path.join(artPath, 'root', partName),
271
+ JSON.stringify(partData, null, 2)
272
+ );
194
273
 
195
274
  for (const file of partData.files) {
196
275
  const workingPath = path.join(targetPath, file.path);
@@ -200,44 +279,79 @@ async function clone (repoSlug, providedToken = null) {
200
279
  }
201
280
  }
202
281
 
203
- const historyRes = await fetch(`${ARTIFACT_HOST}/manifest`, {
204
- method: 'POST',
205
- headers: { 'Content-Type': 'application/json' },
206
- body: JSON.stringify({ type: 'history', handle, repo, branch: 'main', ...(token && { personalAccessToken: token }) })
207
- });
208
-
209
- const historyManifest = await historyRes.json();
210
- const localHistoryDir = path.join(artPath, 'history/local/main');
211
- const remoteHistoryDir = path.join(artPath, 'history/remote/main');
282
+ /**
283
+ * Retrieve the full history manifest for the main branch.
284
+ */
212
285
 
213
- for (const commitHash of historyManifest.commits) {
214
- const commitRes = await fetch(`${ARTIFACT_HOST}/commit`, {
286
+ const historyRes = await fetch(
287
+ `${ARTIFACT_HOST}/manifest`,
288
+ {
215
289
  method: 'POST',
216
290
  headers: { 'Content-Type': 'application/json' },
217
291
  body: JSON.stringify({
292
+ type: 'history',
218
293
  handle,
219
294
  repo,
220
295
  branch: 'main',
221
- hash: commitHash,
296
+ ...(token && { personalAccessToken: token })
297
+ })
298
+ }
299
+ );
300
+
301
+ const historyManifest = await historyRes.json();
302
+ const localHistoryDir = path.join(artPath, 'history/local/main');
303
+ const remoteHistoryDir = path.join(artPath, 'history/remote/main');
304
+
305
+ /**
306
+ * Replay every commit in chronological order to sync local state with
307
+ * remote history.
308
+ */
222
309
 
223
- ...(token && { personalAccessToken: token
310
+ for (const commitHash of historyManifest.commits) {
311
+ const commitRes = await fetch(
312
+ `${ARTIFACT_HOST}/commit`,
313
+ {
314
+ method: 'POST',
315
+ headers: { 'Content-Type': 'application/json' },
316
+ body: JSON.stringify({
317
+ handle,
318
+ repo,
319
+ branch: 'main',
320
+ hash: commitHash,
321
+ ...(token && { personalAccessToken: token })
224
322
  })
225
- })
226
- });
323
+ }
324
+ );
227
325
 
228
326
  const commitMaster = await commitRes.json();
229
327
 
230
328
  let fullChanges = {};
231
329
 
330
+ /**
331
+ * Fetch and merge paginated changes if the commit is split into parts.
332
+ */
333
+
232
334
  if (commitMaster.parts && commitMaster.parts.length > 0) {
233
335
  for (const partName of commitMaster.parts) {
234
- const partRes = await fetch(`${ARTIFACT_HOST}/commit/part`, {
235
- method: 'POST',
236
- headers: { 'Content-Type': 'application/json' },
237
- body: JSON.stringify({ handle, repo, branch: 'main', partName, ...(token && { personalAccessToken: token }) })
238
- });
336
+ const partRes = await fetch(
337
+ `${ARTIFACT_HOST}/part`,
338
+ {
339
+ method: 'POST',
340
+ headers: { 'Content-Type': 'application/json' },
341
+ body: JSON.stringify({
342
+ type: 'history',
343
+ handle,
344
+ repo,
345
+ branch: 'main',
346
+ partName,
347
+
348
+ ...(token && { personalAccessToken: token })
349
+ })
350
+ }
351
+ );
239
352
 
240
353
  const partData = await partRes.json();
354
+
241
355
  fs.writeFileSync(path.join(localHistoryDir, partName), JSON.stringify(partData, null, 2));
242
356
  fs.writeFileSync(path.join(remoteHistoryDir, partName), JSON.stringify(partData, null, 2));
243
357
 
@@ -252,18 +366,24 @@ async function clone (repoSlug, providedToken = null) {
252
366
  fs.writeFileSync(path.join(localHistoryDir, `${commitHash}.json`), masterContent);
253
367
  fs.writeFileSync(path.join(remoteHistoryDir, `${commitHash}.json`), masterContent);
254
368
 
369
+ /**
370
+ * Apply individual file operations (insert, delete, create) to the working tree.
371
+ */
372
+
255
373
  for (const [filePath, changeSet] of Object.entries(fullChanges)) {
256
374
  const fullPath = path.join(targetPath, filePath);
257
375
 
258
376
  if (Array.isArray(changeSet)) {
259
377
  let content = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf8') : '';
260
- for (const op of changeSet) {
261
- if (op.type === 'insert') {
262
- content = content.slice(0, op.position) + op.content + content.slice(op.position);
263
- } else if (op.type === 'delete') {
264
- content = content.slice(0, op.position) + content.slice(op.position + op.length);
378
+
379
+ for (const operation of changeSet) {
380
+ if (operation.type === 'insert') {
381
+ content = content.slice(0, operation.position) + operation.content + content.slice(operation.position);
382
+ } else if (operation.type === 'delete') {
383
+ content = content.slice(0, operation.position) + content.slice(operation.position + operation.length);
265
384
  }
266
385
  }
386
+
267
387
  fs.writeFileSync(fullPath, content);
268
388
  } else if (changeSet.type === 'createFile') {
269
389
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
@@ -274,6 +394,10 @@ async function clone (repoSlug, providedToken = null) {
274
394
  }
275
395
  }
276
396
 
397
+ /**
398
+ * Finalize the manifests and set the active parent pointer to the latest commit.
399
+ */
400
+
277
401
  const manifestJson = JSON.stringify({ commits: historyManifest.commits }, null, 2);
278
402
 
279
403
  fs.writeFileSync(path.join(localHistoryDir, 'manifest.json'), manifestJson);
@@ -294,18 +418,29 @@ async function clone (repoSlug, providedToken = null) {
294
418
 
295
419
  /**
296
420
  * Updates the configuration in art.json.
421
+ * @param {string} key - Configuration key.
422
+ * @param {any} value - Configuration value.
423
+ * @returns {Object} Updated configuration object.
297
424
  */
298
- function config(key, value) {
425
+
426
+ function config (key, value) {
427
+ /**
428
+ * Verify repo existence and update specific configuration fields.
429
+ */
430
+
299
431
  const manifestPath = path.join(process.cwd(), '.art', 'art.json');
300
432
 
301
433
  if (!fs.existsSync(manifestPath)) {
302
434
  throw new Error('No art repository found.');
303
435
  }
304
436
 
305
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
437
+ const manifest = JSON.parse(
438
+ fs.readFileSync(manifestPath, 'utf8')
439
+ );
306
440
 
307
441
  if (key && value !== undefined) {
308
442
  manifest.configuration[key] = value;
443
+
309
444
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
310
445
  }
311
446
 
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Console colors.
3
+ * ANSI colors for both dark and light theme terminals.
4
+ */
5
+
6
+ export const RED = '\x1b[31m';
7
+ export const GREEN = '\x1b[32m';
8
+ export const RESET = '\x1b[0m';
9
+ export const GRAY = '\x1b[90m';
10
+
11
+ /**
12
+ * Artifact configuration
13
+ */
14
+
15
+ export const MAX_PART_SIZE = 32000000;
@@ -1,87 +1,144 @@
1
1
  /**
2
- * art - Modern version control.
3
- * Module: Utils (v0.3.2)
2
+ * Artifact - Modern version control.
3
+ * @author Benny Schmidt (https://github.com/bennyschmidt)
4
+ * @project https://github.com/bennyschmidt/artifact
5
+ * Module: Utils / getStateByHash (v0.3.3)
4
6
  */
5
7
 
6
8
  const fs = require('fs');
7
9
  const path = require('path');
8
10
 
9
11
  /**
10
- * Helper to reconstruct file states at a specific commit hash.
12
+ * Reconstructs the full file state of a branch at a specific point in time by replaying commits.
13
+ * @param {string} branchName - The branch to read history from.
14
+ * @param {string} targetHash - The commit hash representing the desired state.
15
+ * @returns {Object} A map of file paths to their full string content.
11
16
  */
12
17
 
13
- module.exports = (branchName, targetHash) => {
14
- const artPath = path.join(process.cwd(), '.art');
15
- const rootPath = path.join(artPath, 'root/manifest.json');
16
-
17
- if (!fs.existsSync(rootPath)) return {};
18
-
19
- const rootMaster = JSON.parse(fs.readFileSync(rootPath, 'utf8'));
20
- const branchPath = path.join(artPath, 'history/local', branchName);
21
- const manifestPath = path.join(branchPath, 'manifest.json');
22
-
23
- if (!fs.existsSync(manifestPath)) return {};
24
-
25
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
26
-
27
- let state = {};
28
-
29
- for (const partName of rootMaster.parts) {
30
- const partPath = path.join(artPath, 'root', partName);
31
- const partData = JSON.parse(fs.readFileSync(partPath, 'utf8'));
32
-
33
- for (const file of partData.files) {
34
- state[file.path] = file.content;
35
- }
36
- }
37
-
38
- if (!targetHash) return state;
39
-
40
- for (const hash of manifest.commits) {
41
- const commitPath = path.join(branchPath, `${hash}.json`);
42
-
43
- if (!fs.existsSync(commitPath)) continue;
44
-
45
- const commitMaster = JSON.parse(fs.readFileSync(commitPath, 'utf8'));
46
-
47
- let fullChanges = {};
48
-
49
- if (commitMaster.parts && Array.isArray(commitMaster.parts)) {
50
- for (const partName of commitMaster.parts) {
51
- const partPath = path.join(branchPath, partName);
52
- if (fs.existsSync(partPath)) {
53
- const partData = JSON.parse(fs.readFileSync(partPath, 'utf8'));
54
- Object.assign(fullChanges, partData.changes);
55
- }
56
- }
57
- } else if (commitMaster.changes) {
58
- fullChanges = commitMaster.changes;
59
- }
60
-
61
- for (const [filePath, changeSet] of Object.entries(fullChanges)) {
62
- if (Array.isArray(changeSet)) {
63
- let currentContent = state[filePath] || '';
64
-
65
- for (const operation of changeSet) {
66
- if (operation.type === 'insert') {
67
- currentContent = `${currentContent.slice(0, operation.position)}${operation.content}${currentContent.slice(operation.position)}`;
68
- } else if (operation.type === 'delete') {
69
- currentContent = `${currentContent.slice(0, operation.position)}${currentContent.slice(operation.position + operation.length)}`;
70
- }
71
- }
72
-
73
- state[filePath] = currentContent;
74
- } else {
75
- if (changeSet.type === 'createFile') {
76
- state[filePath] = changeSet.content;
77
- } else if (changeSet.type === 'deleteFile') {
78
- delete state[filePath];
79
- }
80
- }
81
- }
82
-
83
- if (hash === targetHash) break;
84
- }
85
-
86
- return state;
87
- };
18
+ function getStateByHash (branchName, targetHash) {
19
+ /**
20
+ * Initialize repository paths and verify the existence of the root manifest.
21
+ */
22
+
23
+ const artifactPath = path.join(process.cwd(), '.art');
24
+ const rootPath = path.join(artifactPath, 'root/manifest.json');
25
+
26
+ if (!fs.existsSync(rootPath)) {
27
+ return {};
28
+ }
29
+
30
+ /**
31
+ * Load the initial "root" state which serves as the base
32
+ * for all subsequent deltas.
33
+ */
34
+
35
+ const rootMaster = JSON.parse(
36
+ fs.readFileSync(rootPath, 'utf8')
37
+ );
38
+ const branchPath = path.join(artifactPath, 'history/local', branchName);
39
+ const manifestPath = path.join(branchPath, 'manifest.json');
40
+
41
+ if (!fs.existsSync(manifestPath)) {
42
+ return {};
43
+ }
44
+
45
+ const manifest = JSON.parse(
46
+ fs.readFileSync(manifestPath, 'utf8')
47
+ );
48
+
49
+ let state = {};
50
+
51
+ /**
52
+ * Populate the state object with the original snapshots
53
+ * from the assembled root manifest parts.
54
+ */
55
+
56
+ for (const partName of rootMaster.parts) {
57
+ const partPath = path.join(artifactPath, 'root', partName);
58
+ const partData = JSON.parse(
59
+ fs.readFileSync(partPath, 'utf8')
60
+ );
61
+
62
+ for (const file of partData.files) {
63
+ state[file.path] = file.content;
64
+ }
65
+ }
66
+
67
+ if (!targetHash) {
68
+ return state;
69
+ }
70
+
71
+ /**
72
+ * Replay the history of the branch chronologically,
73
+ * applying changes until the target hash is reached.
74
+ */
75
+
76
+ for (const hash of manifest.commits) {
77
+ const commitPath = path.join(branchPath, `${hash}.json`);
78
+
79
+ if (!fs.existsSync(commitPath)) {
80
+ continue;
81
+ }
82
+
83
+ const commitMaster = JSON.parse(
84
+ fs.readFileSync(commitPath, 'utf8')
85
+ );
86
+
87
+ let fullChanges = {};
88
+
89
+ /**
90
+ * Consolidate changes from commit parts or the master commit file.
91
+ */
92
+
93
+ if (commitMaster.parts && Array.isArray(commitMaster.parts)) {
94
+ for (const partName of commitMaster.parts) {
95
+ const partPath = path.join(branchPath, partName);
96
+
97
+ if (fs.existsSync(partPath)) {
98
+ const partData = JSON.parse(
99
+ fs.readFileSync(partPath, 'utf8')
100
+ );
101
+
102
+ Object.assign(fullChanges, partData.changes);
103
+ }
104
+ }
105
+ } else if (commitMaster.changes) {
106
+ fullChanges = commitMaster.changes;
107
+ }
108
+
109
+ /**
110
+ * Apply line-based operations (insert/delete)
111
+ * or file-level operations to the current state.
112
+ */
113
+
114
+ for (const [filePath, changeSet] of Object.entries(fullChanges)) {
115
+ if (Array.isArray(changeSet)) {
116
+ let currentContent = state[filePath] || '';
117
+
118
+ for (const operation of changeSet) {
119
+ if (operation.type === 'insert') {
120
+ currentContent = `${currentContent.slice(0, operation.position)}${operation.content}${currentContent.slice(operation.position)}`;
121
+ } else if (operation.type === 'delete') {
122
+ currentContent = `${currentContent.slice(0, operation.position)}${currentContent.slice(operation.position + operation.length)}`;
123
+ }
124
+ }
125
+
126
+ state[filePath] = currentContent;
127
+ } else {
128
+ if (changeSet.type === 'createFile') {
129
+ state[filePath] = changeSet.content;
130
+ } else if (changeSet.type === 'deleteFile') {
131
+ delete state[filePath];
132
+ }
133
+ }
134
+ }
135
+
136
+ if (hash === targetHash) {
137
+ break;
138
+ }
139
+ }
140
+
141
+ return state;
142
+ }
143
+
144
+ module.exports = getStateByHash;