@garthub/gart-npm 0.1.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.
@@ -0,0 +1,365 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const {extractZipArchive} = require('./archive');
4
+ const {computeFileSha256, maybeComputeFileSha256, listFilesRecursively} = require('./hash');
5
+
6
+ const MANAGED_GITIGNORE_START = '# gart managed start';
7
+ const MANAGED_GITIGNORE_END = '# gart managed end';
8
+
9
+ async function synchronizeManagedSkills(
10
+ config,
11
+ resolvedArtifacts,
12
+ options = {},
13
+ ) {
14
+ const extractor = options.extractor || extractArtifactArchive;
15
+ const previousIndex = readManagedIndex(config.managedSkillsDir);
16
+ fs.mkdirSync(config.buildDir, {recursive: true});
17
+ ensureManagedSkillsDirectory(config.managedSkillsDir);
18
+ const stagingRoot = fs.mkdtempSync(path.join(config.buildDir, 'reconcile-'));
19
+
20
+ try {
21
+ const stagedArtifacts = [];
22
+ const desiredFiles = new Map();
23
+
24
+ for (const artifact of resolvedArtifacts) {
25
+ const stagedArtifact = await stageArtifactFiles(artifact, stagingRoot, extractor);
26
+ stagedArtifacts.push(stagedArtifact);
27
+
28
+ for (const file of stagedArtifact.files) {
29
+ if (desiredFiles.has(file.relativePath)) {
30
+ const existing = desiredFiles.get(file.relativePath);
31
+ throw new Error(
32
+ `Duplicate managed skill path detected: ${file.relativePath} from ${existing.coordinates} and ${file.coordinates}.`,
33
+ );
34
+ }
35
+
36
+ desiredFiles.set(file.relativePath, file);
37
+ }
38
+ }
39
+
40
+ validateManagedSkillChanges(config, desiredFiles, previousIndex.managedFiles || []);
41
+ const removedFiles = removeStaleManagedFiles(config, desiredFiles, previousIndex.managedFiles || []);
42
+ const writtenFiles = writeManagedFiles(config, desiredFiles);
43
+ const managedFiles = buildManagedFilesSnapshot(config, desiredFiles);
44
+ const gitignoreEntries = updateManagedGitignore(config.managedSkillsDir, managedFiles);
45
+
46
+ return {
47
+ resolvedArtifacts: stagedArtifacts.map((artifact) => ({
48
+ ...artifact,
49
+ files: artifact.files.map((file) => ({
50
+ relativePath: file.relativePath,
51
+ sha256: file.sha256,
52
+ })),
53
+ })),
54
+ managedFiles,
55
+ gitignoreEntries,
56
+ removedFiles,
57
+ writtenFiles,
58
+ };
59
+ } finally {
60
+ fs.rmSync(stagingRoot, {recursive: true, force: true});
61
+ }
62
+ }
63
+
64
+ function cleanupManagedSkills(config) {
65
+ const previousIndex = readManagedIndex(config.managedSkillsDir);
66
+ const desiredFiles = new Map();
67
+
68
+ validateManagedSkillChanges(config, desiredFiles, previousIndex.managedFiles || []);
69
+ const removedFiles = removeStaleManagedFiles(config, desiredFiles, previousIndex.managedFiles || []);
70
+ const managedFiles = buildManagedFilesSnapshot(config, desiredFiles);
71
+ const gitignoreEntries = updateManagedGitignore(config.managedSkillsDir, managedFiles);
72
+
73
+ return {
74
+ managedFiles,
75
+ gitignoreEntries,
76
+ removedFiles,
77
+ previousManagedFiles: previousIndex.managedFiles || [],
78
+ };
79
+ }
80
+
81
+ async function stageArtifactFiles(
82
+ artifact,
83
+ stagingRoot,
84
+ extractor,
85
+ ) {
86
+ const artifactDir = path.join(stagingRoot, sanitizeArtifactKey(artifact.dependency.coordinates));
87
+ await extractor(artifact, artifactDir);
88
+
89
+ const files = listFilesRecursively(artifactDir).map((filePath) => ({
90
+ relativePath: normalizeRelativePath(path.relative(artifactDir, filePath)),
91
+ absolutePath: filePath,
92
+ sha256: computeFileSha256(filePath),
93
+ coordinates: artifact.dependency.coordinates,
94
+ }));
95
+
96
+ return {
97
+ ...artifact,
98
+ archiveSha256: computeFileSha256(artifact.artifactPath),
99
+ pomSha256: maybeComputeFileSha256(artifact.pomPath),
100
+ files,
101
+ };
102
+ }
103
+
104
+ async function extractArtifactArchive(
105
+ artifact,
106
+ destinationPath,
107
+ ) {
108
+ extractZipArchive(artifact.artifactPath, destinationPath);
109
+ }
110
+
111
+ function readManagedIndex(managedSkillsDir) {
112
+ ensureManagedSkillsDirectory(managedSkillsDir);
113
+ const indexPath = path.join(managedSkillsDir, 'gart.json');
114
+
115
+ if (!fs.existsSync(indexPath)) {
116
+ return {managedFiles: []};
117
+ }
118
+
119
+ return JSON.parse(fs.readFileSync(indexPath, 'utf8'));
120
+ }
121
+
122
+ function validateManagedSkillChanges(
123
+ config,
124
+ desiredFiles,
125
+ previousManagedFiles,
126
+ ) {
127
+ const previousManagedMap = new Map(previousManagedFiles.map((file) => [file.relativePath, file]));
128
+
129
+ for (const [relativePath] of desiredFiles) {
130
+ const targetPath = resolveManagedTargetPath(config, relativePath);
131
+
132
+ if (!fs.existsSync(targetPath)) {
133
+ continue;
134
+ }
135
+
136
+ const currentSha = computeFileSha256(targetPath);
137
+ const previousManagedFile = previousManagedMap.get(relativePath);
138
+
139
+ if (!previousManagedFile) {
140
+ throw new Error(
141
+ `Managed skill path conflicts with a user-authored file: ${relativePath}.`,
142
+ );
143
+ }
144
+
145
+ if (currentSha !== previousManagedFile.sha256) {
146
+ throw new Error(
147
+ `Managed skill file was modified manually and cannot be overwritten safely: ${relativePath}.`,
148
+ );
149
+ }
150
+ }
151
+
152
+ for (const previousManagedFile of previousManagedFiles) {
153
+ if (desiredFiles.has(previousManagedFile.relativePath)) {
154
+ continue;
155
+ }
156
+
157
+ const targetPath = resolveManagedTargetPath(config, previousManagedFile.relativePath);
158
+
159
+ if (!fs.existsSync(targetPath)) {
160
+ continue;
161
+ }
162
+
163
+ const currentSha = computeFileSha256(targetPath);
164
+ if (currentSha !== previousManagedFile.sha256) {
165
+ throw new Error(
166
+ `Managed skill file was modified manually and cannot be removed safely: ${previousManagedFile.relativePath}.`,
167
+ );
168
+ }
169
+ }
170
+ }
171
+
172
+ function removeStaleManagedFiles(
173
+ config,
174
+ desiredFiles,
175
+ previousManagedFiles,
176
+ ) {
177
+ const removedFiles = [];
178
+
179
+ for (const previousManagedFile of previousManagedFiles) {
180
+ if (desiredFiles.has(previousManagedFile.relativePath)) {
181
+ continue;
182
+ }
183
+
184
+ const targetPath = resolveManagedTargetPath(config, previousManagedFile.relativePath);
185
+ if (!fs.existsSync(targetPath)) {
186
+ continue;
187
+ }
188
+
189
+ fs.unlinkSync(targetPath);
190
+ removeEmptyParentDirectories(path.dirname(targetPath), config.managedSkillsDir);
191
+ removedFiles.push(previousManagedFile.relativePath);
192
+ }
193
+
194
+ return removedFiles;
195
+ }
196
+
197
+ function writeManagedFiles(
198
+ config,
199
+ desiredFiles,
200
+ ) {
201
+ const writtenFiles = [];
202
+
203
+ for (const file of desiredFiles.values()) {
204
+ const targetPath = resolveManagedTargetPath(config, file.relativePath);
205
+ fs.mkdirSync(path.dirname(targetPath), {recursive: true});
206
+ fs.copyFileSync(file.absolutePath, targetPath);
207
+ writtenFiles.push(file.relativePath);
208
+ }
209
+
210
+ return writtenFiles;
211
+ }
212
+
213
+ function buildManagedFilesSnapshot(
214
+ config,
215
+ desiredFiles,
216
+ ) {
217
+ return [...desiredFiles.values()].map((file) => ({
218
+ relativePath: file.relativePath,
219
+ sha256: computeFileSha256(resolveManagedTargetPath(config, file.relativePath)),
220
+ coordinates: file.coordinates,
221
+ }));
222
+ }
223
+
224
+ function updateManagedGitignore(
225
+ managedSkillsDir,
226
+ managedFiles,
227
+ ) {
228
+ const gitignorePath = path.join(managedSkillsDir, '.gitignore');
229
+ const existingContent = fs.existsSync(gitignorePath)
230
+ ? fs.readFileSync(gitignorePath, 'utf8')
231
+ : '';
232
+ const preservedContent = stripManagedGitignoreBlock(existingContent);
233
+ const managedEntries = buildManagedGitignoreEntries(managedFiles);
234
+ const nextContent = buildGitignoreContent(preservedContent, managedEntries);
235
+
236
+ if (nextContent === null) {
237
+ if (fs.existsSync(gitignorePath)) {
238
+ fs.unlinkSync(gitignorePath);
239
+ }
240
+ return [];
241
+ }
242
+
243
+ fs.writeFileSync(gitignorePath, nextContent);
244
+ return managedEntries;
245
+ }
246
+
247
+ function buildManagedGitignoreEntries(managedFiles) {
248
+ const entries = new Set();
249
+
250
+ for (const file of managedFiles) {
251
+ const segments = file.relativePath.split('/').filter(Boolean);
252
+ if (segments.length === 0) {
253
+ continue;
254
+ }
255
+
256
+ const firstSegment = segments[0];
257
+ if (firstSegment === '.gitignore' || firstSegment === 'gart.json') {
258
+ continue;
259
+ }
260
+
261
+ if (segments.length === 1) {
262
+ entries.add(`/${firstSegment}`);
263
+ continue;
264
+ }
265
+
266
+ entries.add(`/${firstSegment}/`);
267
+ }
268
+
269
+ return [...entries].sort();
270
+ }
271
+
272
+ function buildGitignoreContent(
273
+ existingContent,
274
+ managedEntries,
275
+ ) {
276
+ const normalizedExisting = normalizeGitignoreContent(existingContent);
277
+ const trimmedExisting = normalizedExisting.trim();
278
+
279
+ if (managedEntries.length === 0) {
280
+ if (trimmedExisting.length === 0) {
281
+ return null;
282
+ }
283
+
284
+ return `${trimmedExisting}\n`;
285
+ }
286
+
287
+ const managedBlock = [
288
+ MANAGED_GITIGNORE_START,
289
+ ...managedEntries,
290
+ MANAGED_GITIGNORE_END,
291
+ ].join('\n');
292
+
293
+ if (trimmedExisting.length === 0) {
294
+ return `${managedBlock}\n`;
295
+ }
296
+
297
+ return `${trimmedExisting}\n\n${managedBlock}\n`;
298
+ }
299
+
300
+ function stripManagedGitignoreBlock(content) {
301
+ const normalized = normalizeGitignoreContent(content);
302
+ const blockPattern = new RegExp(
303
+ `${escapeForRegex(MANAGED_GITIGNORE_START)}\\n[\\s\\S]*?${escapeForRegex(MANAGED_GITIGNORE_END)}\\n?`,
304
+ 'g',
305
+ );
306
+
307
+ return normalized.replace(blockPattern, '').replace(/\n{3,}/g, '\n\n').replace(/^\n+|\n+$/g, '');
308
+ }
309
+
310
+ function normalizeGitignoreContent(content) {
311
+ return String(content || '').replace(/\r\n/g, '\n');
312
+ }
313
+
314
+ function escapeForRegex(value) {
315
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
316
+ }
317
+
318
+ function resolveManagedTargetPath(
319
+ config,
320
+ relativePath,
321
+ ) {
322
+ return path.join(config.managedSkillsDir, ...relativePath.split('/'));
323
+ }
324
+
325
+ function normalizeRelativePath(relativePath) {
326
+ return relativePath.split(path.sep).join('/');
327
+ }
328
+
329
+ function sanitizeArtifactKey(value) {
330
+ return value.replace(/[^a-zA-Z0-9._-]+/g, '_');
331
+ }
332
+
333
+ function ensureManagedSkillsDirectory(managedSkillsDir) {
334
+ fs.mkdirSync(managedSkillsDir, {recursive: true});
335
+ }
336
+
337
+ function removeEmptyParentDirectories(
338
+ startDir,
339
+ boundaryDir,
340
+ ) {
341
+ let currentDir = startDir;
342
+ const normalizedBoundary = path.resolve(boundaryDir);
343
+
344
+ while (path.resolve(currentDir).startsWith(normalizedBoundary) && path.resolve(currentDir) !== normalizedBoundary) {
345
+ if (fs.readdirSync(currentDir).length > 0) {
346
+ return;
347
+ }
348
+
349
+ fs.rmdirSync(currentDir);
350
+ currentDir = path.dirname(currentDir);
351
+ }
352
+ }
353
+
354
+ module.exports = {
355
+ cleanupManagedSkills,
356
+ synchronizeManagedSkills,
357
+ readManagedIndex,
358
+ validateManagedSkillChanges,
359
+ removeStaleManagedFiles,
360
+ updateManagedGitignore,
361
+ buildManagedGitignoreEntries,
362
+ stripManagedGitignoreBlock,
363
+ MANAGED_GITIGNORE_START,
364
+ MANAGED_GITIGNORE_END,
365
+ };
package/src/maven.js ADDED
@@ -0,0 +1,155 @@
1
+ const os = require('node:os');
2
+ const path = require('node:path');
3
+
4
+ const DEFAULT_CLASSIFIER = 'skills';
5
+ const DEFAULT_EXTENSION = 'zip';
6
+ const DEFAULT_POM_EXTENSION = 'pom';
7
+
8
+ function defaultMavenLocalRepository() {
9
+ return path.join(os.homedir(), '.m2', 'repository');
10
+ }
11
+
12
+ function parseSkillCoordinates(coordinates) {
13
+ const parts = String(coordinates).split(':');
14
+
15
+ if (parts.length !== 3 || parts.some((part) => !part)) {
16
+ throw new Error(`Invalid skill coordinates: ${coordinates}`);
17
+ }
18
+
19
+ return {
20
+ coordinates,
21
+ groupId: parts[0],
22
+ artifactId: parts[1],
23
+ version: parts[2],
24
+ classifier: DEFAULT_CLASSIFIER,
25
+ extension: DEFAULT_EXTENSION,
26
+ };
27
+ }
28
+
29
+ function buildMavenRelativeArtifactPath(coordinates) {
30
+ const parsed = typeof coordinates === 'string' ? parseSkillCoordinates(coordinates) : coordinates;
31
+ const groupPath = parsed.groupId.split('.').join('/');
32
+ const fileName = `${parsed.artifactId}-${parsed.version}-${parsed.classifier}.${parsed.extension}`;
33
+
34
+ return `${groupPath}/${parsed.artifactId}/${parsed.version}/${fileName}`;
35
+ }
36
+
37
+ function buildMavenRelativePomPath(coordinates) {
38
+ const parsed = typeof coordinates === 'string' ? parseSkillCoordinates(coordinates) : coordinates;
39
+ const groupPath = parsed.groupId.split('.').join('/');
40
+ const fileName = `${parsed.artifactId}-${parsed.version}.${DEFAULT_POM_EXTENSION}`;
41
+
42
+ return `${groupPath}/${parsed.artifactId}/${parsed.version}/${fileName}`;
43
+ }
44
+
45
+ function buildMavenArtifactUrl(
46
+ repository,
47
+ coordinates,
48
+ ) {
49
+ const baseUrl = repository.url.replace(/\/+$/, '');
50
+ return `${baseUrl}/${buildMavenRelativeArtifactPath(coordinates)}`;
51
+ }
52
+
53
+ function buildMavenPomUrl(
54
+ repository,
55
+ coordinates,
56
+ ) {
57
+ const baseUrl = repository.url.replace(/\/+$/, '');
58
+ return `${baseUrl}/${buildMavenRelativePomPath(coordinates)}`;
59
+ }
60
+
61
+ function normalizeRepository(
62
+ repository,
63
+ cwd,
64
+ id,
65
+ ) {
66
+ if (typeof repository === 'string') {
67
+ if (repository === 'mavenLocal') {
68
+ return {
69
+ id,
70
+ type: 'mavenLocal',
71
+ rootDir: defaultMavenLocalRepository(),
72
+ };
73
+ }
74
+
75
+ return {
76
+ id,
77
+ type: 'maven',
78
+ url: repository,
79
+ };
80
+ }
81
+
82
+ if (!repository || typeof repository !== 'object') {
83
+ throw new Error('Repository entries must be strings or objects');
84
+ }
85
+
86
+ if (repository.type === 'mavenLocal') {
87
+ return {
88
+ id,
89
+ type: 'mavenLocal',
90
+ rootDir: repository.rootDir
91
+ ? path.resolve(cwd, repository.rootDir)
92
+ : defaultMavenLocalRepository(),
93
+ };
94
+ }
95
+
96
+ if (repository.type === 'maven') {
97
+ if (!repository.url || typeof repository.url !== 'string') {
98
+ throw new Error('Maven repositories must define a string url');
99
+ }
100
+
101
+ return {
102
+ id,
103
+ type: 'maven',
104
+ url: repository.url,
105
+ credentials: repository.credentials || null,
106
+ };
107
+ }
108
+
109
+ if (repository.type === 'npm') {
110
+ if (!repository.url || typeof repository.url !== 'string') {
111
+ throw new Error('Npm repositories must define a string url');
112
+ }
113
+
114
+ return {
115
+ id,
116
+ type: 'npm',
117
+ url: repository.url,
118
+ authTokenEnv: repository.authTokenEnv || null,
119
+ };
120
+ }
121
+
122
+ throw new Error(`Unsupported repository type: ${repository.type}`);
123
+ }
124
+
125
+ function buildRepositoryHeaders(repository) {
126
+ if (!repository.credentials) {
127
+ return {};
128
+ }
129
+
130
+ const username = repository.credentials.username || process.env[repository.credentials.usernameEnv || ''];
131
+ const password = repository.credentials.password || process.env[repository.credentials.passwordEnv || ''];
132
+
133
+ if (!username && !password) {
134
+ return {};
135
+ }
136
+
137
+ const token = Buffer.from(`${username || ''}:${password || ''}`).toString('base64');
138
+ return {
139
+ Authorization: `Basic ${token}`,
140
+ };
141
+ }
142
+
143
+ module.exports = {
144
+ parseSkillCoordinates,
145
+ buildMavenRelativeArtifactPath,
146
+ buildMavenRelativePomPath,
147
+ buildMavenArtifactUrl,
148
+ buildMavenPomUrl,
149
+ normalizeRepository,
150
+ buildRepositoryHeaders,
151
+ defaultMavenLocalRepository,
152
+ DEFAULT_CLASSIFIER,
153
+ DEFAULT_EXTENSION,
154
+ DEFAULT_POM_EXTENSION,
155
+ };
package/src/pom.js ADDED
@@ -0,0 +1,64 @@
1
+ function parseTransitiveSkillDependenciesFromPom(pomContent) {
2
+ const dependencies = [];
3
+ const dependencyBlocks = String(pomContent).match(/<dependency\b[\s\S]*?<\/dependency>/g) || [];
4
+
5
+ for (const block of dependencyBlocks) {
6
+ const groupId = readPomTag(block, 'groupId');
7
+ const artifactId = readPomTag(block, 'artifactId');
8
+ const version = readPomTag(block, 'version');
9
+ const classifier = readPomTag(block, 'classifier');
10
+ const type = readPomTag(block, 'type');
11
+ const optional = readPomTag(block, 'optional');
12
+ const scope = readPomTag(block, 'scope');
13
+
14
+ if (optional === 'true') {
15
+ continue;
16
+ }
17
+
18
+ if (classifier !== 'skills' || type !== 'zip') {
19
+ continue;
20
+ }
21
+
22
+ if (!groupId || !artifactId || !version) {
23
+ throw new Error('Encountered a transitive skill dependency in POM without groupId, artifactId, or version.');
24
+ }
25
+
26
+ dependencies.push({
27
+ coordinates: `${groupId}:${artifactId}:${version}`,
28
+ groupId,
29
+ artifactId,
30
+ version,
31
+ classifier,
32
+ type,
33
+ scope: scope || null,
34
+ });
35
+ }
36
+
37
+ return dependencies;
38
+ }
39
+
40
+ function readPomTag(
41
+ xml,
42
+ tagName,
43
+ ) {
44
+ const match = String(xml).match(new RegExp(`<${tagName}>\\s*([^<]+?)\\s*</${tagName}>`));
45
+
46
+ if (!match) {
47
+ return null;
48
+ }
49
+
50
+ return decodeXmlEntities(match[1].trim());
51
+ }
52
+
53
+ function decodeXmlEntities(value) {
54
+ return value
55
+ .replace(/&lt;/g, '<')
56
+ .replace(/&gt;/g, '>')
57
+ .replace(/&amp;/g, '&')
58
+ .replace(/&quot;/g, '"')
59
+ .replace(/&apos;/g, '\'');
60
+ }
61
+
62
+ module.exports = {
63
+ parseTransitiveSkillDependenciesFromPom,
64
+ };