@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,108 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const {resolveSkillArtifacts} = require('../resolve-artifacts');
4
+ const {synchronizeManagedSkills} = require('../managed-skills');
5
+
6
+ async function runResolve(context) {
7
+ const {config, args, extractor} = context;
8
+ const isPostInstall = args.includes('--postinstall');
9
+ const dependencies = [
10
+ ...config.resolution.dependencies.skills,
11
+ ...config.resolution.dependencies.exposedSkills,
12
+ ];
13
+
14
+ if (dependencies.length === 0) {
15
+ if (!isPostInstall) {
16
+ console.log('[gart] No skill dependencies configured.');
17
+ }
18
+ return;
19
+ }
20
+
21
+ fs.mkdirSync(config.managedSkillsDir, {recursive: true});
22
+ fs.mkdirSync(config.buildDir, {recursive: true});
23
+
24
+ const resolution = await resolveSkillArtifacts(config);
25
+ const indexPath = path.join(config.managedSkillsDir, 'gart.json');
26
+ const index = {
27
+ version: 1,
28
+ generatedAt: new Date().toISOString(),
29
+ status: resolution.unresolved.length === 0 ? 'artifactsResolved' : 'artifactLookupFailed',
30
+ managedDir: config.managedSkillsDir,
31
+ repositories: config.resolution.repositories,
32
+ dependencies: {
33
+ skills: config.resolution.dependencies.skills,
34
+ exposedSkills: config.resolution.dependencies.exposedSkills,
35
+ },
36
+ directDependencies: resolution.directDependencies,
37
+ transitiveDependencies: resolution.transitiveDependencies,
38
+ resolvedArtifacts: resolution.resolvedArtifacts,
39
+ unresolved: resolution.unresolved,
40
+ note: 'Artifact lookup and remote download are implemented. Checksum verification, unpacking, and reconciliation are not implemented yet.',
41
+ };
42
+
43
+ if (resolution.unresolved.length > 0) {
44
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));
45
+ throw new Error(formatUnresolvedMessage(resolution.unresolved));
46
+ }
47
+
48
+ try {
49
+ const synchronization = await synchronizeManagedSkills(config, resolution.resolvedArtifacts, {
50
+ extractor,
51
+ });
52
+ index.status = 'skillsReconciled';
53
+ index.resolvedArtifacts = synchronization.resolvedArtifacts;
54
+ index.managedFiles = synchronization.managedFiles;
55
+ index.removedFiles = synchronization.removedFiles;
56
+ index.writtenFiles = synchronization.writtenFiles;
57
+ index.note = 'Artifact lookup, download, unpacking, and managed skill reconciliation are implemented. Checksum verification is still pending.';
58
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));
59
+ } catch (error) {
60
+ index.status = 'reconciliationFailed';
61
+ index.reconciliationError = error.message;
62
+ index.note = 'Artifact lookup and download succeeded, but managed skill reconciliation failed.';
63
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));
64
+ throw error;
65
+ }
66
+
67
+ console.log(`[gart] Resolved ${resolution.resolvedArtifacts.length} skill artifact(s).`);
68
+ console.log(`[gart] Extracted managed skills into ${config.managedSkillsDir}.`);
69
+ console.log(`[gart] Resolution index written to ${indexPath}.`);
70
+ }
71
+
72
+ function formatUnresolvedMessage(unresolvedEntries) {
73
+ const lines = ['Failed to resolve one or more skill artifacts:'];
74
+
75
+ for (const entry of unresolvedEntries) {
76
+ lines.push(`- ${entry.dependency.coordinates}: ${entry.reason}`);
77
+
78
+ for (const attempt of entry.attempts) {
79
+ const details = [];
80
+
81
+ if (attempt.phase) {
82
+ details.push(`phase=${attempt.phase}`);
83
+ }
84
+
85
+ if (typeof attempt.statusCode === 'number') {
86
+ details.push(`HTTP ${attempt.statusCode}`);
87
+ }
88
+
89
+ if (attempt.source) {
90
+ details.push(`source=${attempt.source}`);
91
+ }
92
+
93
+ if (attempt.error) {
94
+ details.push(`error=${attempt.error}`);
95
+ }
96
+
97
+ const suffix = details.length > 0 ? ` [${details.join(', ')}]` : '';
98
+ lines.push(` * ${attempt.repositoryType} (${attempt.repositoryId}) -> ${attempt.location}${suffix}`);
99
+ }
100
+ }
101
+
102
+ return lines.join('\n');
103
+ }
104
+
105
+ module.exports = {
106
+ runResolve,
107
+ formatUnresolvedMessage,
108
+ };
package/src/config.js ADDED
@@ -0,0 +1,186 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const {defaultMavenLocalRepository, normalizeRepository} = require('./maven');
4
+
5
+ const DEFAULT_CONFIG = {
6
+ srcDirs: ['skills'],
7
+ managedSkillsDir: '.agents/skills',
8
+ buildDir: '.gart',
9
+ resolution: {
10
+ repositories: [],
11
+ dependencies: {
12
+ skills: [],
13
+ exposedSkills: [],
14
+ },
15
+ },
16
+ publishing: {
17
+ repository: null,
18
+ },
19
+ };
20
+
21
+ function loadConfig(cwd) {
22
+ const loaded = loadRawConfig(cwd);
23
+ return normalizeConfig(loaded, cwd);
24
+ }
25
+
26
+ function loadRawConfig(cwd) {
27
+ const configFiles = [
28
+ 'gart.config.js',
29
+ 'gart.config.cjs',
30
+ 'gart.config.json',
31
+ ];
32
+
33
+ for (const fileName of configFiles) {
34
+ const filePath = path.join(cwd, fileName);
35
+
36
+ if (!fs.existsSync(filePath)) {
37
+ continue;
38
+ }
39
+
40
+ if (fileName.endsWith('.json')) {
41
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
42
+ }
43
+
44
+ delete require.cache[require.resolve(filePath)];
45
+ const loaded = require(filePath);
46
+ return loaded && loaded.default ? loaded.default : loaded;
47
+ }
48
+
49
+ const packageJsonPath = path.join(cwd, 'package.json');
50
+ if (!fs.existsSync(packageJsonPath)) {
51
+ return {};
52
+ }
53
+
54
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
55
+ return packageJson.gart || {};
56
+ }
57
+
58
+ function normalizeConfig(
59
+ rawConfig,
60
+ cwd,
61
+ ) {
62
+ const config = {
63
+ ...DEFAULT_CONFIG,
64
+ ...rawConfig,
65
+ resolution: {
66
+ ...DEFAULT_CONFIG.resolution,
67
+ ...(rawConfig.resolution || {}),
68
+ dependencies: {
69
+ ...DEFAULT_CONFIG.resolution.dependencies,
70
+ ...((rawConfig.resolution || {}).dependencies || {}),
71
+ },
72
+ },
73
+ publishing: {
74
+ ...DEFAULT_CONFIG.publishing,
75
+ ...(rawConfig.publishing || {}),
76
+ },
77
+ };
78
+
79
+ const buildDir = path.resolve(cwd, config.buildDir);
80
+ const cacheDir = rawConfig.cacheDir ? path.resolve(cwd, rawConfig.cacheDir) : path.join(buildDir, 'cache');
81
+
82
+ return {
83
+ ...config,
84
+ cwd,
85
+ srcDirs: toArray(config.srcDirs),
86
+ managedSkillsDir: path.resolve(cwd, config.managedSkillsDir),
87
+ buildDir,
88
+ cacheDir,
89
+ resolution: {
90
+ ...config.resolution,
91
+ repositories: normalizeRepositories(config.resolution.repositories, cwd),
92
+ dependencies: {
93
+ skills: normalizeCoordinates(config.resolution.dependencies.skills),
94
+ exposedSkills: normalizeCoordinates(config.resolution.dependencies.exposedSkills),
95
+ },
96
+ },
97
+ publishing: {
98
+ ...config.publishing,
99
+ coordinates: normalizeOptionalCoordinates(config.publishing.coordinates, 'Publishing coordinates'),
100
+ groupId: normalizeOptionalString(config.publishing.groupId, 'Publishing groupId'),
101
+ artifactId: normalizeOptionalString(config.publishing.artifactId, 'Publishing artifactId'),
102
+ version: normalizeOptionalString(config.publishing.version, 'Publishing version'),
103
+ repository: config.publishing.repository
104
+ ? normalizeRepository(config.publishing.repository, cwd, 'publishing')
105
+ : null,
106
+ },
107
+ };
108
+ }
109
+
110
+ function normalizeRepositories(
111
+ value,
112
+ cwd,
113
+ ) {
114
+ return toArray(value).map((
115
+ repository,
116
+ index,
117
+ ) => normalizeRepository(repository, cwd, `resolution-${index + 1}`));
118
+ }
119
+
120
+ function toArray(value) {
121
+ if (!value) {
122
+ return [];
123
+ }
124
+
125
+ return Array.isArray(value) ? value : [value];
126
+ }
127
+
128
+ function normalizeCoordinates(value) {
129
+ return toArray(value).map((entry) => {
130
+ if (typeof entry !== 'string') {
131
+ throw new Error('Skill coordinates must be strings in the form group:artifact:version');
132
+ }
133
+
134
+ const parts = entry.split(':');
135
+ if (parts.length !== 3 || parts.some((part) => !part)) {
136
+ throw new Error(`Invalid skill coordinates: ${entry}`);
137
+ }
138
+
139
+ return entry;
140
+ });
141
+ }
142
+
143
+ function normalizeOptionalCoordinates(
144
+ value,
145
+ label,
146
+ ) {
147
+ if (value == null) {
148
+ return null;
149
+ }
150
+
151
+ if (typeof value !== 'string') {
152
+ throw new Error(`${label} must be a string in the form group:artifact:version`);
153
+ }
154
+
155
+ const parts = value.split(':');
156
+ if (parts.length !== 3 || parts.some((part) => !part)) {
157
+ throw new Error(`${label} must be a string in the form group:artifact:version`);
158
+ }
159
+
160
+ return value;
161
+ }
162
+
163
+ function normalizeOptionalString(
164
+ value,
165
+ label,
166
+ ) {
167
+ if (value == null) {
168
+ return null;
169
+ }
170
+
171
+ if (typeof value !== 'string' || value.trim().length === 0) {
172
+ throw new Error(`${label} must be a non-empty string`);
173
+ }
174
+
175
+ return value;
176
+ }
177
+
178
+ module.exports = {
179
+ loadConfig,
180
+ loadRawConfig,
181
+ normalizeConfig,
182
+ normalizeCoordinates,
183
+ normalizeOptionalCoordinates,
184
+ DEFAULT_CONFIG,
185
+ DEFAULT_MAVEN_LOCAL_REPOSITORY: defaultMavenLocalRepository(),
186
+ };
package/src/hash.js ADDED
@@ -0,0 +1,47 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const crypto = require('node:crypto');
4
+
5
+ function computeFileSha256(filePath) {
6
+ const hash = crypto.createHash('sha256');
7
+ const content = fs.readFileSync(filePath);
8
+ hash.update(content);
9
+ return hash.digest('hex');
10
+ }
11
+
12
+ function maybeComputeFileSha256(filePath) {
13
+ if (!filePath || !fs.existsSync(filePath)) {
14
+ return null;
15
+ }
16
+
17
+ return computeFileSha256(filePath);
18
+ }
19
+
20
+ function listFilesRecursively(rootDir) {
21
+ const files = [];
22
+
23
+ if (!fs.existsSync(rootDir)) {
24
+ return files;
25
+ }
26
+
27
+ for (const entry of fs.readdirSync(rootDir, {withFileTypes: true})) {
28
+ const absolutePath = path.join(rootDir, entry.name);
29
+
30
+ if (entry.isDirectory()) {
31
+ files.push(...listFilesRecursively(absolutePath));
32
+ continue;
33
+ }
34
+
35
+ if (entry.isFile()) {
36
+ files.push(absolutePath);
37
+ }
38
+ }
39
+
40
+ return files;
41
+ }
42
+
43
+ module.exports = {
44
+ computeFileSha256,
45
+ maybeComputeFileSha256,
46
+ listFilesRecursively,
47
+ };
package/src/http.js ADDED
@@ -0,0 +1,146 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const http = require('node:http');
4
+ const https = require('node:https');
5
+
6
+ function downloadToFile(
7
+ url,
8
+ destinationPath,
9
+ options = {},
10
+ ) {
11
+ return requestToFile(url, destinationPath, options, 0);
12
+ }
13
+
14
+ function uploadFile(
15
+ url,
16
+ sourcePath,
17
+ options = {},
18
+ ) {
19
+ return uploadBuffer(url, fs.readFileSync(sourcePath), options);
20
+ }
21
+
22
+ function uploadText(
23
+ url,
24
+ content,
25
+ options = {},
26
+ ) {
27
+ return uploadBuffer(url, Buffer.from(content, 'utf8'), options);
28
+ }
29
+
30
+ function requestToFile(
31
+ url,
32
+ destinationPath,
33
+ options,
34
+ redirectCount,
35
+ ) {
36
+ return new Promise((
37
+ resolve,
38
+ reject,
39
+ ) => {
40
+ const transport = url.startsWith('https://') ? https : http;
41
+ const request = transport.get(url, {headers: options.headers || {}}, (response) => {
42
+ const statusCode = response.statusCode || 0;
43
+
44
+ if (isRedirect(statusCode) && response.headers.location) {
45
+ if (redirectCount >= 5) {
46
+ response.resume();
47
+ resolve({ok: false, statusCode, url, note: 'Too many redirects.'});
48
+ return;
49
+ }
50
+
51
+ const redirectUrl = new URL(response.headers.location, url).toString();
52
+ response.resume();
53
+ resolve(requestToFile(redirectUrl, destinationPath, options, redirectCount + 1));
54
+ return;
55
+ }
56
+
57
+ if (statusCode !== 200) {
58
+ response.resume();
59
+ resolve({ok: false, statusCode, url});
60
+ return;
61
+ }
62
+
63
+ fs.mkdirSync(path.dirname(destinationPath), {recursive: true});
64
+ const output = fs.createWriteStream(destinationPath);
65
+
66
+ response.pipe(output);
67
+
68
+ output.on('finish', () => {
69
+ output.close(() => {
70
+ resolve({ok: true, statusCode, url, destinationPath});
71
+ });
72
+ });
73
+
74
+ output.on('error', (error) => {
75
+ output.destroy();
76
+ safeDelete(destinationPath);
77
+ reject(error);
78
+ });
79
+ });
80
+
81
+ request.on('error', (error) => {
82
+ safeDelete(destinationPath);
83
+ reject(error);
84
+ });
85
+ });
86
+ }
87
+
88
+ function safeDelete(filePath) {
89
+ try {
90
+ if (fs.existsSync(filePath)) {
91
+ fs.unlinkSync(filePath);
92
+ }
93
+ } catch (error) {
94
+ // Ignore cleanup failures for temporary download files.
95
+ }
96
+ }
97
+
98
+ function isRedirect(statusCode) {
99
+ return [301, 302, 303, 307, 308].includes(statusCode);
100
+ }
101
+
102
+ function uploadBuffer(
103
+ url,
104
+ body,
105
+ options = {},
106
+ ) {
107
+ return new Promise((
108
+ resolve,
109
+ reject,
110
+ ) => {
111
+ const transport = url.startsWith('https://') ? https : http;
112
+ const request = transport.request(url, {
113
+ method: options.method || 'PUT',
114
+ headers: {
115
+ 'Content-Length': body.length,
116
+ ...(options.headers || {}),
117
+ },
118
+ }, (response) => {
119
+ const chunks = [];
120
+
121
+ response.on('data', (chunk) => {
122
+ chunks.push(chunk);
123
+ });
124
+
125
+ response.on('end', () => {
126
+ const statusCode = response.statusCode || 0;
127
+ resolve({
128
+ ok: [200, 201, 202, 204].includes(statusCode),
129
+ statusCode,
130
+ url,
131
+ body: Buffer.concat(chunks).toString('utf8'),
132
+ });
133
+ });
134
+ });
135
+
136
+ request.on('error', reject);
137
+ request.write(body);
138
+ request.end();
139
+ });
140
+ }
141
+
142
+ module.exports = {
143
+ downloadToFile,
144
+ uploadFile,
145
+ uploadText,
146
+ };