pagan-artifact 0.2.5
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/.nvmrc +1 -0
- package/README.md +13 -0
- package/bin/art.js +224 -0
- package/branching/index.js +234 -0
- package/caches/index.js +223 -0
- package/changes/index.js +109 -0
- package/contributions/index.js +239 -0
- package/index.js +64 -0
- package/package.json +26 -0
- package/setup/index.js +271 -0
- package/utils/getStateByHash/index.js +67 -0
- package/workflow/index.js +228 -0
package/changes/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* art - Modern version control.
|
|
3
|
+
* Module: Changes (v0.2.5)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const getStateByHash = require('../utils/getStateByHash');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Iterates through the JSON commit files in the active branch folder.
|
|
13
|
+
* @returns {string} A formatted string of commit history.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function log () {
|
|
17
|
+
const artPath = path.join(process.cwd(), '.art');
|
|
18
|
+
const artJsonPath = path.join(artPath, 'art.json');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(artJsonPath)) {
|
|
21
|
+
throw new Error('No art repository found.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const artJson = JSON.parse(fs.readFileSync(artJsonPath, 'utf8'));
|
|
25
|
+
const branch = artJson.active.branch;
|
|
26
|
+
const branchPath = path.join(artPath, 'history/local', branch);
|
|
27
|
+
const manifestPath = path.join(branchPath, 'manifest.json');
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(manifestPath)) {
|
|
30
|
+
return 'No commits found.';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
34
|
+
|
|
35
|
+
let output = `Branch: ${branch}\n\n`;
|
|
36
|
+
|
|
37
|
+
// Display commits in reverse chronological order
|
|
38
|
+
|
|
39
|
+
for (let i = manifest.commits.length - 1; i >= 0; i--) {
|
|
40
|
+
const hash = manifest.commits[i];
|
|
41
|
+
const commitData = JSON.parse(fs.readFileSync(path.join(branchPath, `${hash}.json`), 'utf8'));
|
|
42
|
+
|
|
43
|
+
output += `commit ${commitData.hash}\n`;
|
|
44
|
+
output += `Date: ${new Date(commitData.timestamp).toLocaleString()}\n`;
|
|
45
|
+
output += `\n ${commitData.message}\n\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return output;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Displays line-by-line differences between working directory and the last commit/stage.
|
|
53
|
+
* @returns {string} Formatted diff output.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
function diff () {
|
|
57
|
+
const root = process.cwd();
|
|
58
|
+
const artPath = path.join(root, '.art');
|
|
59
|
+
const artJson = JSON.parse(fs.readFileSync(path.join(artPath, 'art.json'), 'utf8'));
|
|
60
|
+
|
|
61
|
+
const activeBranch = artJson.active.branch;
|
|
62
|
+
const lastCommitHash = artJson.active.parent;
|
|
63
|
+
const lastCommitState = lastCommitHash ? getStateByHash(activeBranch, lastCommitHash) : {};
|
|
64
|
+
|
|
65
|
+
const currentFiles = fs.readdirSync(root, { recursive: true })
|
|
66
|
+
.filter(f => !f.startsWith('.art') && !fs.statSync(path.join(root, f)).isDirectory());
|
|
67
|
+
|
|
68
|
+
const fileDiffs = [];
|
|
69
|
+
|
|
70
|
+
for (const filePath of currentFiles) {
|
|
71
|
+
const fullPath = path.join(root, filePath);
|
|
72
|
+
const currentContent = fs.readFileSync(fullPath, 'utf8');
|
|
73
|
+
const previousContent = lastCommitState[filePath] || '';
|
|
74
|
+
|
|
75
|
+
if (currentContent !== previousContent) {
|
|
76
|
+
let start = 0;
|
|
77
|
+
|
|
78
|
+
while (start < previousContent.length && start < currentContent.length && previousContent[start] === currentContent[start]) {
|
|
79
|
+
start++;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let oldEnd = previousContent.length - 1;
|
|
83
|
+
let newEnd = currentContent.length - 1;
|
|
84
|
+
|
|
85
|
+
while (oldEnd >= start && newEnd >= start && previousContent[oldEnd] === currentContent[newEnd]) {
|
|
86
|
+
oldEnd--;
|
|
87
|
+
newEnd--;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fileDiffs.push({
|
|
91
|
+
file: filePath,
|
|
92
|
+
deleted: previousContent.slice(start, oldEnd + 1),
|
|
93
|
+
added: currentContent.slice(start, newEnd + 1)
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const stagePath = path.join(artPath, 'stage.json');
|
|
99
|
+
const staged = fs.existsSync(stagePath) ? Object.keys(JSON.parse(fs.readFileSync(stagePath, 'utf8')).changes) : [];
|
|
100
|
+
|
|
101
|
+
return { fileDiffs, staged };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
__libraryVersion: '0.2.5',
|
|
106
|
+
__libraryAPIName: 'Changes',
|
|
107
|
+
log,
|
|
108
|
+
diff
|
|
109
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* art - Modern version control.
|
|
3
|
+
* Module: Contributions (v0.2.5)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const pkg = require('../package.json');
|
|
10
|
+
const { checkout } = require('../branching/index.js');
|
|
11
|
+
|
|
12
|
+
const ARTIFACT_HOST = pkg.artConfig.host || 'http://localhost:1337';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configures the single URL endpoint in art.json for synchronization.
|
|
16
|
+
* Supports full URLs or "handle/repo" slugs.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
function remote (input) {
|
|
20
|
+
const artPath = path.join(process.cwd(), '.art', 'art.json');
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(artPath)) {
|
|
23
|
+
throw new Error('No art repository found.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const artJson = JSON.parse(fs.readFileSync(artPath, 'utf8'));
|
|
27
|
+
|
|
28
|
+
if (input) {
|
|
29
|
+
let finalUrl = input;
|
|
30
|
+
|
|
31
|
+
if (input.includes('/') && !input.startsWith('http')) {
|
|
32
|
+
finalUrl = `${ARTIFACT_HOST}/${input}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
artJson.remote = finalUrl;
|
|
36
|
+
fs.writeFileSync(artPath, JSON.stringify(artJson, null, 2));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return artJson.remote;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Downloads JSON diff files from the remote server via POST.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
async function fetchRemote () {
|
|
47
|
+
const artPath = path.join(process.cwd(), '.art');
|
|
48
|
+
const artJson = JSON.parse(fs.readFileSync(path.join(artPath, 'art.json'), 'utf8'));
|
|
49
|
+
|
|
50
|
+
if (!artJson.remote) {
|
|
51
|
+
throw new Error('Remote URL not configured. Use "art remote <handle>/<repo>".');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const branch = artJson.active.branch;
|
|
55
|
+
const token = artJson.configuration.personalAccessToken;
|
|
56
|
+
|
|
57
|
+
const remoteParts = artJson.remote.split('/');
|
|
58
|
+
const repo = remoteParts.pop();
|
|
59
|
+
const handle = remoteParts.pop();
|
|
60
|
+
|
|
61
|
+
const remoteBranchPath = path.join(artPath, 'history/remote', branch);
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(remoteBranchPath)) {
|
|
64
|
+
fs.mkdirSync(remoteBranchPath, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const response = await fetch(`${ARTIFACT_HOST}/manifest`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
type: 'history',
|
|
72
|
+
handle,
|
|
73
|
+
repo,
|
|
74
|
+
branch,
|
|
75
|
+
|
|
76
|
+
...(token && { personalAccessToken: token })
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const remoteManifest = await response.json();
|
|
81
|
+
|
|
82
|
+
for (const commitHash of remoteManifest.commits) {
|
|
83
|
+
const commitFilePath = path.join(remoteBranchPath, `${commitHash}.json`);
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(commitFilePath)) {
|
|
86
|
+
const commitResponse = await fetch(`${ARTIFACT_HOST}/commit`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
handle,
|
|
91
|
+
repo,
|
|
92
|
+
branch,
|
|
93
|
+
hash: commitHash,
|
|
94
|
+
|
|
95
|
+
...(token && { personalAccessToken: token })
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const commitDiff = await commitResponse.json();
|
|
100
|
+
|
|
101
|
+
fs.writeFileSync(commitFilePath, JSON.stringify(commitDiff, null, 2));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
path.join(remoteBranchPath, 'manifest.json'),
|
|
107
|
+
JSON.stringify(remoteManifest, null, 2)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return `Fetched remote history for branch: ${branch}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Performs a fetch and applies remote JSON diffs to local branch and files.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
async function pull () {
|
|
118
|
+
const artPath = path.join(process.cwd(), '.art');
|
|
119
|
+
const artJson = JSON.parse(fs.readFileSync(path.join(artPath, 'art.json'), 'utf8'));
|
|
120
|
+
const branch = artJson.active.branch;
|
|
121
|
+
|
|
122
|
+
await fetchRemote();
|
|
123
|
+
|
|
124
|
+
const remoteManifestPath = path.join(artPath, 'history/remote', branch, 'manifest.json');
|
|
125
|
+
const remoteManifest = JSON.parse(fs.readFileSync(remoteManifestPath, 'utf8'));
|
|
126
|
+
|
|
127
|
+
const localManifestPath = path.join(artPath, 'history/local', branch, 'manifest.json');
|
|
128
|
+
const localManifest = JSON.parse(fs.readFileSync(localManifestPath, 'utf8'));
|
|
129
|
+
|
|
130
|
+
const newCommits = remoteManifest.commits.filter(hash => !localManifest.commits.includes(hash));
|
|
131
|
+
|
|
132
|
+
if (newCommits.length === 0) {
|
|
133
|
+
return 'Already up to date.';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const commitHash of newCommits) {
|
|
137
|
+
const remoteCommitFile = path.join(artPath, 'history/remote', branch, `${commitHash}.json`);
|
|
138
|
+
const remoteData = fs.readFileSync(remoteCommitFile, 'utf8');
|
|
139
|
+
|
|
140
|
+
fs.writeFileSync(
|
|
141
|
+
path.join(artPath, 'history/local', branch, `${commitHash}.json`),
|
|
142
|
+
remoteData
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
localManifest.commits.push(commitHash);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fs.writeFileSync(localManifestPath, JSON.stringify(localManifest, null, 2));
|
|
149
|
+
checkout(branch);
|
|
150
|
+
|
|
151
|
+
return `Applied ${newCommits.length} commits.`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Uploads local JSON diffs that do not exist in the remote history.
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
async function push () {
|
|
159
|
+
const artPath = path.join(process.cwd(), '.art');
|
|
160
|
+
const artJson = JSON.parse(fs.readFileSync(path.join(artPath, 'art.json'), 'utf8'));
|
|
161
|
+
|
|
162
|
+
if (!artJson.remote) {
|
|
163
|
+
throw new Error('Remote URL not configured.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const branch = artJson.active.branch;
|
|
167
|
+
const token = artJson.configuration.personalAccessToken;
|
|
168
|
+
const remoteParts = artJson.remote.split('/');
|
|
169
|
+
const repo = remoteParts.pop();
|
|
170
|
+
const handle = remoteParts.pop();
|
|
171
|
+
|
|
172
|
+
const localManifest = JSON.parse(fs.readFileSync(path.join(artPath, 'history/local', branch, 'manifest.json'), 'utf8'));
|
|
173
|
+
|
|
174
|
+
const response = await fetch(`${ARTIFACT_HOST}/manifest`, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
type: 'history',
|
|
179
|
+
handle,
|
|
180
|
+
repo,
|
|
181
|
+
branch,
|
|
182
|
+
|
|
183
|
+
...(token && { personalAccessToken: token })
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const remoteManifest = await response.json();
|
|
188
|
+
const missingCommits = localManifest.commits.filter(hash => !remoteManifest.commits.includes(hash));
|
|
189
|
+
|
|
190
|
+
if (missingCommits.length === 0) {
|
|
191
|
+
return 'Everything up to date.';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let rootData = null;
|
|
195
|
+
|
|
196
|
+
if (remoteManifest.commits.length === 0) {
|
|
197
|
+
const rootManifestPath = path.join(artPath, 'root/manifest.json');
|
|
198
|
+
|
|
199
|
+
if (fs.existsSync(rootManifestPath)) {
|
|
200
|
+
rootData = JSON.parse(fs.readFileSync(rootManifestPath, 'utf8'));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const commitHash of missingCommits) {
|
|
205
|
+
const commitData = JSON.parse(fs.readFileSync(path.join(artPath, 'history/local', branch, `${commitHash}.json`), 'utf8'));
|
|
206
|
+
|
|
207
|
+
await fetch(`${ARTIFACT_HOST}/push`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
handle,
|
|
212
|
+
repo,
|
|
213
|
+
branch,
|
|
214
|
+
commit: commitData,
|
|
215
|
+
|
|
216
|
+
...(rootData && { root: rootData }),
|
|
217
|
+
|
|
218
|
+
...(token && { personalAccessToken: token })
|
|
219
|
+
})
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
rootData = null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const remoteManifestPath = path.join(artPath, 'history/remote', branch, 'manifest.json');
|
|
226
|
+
|
|
227
|
+
fs.writeFileSync(remoteManifestPath, JSON.stringify(localManifest, null, 2));
|
|
228
|
+
|
|
229
|
+
return `Pushed ${missingCommits.length} commits to remote.`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
__libraryVersion: pkg.version,
|
|
234
|
+
__libraryAPIName: 'Contributions',
|
|
235
|
+
remote,
|
|
236
|
+
fetch: fetchRemote,
|
|
237
|
+
pull,
|
|
238
|
+
push
|
|
239
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* art - Modern version control.
|
|
3
|
+
* Core Library Entry Point (v0.2.5)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Setup = require('./setup');
|
|
7
|
+
const Workflow = require('./workflow');
|
|
8
|
+
const Branching = require('./branching');
|
|
9
|
+
const Contributions = require('./contributions');
|
|
10
|
+
const Changes = require('./changes');
|
|
11
|
+
const Caches = require('./caches');
|
|
12
|
+
|
|
13
|
+
const art = {
|
|
14
|
+
|
|
15
|
+
// Setup
|
|
16
|
+
|
|
17
|
+
init: Setup.init,
|
|
18
|
+
clone: Setup.clone,
|
|
19
|
+
config: Setup.config,
|
|
20
|
+
|
|
21
|
+
// Workflow
|
|
22
|
+
|
|
23
|
+
status: Workflow.status,
|
|
24
|
+
add: Workflow.add,
|
|
25
|
+
commit: Workflow.commit,
|
|
26
|
+
|
|
27
|
+
// Branching
|
|
28
|
+
|
|
29
|
+
branch: Branching.branch,
|
|
30
|
+
checkout: Branching.checkout,
|
|
31
|
+
merge: Branching.merge,
|
|
32
|
+
|
|
33
|
+
// Contributions
|
|
34
|
+
|
|
35
|
+
remote: Contributions.remote,
|
|
36
|
+
fetch: Contributions.fetch,
|
|
37
|
+
pull: Contributions.pull,
|
|
38
|
+
push: Contributions.push,
|
|
39
|
+
|
|
40
|
+
// Changes
|
|
41
|
+
|
|
42
|
+
log: Changes.log,
|
|
43
|
+
diff: Changes.diff,
|
|
44
|
+
|
|
45
|
+
// Caches
|
|
46
|
+
|
|
47
|
+
stash: Caches.stash,
|
|
48
|
+
reset: Caches.reset,
|
|
49
|
+
rm: Caches.rm,
|
|
50
|
+
|
|
51
|
+
// Metadata
|
|
52
|
+
|
|
53
|
+
version: '0.2.5',
|
|
54
|
+
modules: [
|
|
55
|
+
Setup.__libraryAPIName,
|
|
56
|
+
Workflow.__libraryAPIName,
|
|
57
|
+
Branching.__libraryAPIName,
|
|
58
|
+
Contributions.__libraryAPIName,
|
|
59
|
+
Changes.__libraryAPIName,
|
|
60
|
+
Caches.__libraryAPIName
|
|
61
|
+
]
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
module.exports = art;
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pagan-artifact",
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"description": "Modern version control.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"art": "./bin/art.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"vcs",
|
|
14
|
+
"version-control",
|
|
15
|
+
"git-alternative",
|
|
16
|
+
"json-diff"
|
|
17
|
+
],
|
|
18
|
+
"author": "bennyschmidt",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24.11.0"
|
|
22
|
+
},
|
|
23
|
+
"artConfig": {
|
|
24
|
+
"host": "https://www.paganartifact.com/artifact"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/setup/index.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* art - Modern version control.
|
|
3
|
+
* Module: Setup (v0.2.5)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const pkg = require('../package.json');
|
|
10
|
+
const { remote } = require('../contributions');
|
|
11
|
+
|
|
12
|
+
const ARTIFACT_HOST = pkg.artConfig.host || 'http://localhost:1337';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initializes the local .art directory structure.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
function init (directoryPath = process.cwd()) {
|
|
19
|
+
const artDirectory = path.join(directoryPath, '.art');
|
|
20
|
+
|
|
21
|
+
const folders = [
|
|
22
|
+
'',
|
|
23
|
+
'root',
|
|
24
|
+
'history',
|
|
25
|
+
'history/local',
|
|
26
|
+
'history/local/main',
|
|
27
|
+
'history/remote',
|
|
28
|
+
'history/remote/main'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(artDirectory)) {
|
|
32
|
+
return `Reinitialized existing art repository in ${artDirectory}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const folder of folders) {
|
|
36
|
+
const fullPath = path.join(artDirectory, folder);
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(fullPath)) {
|
|
39
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const files = fs.readdirSync(directoryPath).filter(f => f !== '.art');
|
|
44
|
+
const rootManifest = { files: [] };
|
|
45
|
+
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const fullPath = path.join(directoryPath, file);
|
|
48
|
+
|
|
49
|
+
if (fs.lstatSync(fullPath).isFile()) {
|
|
50
|
+
rootManifest.files.push({
|
|
51
|
+
path: file,
|
|
52
|
+
content: fs.readFileSync(fullPath, 'utf8')
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
path.join(artDirectory, 'root/manifest.json'),
|
|
59
|
+
JSON.stringify(rootManifest, null, 2)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
fs.writeFileSync(
|
|
63
|
+
path.join(artDirectory, 'history/local/main/manifest.json'),
|
|
64
|
+
JSON.stringify({ commits: [] }, null, 2)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(artDirectory, 'history/remote/main/manifest.json'),
|
|
69
|
+
JSON.stringify({ commits: [] }, null, 2)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const artFile = {
|
|
73
|
+
active: { branch: 'main', parent: null },
|
|
74
|
+
remote: '',
|
|
75
|
+
configuration: { handle: '', personalAccessToken: '' }
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
fs.writeFileSync(
|
|
79
|
+
path.join(artDirectory, 'art.json'),
|
|
80
|
+
JSON.stringify(artFile, null, 2)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return `Initialized empty art repository in ${artDirectory}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clones a repository by fetching manifests and commits via POST.
|
|
88
|
+
* @param {string} repoSlug - The handle/repo identifier.
|
|
89
|
+
* @param {string} providedToken - Optional token for authentication.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
async function clone (repoSlug, providedToken = null) {
|
|
93
|
+
if (!repoSlug || !repoSlug.includes('/')) {
|
|
94
|
+
throw new Error('A valid slug is required (e.g., handle/repo).');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [handle, repo] = repoSlug.split('/');
|
|
98
|
+
const targetPath = path.join(process.cwd(), repo);
|
|
99
|
+
const originalCwd = process.cwd();
|
|
100
|
+
|
|
101
|
+
if (fs.existsSync(targetPath)) {
|
|
102
|
+
throw new Error(`Destination path "${targetPath}" already exists.`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
106
|
+
init(targetPath);
|
|
107
|
+
process.chdir(targetPath);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const artPath = path.join(targetPath, '.art');
|
|
111
|
+
const artJsonPath = path.join(artPath, 'art.json');
|
|
112
|
+
const artJson = JSON.parse(fs.readFileSync(artJsonPath, 'utf8'));
|
|
113
|
+
|
|
114
|
+
if (providedToken) {
|
|
115
|
+
artJson.configuration.personalAccessToken = providedToken;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
artJson.remote = `${ARTIFACT_HOST}/${handle}/${repo}`;
|
|
119
|
+
|
|
120
|
+
fs.writeFileSync(artJsonPath, JSON.stringify(artJson, null, 2));
|
|
121
|
+
|
|
122
|
+
const token = artJson.configuration.personalAccessToken;
|
|
123
|
+
|
|
124
|
+
const rootRes = await fetch(`${ARTIFACT_HOST}/manifest`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
type: 'root',
|
|
129
|
+
handle,
|
|
130
|
+
repo,
|
|
131
|
+
branch: 'main',
|
|
132
|
+
|
|
133
|
+
...(token && { personalAccessToken: token })
|
|
134
|
+
})
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!rootRes.ok) {
|
|
138
|
+
throw new Error(`Failed to fetch root: ${rootRes.statusText}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const rootManifest = await rootRes.json();
|
|
142
|
+
|
|
143
|
+
if (rootManifest.files) {
|
|
144
|
+
fs.writeFileSync(
|
|
145
|
+
path.join(artPath, 'root/manifest.json'),
|
|
146
|
+
JSON.stringify(rootManifest, null, 2)
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
for (const file of rootManifest.files) {
|
|
150
|
+
const workingPath = path.join(targetPath, file.path);
|
|
151
|
+
|
|
152
|
+
fs.mkdirSync(path.dirname(workingPath), { recursive: true });
|
|
153
|
+
fs.writeFileSync(workingPath, file.content);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const historyRes = await fetch(`${ARTIFACT_HOST}/manifest`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
type: 'history',
|
|
162
|
+
handle,
|
|
163
|
+
repo,
|
|
164
|
+
branch: 'main',
|
|
165
|
+
|
|
166
|
+
...(token && { personalAccessToken: token })
|
|
167
|
+
})
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const historyManifest = await historyRes.json();
|
|
171
|
+
const localManifest = { commits: [] };
|
|
172
|
+
const localHistoryDir = path.join(artPath, 'history/local/main');
|
|
173
|
+
const remoteHistoryDir = path.join(artPath, 'history/remote/main');
|
|
174
|
+
|
|
175
|
+
if (historyManifest.commits) {
|
|
176
|
+
for (const commitHash of historyManifest.commits) {
|
|
177
|
+
const commitRes = await fetch(`${ARTIFACT_HOST}/commit`, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
handle,
|
|
182
|
+
repo,
|
|
183
|
+
branch: 'main',
|
|
184
|
+
hash: commitHash,
|
|
185
|
+
|
|
186
|
+
...(token && { personalAccessToken: token })
|
|
187
|
+
})
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const commitDiff = await commitRes.json();
|
|
191
|
+
|
|
192
|
+
for (const filePath of Object.keys(commitDiff.changes)) {
|
|
193
|
+
const fullPath = path.join(targetPath, filePath);
|
|
194
|
+
const changeSet = commitDiff.changes[filePath];
|
|
195
|
+
|
|
196
|
+
if (Array.isArray(changeSet)) {
|
|
197
|
+
let content = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf8') : '';
|
|
198
|
+
|
|
199
|
+
for (const operation of changeSet) {
|
|
200
|
+
if (operation.type === 'insert') {
|
|
201
|
+
content = `${content.slice(0, operation.position)}${operation.content}${content.slice(operation.position)}`;
|
|
202
|
+
} else if (operation.type === 'delete') {
|
|
203
|
+
content = `${content.slice(0, operation.position)}${content.slice(operation.position + operation.length)}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fs.writeFileSync(fullPath, content);
|
|
208
|
+
} else if (changeSet.type === 'createFile') {
|
|
209
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
210
|
+
fs.writeFileSync(fullPath, changeSet.content || '');
|
|
211
|
+
} else if (changeSet.type === 'deleteFile' && fs.existsSync(fullPath)) {
|
|
212
|
+
fs.unlinkSync(fullPath);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const commitContent = JSON.stringify(commitDiff, null, 2);
|
|
217
|
+
|
|
218
|
+
fs.writeFileSync(path.join(localHistoryDir, `${commitHash}.json`), commitContent);
|
|
219
|
+
fs.writeFileSync(path.join(remoteHistoryDir, `${commitHash}.json`), commitContent);
|
|
220
|
+
|
|
221
|
+
localManifest.commits.push(commitHash);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fs.writeFileSync(path.join(localHistoryDir, 'manifest.json'), JSON.stringify(localManifest, null, 2));
|
|
226
|
+
fs.writeFileSync(path.join(remoteHistoryDir, 'manifest.json'), JSON.stringify(localManifest, null, 2));
|
|
227
|
+
|
|
228
|
+
const updatedDepJson = JSON.parse(fs.readFileSync(artJsonPath, 'utf8'));
|
|
229
|
+
|
|
230
|
+
if (localManifest.commits.length > 0) {
|
|
231
|
+
updatedDepJson.active.parent = localManifest.commits[localManifest.commits.length - 1];
|
|
232
|
+
fs.writeFileSync(artJsonPath, JSON.stringify(updatedDepJson, null, 2));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return `Successfully cloned and replayed ${repoSlug}.`;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw error;
|
|
238
|
+
} finally {
|
|
239
|
+
process.chdir(originalCwd);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Updates the configuration in art.json.
|
|
245
|
+
*/
|
|
246
|
+
|
|
247
|
+
function config (key, value) {
|
|
248
|
+
const manifestPath = path.join(process.cwd(), '.art', 'art.json');
|
|
249
|
+
|
|
250
|
+
if (!fs.existsSync(manifestPath)) {
|
|
251
|
+
throw new Error('No art repository found.');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
255
|
+
|
|
256
|
+
if (key && value !== undefined) {
|
|
257
|
+
manifest.configuration[key] = value;
|
|
258
|
+
|
|
259
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return manifest.configuration;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
__libraryVersion: pkg.version,
|
|
267
|
+
__libraryAPIName: 'Setup',
|
|
268
|
+
init,
|
|
269
|
+
clone,
|
|
270
|
+
config
|
|
271
|
+
};
|