@josfox/jos 3.1.1 → 4.0.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/README.md +273 -35
- package/bin/jos +76 -0
- package/package.json +21 -49
- package/src/commands/get.js +245 -0
- package/src/commands/repo.js +139 -0
- package/src/commands/run.js +225 -0
- package/src/commands/secrets.js +137 -0
- package/src/index.js +0 -0
- package/src/serve.js +780 -0
- package/LICENSE +0 -20
- package/NOTICE +0 -4
- package/THIRD_PARTY_NOTICES.md +0 -11
- package/bin/jos.js +0 -71
- package/examples/env-check.json +0 -39
- package/lib/resolve.js +0 -32
- package/lib/run.js +0 -43
- package/lib/serve.js +0 -6
- package/lib/validate.js +0 -11
- package/schemas/jos.v0.3.1.schema.json +0 -64
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOS GET Command - Fetch .jos packages from repos
|
|
3
|
+
* Architecture similar to npm/brew/apt but offline-first
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
// AURORA colors
|
|
13
|
+
const C = {
|
|
14
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
15
|
+
purple: '\x1b[38;5;135m', magenta: '\x1b[38;5;198m', cyan: '\x1b[38;5;51m',
|
|
16
|
+
green: '\x1b[38;5;78m', red: '\x1b[38;5;196m', gray: '\x1b[38;5;245m',
|
|
17
|
+
white: '\x1b[38;5;255m', yellow: '\x1b[38;5;220m'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Default repos configuration
|
|
21
|
+
const DEFAULT_REPOS = {
|
|
22
|
+
default: 'https://registry.josfox.ai',
|
|
23
|
+
local: '~/.jos/artifacts'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function getReposConfig(home) {
|
|
27
|
+
const reposPath = path.join(home, 'repos.json');
|
|
28
|
+
if (fs.existsSync(reposPath)) {
|
|
29
|
+
return { ...DEFAULT_REPOS, ...JSON.parse(fs.readFileSync(reposPath)) };
|
|
30
|
+
}
|
|
31
|
+
return DEFAULT_REPOS;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function computeIntegrity(content) {
|
|
35
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Lock file management
|
|
39
|
+
function getLockPath(home) {
|
|
40
|
+
return path.join(home, 'lock.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadLock(home) {
|
|
44
|
+
const lockPath = getLockPath(home);
|
|
45
|
+
if (fs.existsSync(lockPath)) {
|
|
46
|
+
return JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
return { locked: null, packages: {} };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function saveLock(home, lock) {
|
|
52
|
+
const lockPath = getLockPath(home);
|
|
53
|
+
lock.locked = new Date().toISOString();
|
|
54
|
+
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function updateLock(home, pkgName, version, integrity) {
|
|
58
|
+
const lock = loadLock(home);
|
|
59
|
+
lock.packages[pkgName] = { version, integrity: `sha256-${integrity}` };
|
|
60
|
+
saveLock(home, lock);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parse package reference: "pkg", "repo:pkg", "./path", "http://..."
|
|
64
|
+
function parsePackageRef(ref) {
|
|
65
|
+
// URL
|
|
66
|
+
if (ref.startsWith('http://') || ref.startsWith('https://')) {
|
|
67
|
+
return { type: 'url', url: ref };
|
|
68
|
+
}
|
|
69
|
+
// Local path
|
|
70
|
+
if (ref.startsWith('./') || ref.startsWith('/') || ref.startsWith('~')) {
|
|
71
|
+
return { type: 'local', path: ref };
|
|
72
|
+
}
|
|
73
|
+
// Named repo: "myrepo:package"
|
|
74
|
+
if (ref.includes(':')) {
|
|
75
|
+
const [repo, pkg] = ref.split(':');
|
|
76
|
+
return { type: 'repo', repo, package: pkg };
|
|
77
|
+
}
|
|
78
|
+
// Default: package name
|
|
79
|
+
return { type: 'default', package: ref };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fetch from URL
|
|
83
|
+
function fetchUrl(url) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
86
|
+
protocol.get(url, (res) => {
|
|
87
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
88
|
+
return fetchUrl(res.headers.location).then(resolve).catch(reject);
|
|
89
|
+
}
|
|
90
|
+
if (res.statusCode !== 200) {
|
|
91
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
92
|
+
}
|
|
93
|
+
let data = '';
|
|
94
|
+
res.on('data', chunk => data += chunk);
|
|
95
|
+
res.on('end', () => resolve(data));
|
|
96
|
+
}).on('error', reject);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
exports.execute = async (args, home) => {
|
|
101
|
+
const target = args[0];
|
|
102
|
+
const fromRepo = args.find((a, i) => args[i - 1] === '--from');
|
|
103
|
+
const showHelp = args.includes('--help') || args.includes('-h');
|
|
104
|
+
|
|
105
|
+
if (showHelp || !target) {
|
|
106
|
+
console.log(`
|
|
107
|
+
${C.cyan}${C.bold}JOS GET${C.reset} - Fetch .jos packages
|
|
108
|
+
|
|
109
|
+
${C.white}Usage:${C.reset} jos get <package> [options]
|
|
110
|
+
|
|
111
|
+
${C.white}Package formats:${C.reset}
|
|
112
|
+
jos get hello ${C.dim}# From default registry${C.reset}
|
|
113
|
+
jos get ./my-package ${C.dim}# From local folder${C.reset}
|
|
114
|
+
jos get myrepo:package ${C.dim}# From named repo${C.reset}
|
|
115
|
+
jos get http://host/p.jos ${C.dim}# From URL${C.reset}
|
|
116
|
+
|
|
117
|
+
${C.white}Options:${C.reset}
|
|
118
|
+
--from <host> Override source (e.g., --from 192.168.1.10:1111)
|
|
119
|
+
--help, -h Show this help
|
|
120
|
+
|
|
121
|
+
${C.white}Configuration:${C.reset}
|
|
122
|
+
Repos defined in: ~/.jos/repos.json
|
|
123
|
+
|
|
124
|
+
${C.white}Example repos.json:${C.reset}
|
|
125
|
+
{
|
|
126
|
+
"default": "https://registry.josfox.ai",
|
|
127
|
+
"local": "~/.jos/artifacts",
|
|
128
|
+
"myrepo": "http://192.168.1.10:1111"
|
|
129
|
+
}
|
|
130
|
+
`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(`\n${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
|
|
135
|
+
console.log(`${C.cyan}${C.bold}JOS GET${C.reset} // ${C.gray}Package Manager${C.reset}`);
|
|
136
|
+
console.log(`${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`);
|
|
137
|
+
|
|
138
|
+
const repos = getReposConfig(home);
|
|
139
|
+
const artifactsDir = path.join(home, 'artifacts');
|
|
140
|
+
if (!fs.existsSync(artifactsDir)) fs.mkdirSync(artifactsDir, { recursive: true });
|
|
141
|
+
|
|
142
|
+
// Parse reference
|
|
143
|
+
let ref = parsePackageRef(target);
|
|
144
|
+
|
|
145
|
+
// Override with --from
|
|
146
|
+
if (fromRepo) {
|
|
147
|
+
const host = fromRepo.includes(':') ? fromRepo : `${fromRepo}:1111`;
|
|
148
|
+
ref = { type: 'url', url: `http://${host}/${target}.jos` };
|
|
149
|
+
console.log(`${C.white}📡 Source:${C.reset} ${host}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(`${C.white}📦 Package:${C.reset} ${target}`);
|
|
153
|
+
console.log(`${C.white}📁 Destination:${C.reset} ${artifactsDir}`);
|
|
154
|
+
|
|
155
|
+
let content, sourcePath;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
switch (ref.type) {
|
|
159
|
+
case 'local':
|
|
160
|
+
// Local folder/file
|
|
161
|
+
sourcePath = ref.path.replace('~', process.env.HOME);
|
|
162
|
+
if (!sourcePath.endsWith('.jos')) sourcePath += '.jos';
|
|
163
|
+
if (!fs.existsSync(sourcePath)) throw new Error(`Not found: ${sourcePath}`);
|
|
164
|
+
content = fs.readFileSync(sourcePath, 'utf8');
|
|
165
|
+
console.log(`${C.green}✓ Found locally:${C.reset} ${sourcePath}`);
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case 'url':
|
|
169
|
+
// Direct URL
|
|
170
|
+
console.log(`${C.dim}⏳ Fetching from URL...${C.reset}`);
|
|
171
|
+
content = await fetchUrl(ref.url);
|
|
172
|
+
console.log(`${C.green}✓ Downloaded from:${C.reset} ${ref.url}`);
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
case 'repo':
|
|
176
|
+
// Named repo
|
|
177
|
+
const repoUrl = repos[ref.repo];
|
|
178
|
+
if (!repoUrl) throw new Error(`Unknown repo: ${ref.repo}`);
|
|
179
|
+
const pkgUrl = repoUrl.startsWith('http')
|
|
180
|
+
? `${repoUrl}/${ref.package}.jos`
|
|
181
|
+
: path.join(repoUrl.replace('~', process.env.HOME), `${ref.package}.jos`);
|
|
182
|
+
|
|
183
|
+
if (pkgUrl.startsWith('http')) {
|
|
184
|
+
console.log(`${C.dim}⏳ Fetching from ${ref.repo}...${C.reset}`);
|
|
185
|
+
content = await fetchUrl(pkgUrl);
|
|
186
|
+
} else {
|
|
187
|
+
if (!fs.existsSync(pkgUrl)) throw new Error(`Not found: ${pkgUrl}`);
|
|
188
|
+
content = fs.readFileSync(pkgUrl, 'utf8');
|
|
189
|
+
}
|
|
190
|
+
console.log(`${C.green}✓ Found in ${ref.repo}:${C.reset} ${ref.package}`);
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
case 'default':
|
|
194
|
+
// Try local first, then default registry
|
|
195
|
+
const localPath = path.join(artifactsDir, `${ref.package}.jos`);
|
|
196
|
+
if (fs.existsSync(localPath)) {
|
|
197
|
+
console.log(`${C.green}✓ Already cached:${C.reset} ${localPath}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Try default registry
|
|
202
|
+
if (repos.default && repos.default.startsWith('http')) {
|
|
203
|
+
console.log(`${C.dim}⏳ Fetching from registry...${C.reset}`);
|
|
204
|
+
try {
|
|
205
|
+
content = await fetchUrl(`${repos.default}/${ref.package}.jos`);
|
|
206
|
+
console.log(`${C.green}✓ Downloaded:${C.reset} ${ref.package}`);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
throw new Error(`Package not found: ${ref.package}`);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
throw new Error(`Package not found: ${ref.package}`);
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Validate JSON
|
|
217
|
+
let artifact;
|
|
218
|
+
try {
|
|
219
|
+
artifact = JSON.parse(content);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
throw new Error('Invalid .jos file (not valid JSON)');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Compute integrity
|
|
225
|
+
const integrity = computeIntegrity(content);
|
|
226
|
+
console.log(`${C.white}🔐 Integrity:${C.reset} ${C.green}${integrity.substring(0, 16)}...${C.reset}`);
|
|
227
|
+
|
|
228
|
+
// Save artifact
|
|
229
|
+
const pkgName = artifact.meta?.name || artifact.name || target.replace(/[^a-z0-9]/gi, '_');
|
|
230
|
+
const pkgVersion = artifact.meta?.version || artifact.orchestration_contract?.version || '1.0.0';
|
|
231
|
+
const destPath = path.join(artifactsDir, `${pkgName}.jos`);
|
|
232
|
+
fs.writeFileSync(destPath, content);
|
|
233
|
+
|
|
234
|
+
// Update lock file
|
|
235
|
+
updateLock(home, pkgName, pkgVersion, integrity);
|
|
236
|
+
console.log(`${C.dim}📋 Lock file updated${C.reset}`);
|
|
237
|
+
|
|
238
|
+
console.log(`\n${C.green}✓ Saved to:${C.reset} ${destPath}`);
|
|
239
|
+
console.log(`${C.dim} Run with: jos run ${pkgName}.jos${C.reset}\n`);
|
|
240
|
+
|
|
241
|
+
} catch (e) {
|
|
242
|
+
console.log(`\n${C.red}✖ Error:${C.reset} ${e.message}\n`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOS REPO Command - Manage package repositories
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// AURORA colors
|
|
9
|
+
const C = {
|
|
10
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
11
|
+
purple: '\x1b[38;5;135m', magenta: '\x1b[38;5;198m', cyan: '\x1b[38;5;51m',
|
|
12
|
+
green: '\x1b[38;5;78m', red: '\x1b[38;5;196m', gray: '\x1b[38;5;245m',
|
|
13
|
+
white: '\x1b[38;5;255m', yellow: '\x1b[38;5;220m'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_REPOS = {
|
|
17
|
+
default: 'https://registry.josfox.ai',
|
|
18
|
+
local: '~/.jos/artifacts'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getReposPath(home) {
|
|
22
|
+
return path.join(home, 'repos.json');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadRepos(home) {
|
|
26
|
+
const reposPath = getReposPath(home);
|
|
27
|
+
if (fs.existsSync(reposPath)) {
|
|
28
|
+
return { ...DEFAULT_REPOS, ...JSON.parse(fs.readFileSync(reposPath, 'utf8')) };
|
|
29
|
+
}
|
|
30
|
+
return { ...DEFAULT_REPOS };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function saveRepos(home, repos) {
|
|
34
|
+
const reposPath = getReposPath(home);
|
|
35
|
+
fs.writeFileSync(reposPath, JSON.stringify(repos, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
exports.execute = async (args, home) => {
|
|
39
|
+
const action = args[0];
|
|
40
|
+
const name = args[1];
|
|
41
|
+
const url = args[2];
|
|
42
|
+
const showHelp = args.includes('--help') || args.includes('-h');
|
|
43
|
+
|
|
44
|
+
if (showHelp || !action) {
|
|
45
|
+
console.log(`
|
|
46
|
+
${C.cyan}${C.bold}JOS REPO${C.reset} - Manage package repositories
|
|
47
|
+
|
|
48
|
+
${C.white}Usage:${C.reset} jos repo <action> [name] [url]
|
|
49
|
+
|
|
50
|
+
${C.white}Actions:${C.reset}
|
|
51
|
+
list Show all configured repos
|
|
52
|
+
add <name> <url> Add a new repository
|
|
53
|
+
remove <name> Remove a repository
|
|
54
|
+
default <name> Set default repository
|
|
55
|
+
|
|
56
|
+
${C.white}Examples:${C.reset}
|
|
57
|
+
jos repo list
|
|
58
|
+
jos repo add myserver http://192.168.1.10:1111
|
|
59
|
+
jos repo add company https://artifacts.company.com
|
|
60
|
+
jos repo default myserver
|
|
61
|
+
jos repo remove myserver
|
|
62
|
+
|
|
63
|
+
${C.white}Using repos:${C.reset}
|
|
64
|
+
jos get myserver:package-name
|
|
65
|
+
jos get package --from 192.168.1.10
|
|
66
|
+
`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const repos = loadRepos(home);
|
|
71
|
+
|
|
72
|
+
switch (action) {
|
|
73
|
+
case 'list':
|
|
74
|
+
console.log(`\n${C.cyan}${C.bold}📦 Configured Repositories${C.reset}\n`);
|
|
75
|
+
Object.entries(repos).forEach(([name, url]) => {
|
|
76
|
+
const isDefault = name === 'default';
|
|
77
|
+
const isLocal = name === 'local';
|
|
78
|
+
console.log(` ${C.white}${name}${C.reset}`);
|
|
79
|
+
console.log(` ${C.gray}${url}${C.reset}`);
|
|
80
|
+
if (isDefault) console.log(` ${C.green}★ Default registry${C.reset}`);
|
|
81
|
+
if (isLocal) console.log(` ${C.cyan}📁 Local artifacts${C.reset}`);
|
|
82
|
+
console.log();
|
|
83
|
+
});
|
|
84
|
+
console.log(`${C.dim}Config: ~/.jos/repos.json${C.reset}\n`);
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case 'add':
|
|
88
|
+
if (!name || !url) {
|
|
89
|
+
console.log(`${C.red}✖ Usage: jos repo add <name> <url>${C.reset}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
if (repos[name] && name !== 'default' && name !== 'local') {
|
|
93
|
+
console.log(`${C.yellow}⚠ Repository '${name}' already exists. Updating...${C.reset}`);
|
|
94
|
+
}
|
|
95
|
+
repos[name] = url;
|
|
96
|
+
saveRepos(home, repos);
|
|
97
|
+
console.log(`${C.green}✓ Repository '${name}' added: ${url}${C.reset}`);
|
|
98
|
+
console.log(`${C.dim} Use: jos get ${name}:package-name${C.reset}`);
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case 'remove':
|
|
102
|
+
case 'rm':
|
|
103
|
+
if (!name) {
|
|
104
|
+
console.log(`${C.red}✖ Usage: jos repo remove <name>${C.reset}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
if (name === 'default' || name === 'local') {
|
|
108
|
+
console.log(`${C.red}✖ Cannot remove built-in repository '${name}'${C.reset}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
if (!repos[name]) {
|
|
112
|
+
console.log(`${C.yellow}⚠ Repository '${name}' not found${C.reset}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
delete repos[name];
|
|
116
|
+
saveRepos(home, repos);
|
|
117
|
+
console.log(`${C.green}✓ Repository '${name}' removed${C.reset}`);
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'default':
|
|
121
|
+
if (!name) {
|
|
122
|
+
console.log(`${C.white}Current default: ${repos.default}${C.reset}`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!repos[name] && !name.startsWith('http')) {
|
|
126
|
+
console.log(`${C.red}✖ Repository '${name}' not found${C.reset}`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
repos.default = repos[name] || name;
|
|
130
|
+
saveRepos(home, repos);
|
|
131
|
+
console.log(`${C.green}✓ Default repository set to: ${repos.default}${C.reset}`);
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
default:
|
|
135
|
+
console.log(`${C.red}✖ Unknown action: ${action}${C.reset}`);
|
|
136
|
+
console.log(`${C.dim}Run 'jos repo --help' for usage${C.reset}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOS RUN Command - Execute .jos artifacts
|
|
3
|
+
* Complies with JOSFOXAI MAGIC contract
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync, spawn } = require('child_process');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
// AURORA colors
|
|
12
|
+
const C = {
|
|
13
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
14
|
+
purple: '\x1b[38;5;135m', magenta: '\x1b[38;5;198m', cyan: '\x1b[38;5;51m',
|
|
15
|
+
green: '\x1b[38;5;78m', red: '\x1b[38;5;196m', gray: '\x1b[38;5;245m',
|
|
16
|
+
white: '\x1b[38;5;255m', yellow: '\x1b[38;5;220m'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Compute SHA-256 integrity
|
|
20
|
+
function computeIntegrity(content) {
|
|
21
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate JOSFOXAI MAGIC contract (flexible for different schemas)
|
|
25
|
+
function validateMagic(artifact) {
|
|
26
|
+
const errors = [];
|
|
27
|
+
const warnings = [];
|
|
28
|
+
|
|
29
|
+
// MAGIC components (Intention) - flexible detection
|
|
30
|
+
const hasMeta = artifact.meta || artifact._josfox || artifact.jos_schema;
|
|
31
|
+
const hasIntention = artifact.intention || artifact.meta?.intention ||
|
|
32
|
+
artifact.description || artifact._josfox?.description;
|
|
33
|
+
|
|
34
|
+
if (!hasMeta) warnings.push('Recommended: meta or _josfox');
|
|
35
|
+
if (!hasIntention) warnings.push('Recommended: intention or description');
|
|
36
|
+
|
|
37
|
+
// JOSFOXAI components (Execution) - at least one required
|
|
38
|
+
const hasExecution = artifact.flow || artifact.tasks || artifact.pipelines ||
|
|
39
|
+
artifact.jos || artifact.shell;
|
|
40
|
+
if (!hasExecution) errors.push('Missing: execution (flow/tasks/pipelines/shell)');
|
|
41
|
+
|
|
42
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
exports.execute = async (args, home) => {
|
|
46
|
+
const target = args[0];
|
|
47
|
+
const dryRun = args.includes('--dry-run');
|
|
48
|
+
const showHelp = args.includes('--help') || args.includes('-h');
|
|
49
|
+
|
|
50
|
+
if (showHelp || !target) {
|
|
51
|
+
console.log(`
|
|
52
|
+
${C.cyan}${C.bold}JOS RUN${C.reset} - Execute .jos artifacts
|
|
53
|
+
|
|
54
|
+
${C.white}Usage:${C.reset} jos run <file.jos> [options]
|
|
55
|
+
|
|
56
|
+
${C.white}Options:${C.reset}
|
|
57
|
+
--dry-run Validate and show plan without executing
|
|
58
|
+
--task <name> Run specific task only
|
|
59
|
+
--help, -h Show this help
|
|
60
|
+
|
|
61
|
+
${C.white}Examples:${C.reset}
|
|
62
|
+
jos run hello.jos
|
|
63
|
+
jos run ./my-artifact.jos --dry-run
|
|
64
|
+
jos run artifact.jos --task build
|
|
65
|
+
`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Resolve path
|
|
70
|
+
let artifactPath = target;
|
|
71
|
+
if (!fs.existsSync(artifactPath)) {
|
|
72
|
+
artifactPath = path.join(home, 'artifacts', target);
|
|
73
|
+
if (!fs.existsSync(artifactPath)) {
|
|
74
|
+
artifactPath = path.join(process.cwd(), target);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(artifactPath)) {
|
|
79
|
+
console.log(`${C.red}✖ Artifact not found:${C.reset} ${target}`);
|
|
80
|
+
console.log(`${C.dim} Searched: ./, ~/.jos/artifacts/${C.reset}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`\n${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
|
|
85
|
+
console.log(`${C.cyan}${C.bold}JOS RUN${C.reset} // ${C.gray}JOSFOXAI MAGIC Runtime${C.reset}`);
|
|
86
|
+
console.log(`${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`);
|
|
87
|
+
|
|
88
|
+
// Load artifact
|
|
89
|
+
const content = fs.readFileSync(artifactPath, 'utf8');
|
|
90
|
+
const integrity = computeIntegrity(content);
|
|
91
|
+
let artifact;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
artifact = JSON.parse(content);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.log(`${C.red}✖ Invalid JSON:${C.reset} ${e.message}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Display metadata
|
|
101
|
+
console.log(`${C.white}📦 Artifact:${C.reset} ${artifact.meta?.name || artifact.name || path.basename(artifactPath)}`);
|
|
102
|
+
console.log(`${C.white}📄 File:${C.reset} ${artifactPath}`);
|
|
103
|
+
console.log(`${C.white}🔐 Integrity:${C.reset} ${C.green}${integrity.substring(0, 16)}...${C.reset}`);
|
|
104
|
+
|
|
105
|
+
// Validate MAGIC
|
|
106
|
+
const validation = validateMagic(artifact);
|
|
107
|
+
if (!validation.valid) {
|
|
108
|
+
console.log(`\n${C.red}✖ MAGIC Validation Failed:${C.reset}`);
|
|
109
|
+
validation.errors.forEach(e => console.log(` ${C.red}•${C.reset} ${e}`));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
console.log(`${C.white}✓ MAGIC:${C.reset} ${C.green}Valid${C.reset}`);
|
|
113
|
+
|
|
114
|
+
// Show intention
|
|
115
|
+
const intention = artifact.intention?.objective || artifact.meta?.intention || 'No intention defined';
|
|
116
|
+
console.log(`${C.white}🎯 Intention:${C.reset} ${intention}`);
|
|
117
|
+
|
|
118
|
+
// Check guardrails
|
|
119
|
+
if (artifact.guardrails?.avoid?.length > 0) {
|
|
120
|
+
console.log(`${C.white}🛡️ Guardrails:${C.reset} ${artifact.guardrails.avoid.join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Determine execution mode
|
|
124
|
+
const taskName = args.find((a, i) => args[i - 1] === '--task');
|
|
125
|
+
|
|
126
|
+
console.log(`\n${C.cyan}▶ Execution Plan:${C.reset}`);
|
|
127
|
+
|
|
128
|
+
// Execute based on artifact type
|
|
129
|
+
if (artifact.pipelines && !taskName) {
|
|
130
|
+
// Pipeline mode
|
|
131
|
+
const pipelineNames = Object.keys(artifact.pipelines);
|
|
132
|
+
console.log(` ${C.dim}Mode: Pipeline${C.reset}`);
|
|
133
|
+
pipelineNames.forEach(name => {
|
|
134
|
+
const pipeline = artifact.pipelines[name];
|
|
135
|
+
console.log(` ${C.cyan}→${C.reset} ${name}: ${pipeline.steps?.length || 0} steps`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (dryRun) {
|
|
139
|
+
console.log(`\n${C.yellow}⚡ Dry run - no execution${C.reset}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Execute first pipeline
|
|
144
|
+
const firstPipeline = pipelineNames[0];
|
|
145
|
+
await executePipeline(artifact, firstPipeline, artifact.pipelines[firstPipeline]);
|
|
146
|
+
|
|
147
|
+
} else if (artifact.tasks) {
|
|
148
|
+
// Task mode
|
|
149
|
+
const taskNames = taskName ? [taskName] : Object.keys(artifact.tasks);
|
|
150
|
+
console.log(` ${C.dim}Mode: Tasks${C.reset}`);
|
|
151
|
+
taskNames.forEach(name => {
|
|
152
|
+
const task = artifact.tasks[name];
|
|
153
|
+
console.log(` ${C.cyan}→${C.reset} ${name}: ${task.description || ''}`);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (dryRun) {
|
|
157
|
+
console.log(`\n${C.yellow}⚡ Dry run - no execution${C.reset}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Execute tasks
|
|
162
|
+
for (const name of taskNames) {
|
|
163
|
+
await executeTask(name, artifact.tasks[name]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
} else if (artifact.flow?.steps) {
|
|
167
|
+
// Simple flow mode
|
|
168
|
+
console.log(` ${C.dim}Mode: Flow${C.reset}`);
|
|
169
|
+
artifact.flow.steps.forEach((step, i) => {
|
|
170
|
+
console.log(` ${C.cyan}${i + 1}.${C.reset} ${step}`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (dryRun) {
|
|
174
|
+
console.log(`\n${C.yellow}⚡ Dry run - no execution${C.reset}`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Execute steps
|
|
179
|
+
for (const step of artifact.flow.steps) {
|
|
180
|
+
executeStep(step);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`\n${C.green}✓ Execution complete${C.reset}\n`);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
async function executePipeline(artifact, name, pipeline) {
|
|
188
|
+
console.log(`\n${C.purple}▶ Running pipeline: ${name}${C.reset}\n`);
|
|
189
|
+
|
|
190
|
+
for (const stepRef of pipeline.steps || []) {
|
|
191
|
+
// Parse step reference (e.g., "tasks.build")
|
|
192
|
+
const [type, taskName] = stepRef.split('.');
|
|
193
|
+
|
|
194
|
+
if (type === 'tasks' && artifact.tasks[taskName]) {
|
|
195
|
+
await executeTask(taskName, artifact.tasks[taskName]);
|
|
196
|
+
} else {
|
|
197
|
+
console.log(` ${C.yellow}⚠ Unknown step: ${stepRef}${C.reset}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function executeTask(name, task) {
|
|
203
|
+
console.log(`\n${C.cyan}▶ Task: ${name}${C.reset}`);
|
|
204
|
+
if (task.description) console.log(` ${C.dim}${task.description}${C.reset}`);
|
|
205
|
+
|
|
206
|
+
if (task.shell && Array.isArray(task.shell)) {
|
|
207
|
+
const script = task.shell.join('\n');
|
|
208
|
+
try {
|
|
209
|
+
execSync(script, { stdio: 'inherit', shell: '/bin/bash' });
|
|
210
|
+
console.log(` ${C.green}✓ Task complete${C.reset}`);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.log(` ${C.red}✖ Task failed: ${e.message}${C.reset}`);
|
|
213
|
+
throw e;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function executeStep(step) {
|
|
219
|
+
console.log(` ${C.cyan}▶${C.reset} ${step}`);
|
|
220
|
+
try {
|
|
221
|
+
execSync(step, { stdio: 'inherit', shell: true });
|
|
222
|
+
} catch (e) {
|
|
223
|
+
console.log(` ${C.red}✖ Step failed${C.reset}`);
|
|
224
|
+
}
|
|
225
|
+
}
|