@josfox/jos 4.1.0 → 5.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.
Files changed (64) hide show
  1. package/LICENSE +208 -21
  2. package/PROMISES.md +52 -0
  3. package/README.md +334 -138
  4. package/SECURITY-MCP.md +117 -0
  5. package/SECURITY-SECRETS.md +108 -0
  6. package/STYLE.md +23 -0
  7. package/bin/jos +170 -160
  8. package/examples/01-hello.jos +44 -0
  9. package/examples/02-criteria.jos +54 -0
  10. package/examples/03-flow-gate.jos +83 -0
  11. package/examples/04-use-template/README.md +22 -0
  12. package/examples/04-use-template/lib/greeting.jos +56 -0
  13. package/examples/04-use-template/use.jos +64 -0
  14. package/examples/05-sealed/.jos/manifest.jos +53 -0
  15. package/examples/05-sealed/README.md +25 -0
  16. package/examples/06-requires.jos +42 -0
  17. package/examples/07-sandbox.jos +39 -0
  18. package/examples/08-ids-ledger-meter.md +26 -0
  19. package/examples/10-ollama.jos +47 -0
  20. package/examples/11-openai.jos +47 -0
  21. package/examples/12-gemini.jos +47 -0
  22. package/examples/13-anthropic.jos +47 -0
  23. package/examples/14-serve.jos +88 -0
  24. package/examples/README.md +105 -0
  25. package/examples/hello.jos +47 -0
  26. package/package.json +57 -40
  27. package/promises.json +894 -0
  28. package/src/args.js +47 -0
  29. package/src/commands/_frozen/add.README.md +1 -0
  30. package/src/commands/_frozen/get.README.md +1 -0
  31. package/src/commands/_frozen/llm.README.md +1 -0
  32. package/src/commands/_frozen/prompts.README.md +1 -0
  33. package/src/commands/_frozen/repo.README.md +1 -0
  34. package/src/commands/_frozen/serve-explorer.README.md +1 -0
  35. package/src/commands/ids.js +101 -0
  36. package/src/commands/init.js +128 -162
  37. package/src/commands/ledger.js +94 -0
  38. package/src/commands/meter.js +96 -0
  39. package/src/commands/migrate.js +214 -0
  40. package/src/commands/overview.js +118 -0
  41. package/src/commands/run.js +194 -244
  42. package/src/commands/runs.js +102 -0
  43. package/src/commands/secrets.js +717 -137
  44. package/src/commands/start.js +210 -0
  45. package/src/commands/validate.js +178 -194
  46. package/src/commands/verify.js +56 -98
  47. package/src/commands/version.js +23 -0
  48. package/src/intention.js +114 -0
  49. package/src/kernel.js +69 -0
  50. package/src/ledgers.js +57 -0
  51. package/src/mcp-guide.js +138 -0
  52. package/src/mcp.js +734 -0
  53. package/src/serve.js +48 -781
  54. package/src/ui.js +98 -0
  55. package/src/adapters/index.js +0 -16
  56. package/src/adapters/native.js +0 -41
  57. package/src/adapters/ollama.js +0 -140
  58. package/src/commands/add.js +0 -145
  59. package/src/commands/get.js +0 -245
  60. package/src/commands/llm.js +0 -159
  61. package/src/commands/prompts.js +0 -229
  62. package/src/commands/repo.js +0 -139
  63. package/src/utils/crypto.js +0 -41
  64. package/src/utils/request.js +0 -41
package/src/ui.js ADDED
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+ // The one style module of the host (STYLE.md). Every line written for a person goes through a printer of this file:
3
+ // the header with rules, the marks ✓ ✖ ⚠, the colors and the indentation live here and nowhere else. Colors are off
4
+ // when NO_COLOR is set or the stream is not a terminal, so piped output carries no escape codes. Machine output
5
+ // (`--json`) and data that is not a message (a child's output, a revealed value) also leave through this file, by
6
+ // json() and data(), which never add color or decoration.
7
+
8
+ const FULL = {
9
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
10
+ purple: '\x1b[38;5;135m', magenta: '\x1b[38;5;198m', cyan: '\x1b[38;5;51m',
11
+ blue: '\x1b[38;5;39m', white: '\x1b[38;5;255m', gray: '\x1b[38;5;245m',
12
+ pink: '\x1b[38;5;213m', teal: '\x1b[38;5;44m',
13
+ green: '\x1b[38;5;78m', red: '\x1b[38;5;196m', yellow: '\x1b[38;5;220m',
14
+ };
15
+
16
+ const BASIC = {
17
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
18
+ purple: '\x1b[35m', magenta: '\x1b[35m', cyan: '\x1b[36m',
19
+ blue: '\x1b[34m', white: '\x1b[37m', gray: '\x1b[90m',
20
+ pink: '\x1b[35m', teal: '\x1b[36m',
21
+ green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m',
22
+ };
23
+
24
+ function supports256Color() {
25
+ const term = process.env.TERM || '';
26
+ const colorterm = process.env.COLORTERM || '';
27
+ return term.includes('256color') || colorterm === 'truecolor' ||
28
+ colorterm === '24bit' || process.platform === 'darwin';
29
+ }
30
+
31
+ function supportsUnicode() {
32
+ const term = process.env.TERM || '';
33
+ const lang = process.env.LANG || '';
34
+ if (lang && !lang.includes('UTF-8') && !lang.includes('utf8')) return false;
35
+ return lang.includes('UTF-8') || lang.includes('utf8') ||
36
+ term.includes('xterm') || term.includes('256color') ||
37
+ (process.platform === 'darwin' && !lang);
38
+ }
39
+
40
+ function palette(stream) {
41
+ const on = !process.env.NO_COLOR && stream && stream.isTTY;
42
+ const src = supports256Color() ? FULL : BASIC;
43
+ const out = {};
44
+ for (const k of Object.keys(FULL)) out[k] = on ? src[k] : '';
45
+ return out;
46
+ }
47
+
48
+ const RULE = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
49
+
50
+ // A printer bound to one stream (process.stdout, process.stderr or a stream a test passes in). The stream's write
51
+ // is looked up on every call, so a test that replaces process.stdout.write sees every line.
52
+ // header(title, sub) blank line, rule, "TITLE // sub", rule, blank line
53
+ // ok / fail / warn "✓ text", "✖ text", "⚠ text" in green / red / yellow, then an optional gray detail
54
+ // step "▶ text" in cyan: something that starts now (a command the MCP client runs)
55
+ // hint(text) an indented, dimmed follow-up line (usage, next step, why)
56
+ // field(label, value) an indented "label: value" line
57
+ // item(text) an indented "• text" line
58
+ // cmd(text) an indented command to copy: " $ text"
59
+ // line(s) / text(s) a line, or a block or prompt written as given (composed with C, this printer's palette)
60
+ function printer(stream) {
61
+ const C = palette(stream);
62
+ const text = (s) => { stream.write(s); };
63
+ const line = (s) => text((s === undefined ? '' : s) + '\n');
64
+ const mark = (color, sign) => (s, detail) => line(`${C[color]}${sign} ${s}${C.reset}` + (detail ? ` ${C.gray}${detail}${C.reset}` : ''));
65
+ return {
66
+ C,
67
+ text,
68
+ line,
69
+ header(title, sub) {
70
+ line();
71
+ line(`${C.purple}${RULE}${C.reset}`);
72
+ line(`${C.bold}${title}${C.reset}` + (sub ? ` // ${C.gray}${sub}${C.reset}` : ''));
73
+ line(`${C.purple}${RULE}${C.reset}`);
74
+ line();
75
+ },
76
+ ok: mark('green', '✓'),
77
+ fail: mark('red', '✖'),
78
+ warn: mark('yellow', '⚠'),
79
+ step: mark('cyan', '▶'),
80
+ hint: (s) => line(` ${C.dim}${s}${C.reset}`),
81
+ field: (label, value) => line(` ${C.cyan}${label}:${C.reset} ${value}`),
82
+ item: (s) => line(` ${C.white}•${C.reset} ${s}`),
83
+ cmd: (s) => line(` ${C.gray}$${C.reset} ${C.cyan}${s}${C.reset}`),
84
+ };
85
+ }
86
+
87
+ // Machine output (--json): plain JSON, two-space indent, one final newline; never colored, whatever the terminal.
88
+ function json(stream, value) { stream.write(JSON.stringify(value, null, 2) + '\n'); }
89
+
90
+ // Data that is not a message (a child's stdout or stderr, a value revealed on a terminal): written byte for byte.
91
+ function data(stream, chunk) { stream.write(chunk); }
92
+
93
+ // One argument as a POSIX shell word, for commands printed to be copied: left as is when it is plain, else quoted.
94
+ function shellArg(s) {
95
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(s) ? s : "'" + s.replace(/'/g, "'\\''") + "'";
96
+ }
97
+
98
+ module.exports = { palette, supportsUnicode, RULE, printer, json, data, shellArg };
@@ -1,16 +0,0 @@
1
- const NativeAdapter = require('./native');
2
- const OllamaAdapter = require('./ollama');
3
-
4
- /**
5
- * Adapter Factory
6
- * Returns the appropriate adapter based on provider type
7
- */
8
- exports.getAdapter = (type) => {
9
- switch (type) {
10
- case 'ollama':
11
- return OllamaAdapter;
12
- case 'native':
13
- default:
14
- return NativeAdapter;
15
- }
16
- };
@@ -1,41 +0,0 @@
1
- /**
2
- * Native JOS Adapter
3
- * Connects to the standard JOS Prompts API (e.g. josfox.cloud)
4
- */
5
- const { apiRequest } = require('../utils/request');
6
-
7
- module.exports = {
8
- type: 'native',
9
-
10
- async optimize(artifact, options, providerUrl, model, apiKey) {
11
- const res = await apiRequest(`${providerUrl}/optimize`, 'POST', {
12
- artifact, options, format_version: '0.0.7'
13
- }, apiKey);
14
- if (res.status !== 200) throw new Error(res.data?.error?.message || `API ${res.status}`);
15
- return res.data;
16
- },
17
-
18
- async validate(artifact, providerUrl, model, apiKey) {
19
- const res = await apiRequest(`${providerUrl}/validate`, 'POST', {
20
- artifact, format_version: '0.0.7'
21
- }, apiKey);
22
- if (res.status !== 200) throw new Error(res.data?.error?.message || `API ${res.status}`);
23
- return res.data;
24
- },
25
-
26
- async generate(intention, options, providerUrl, model, apiKey) {
27
- const res = await apiRequest(`${providerUrl}/generate`, 'POST', {
28
- intention, options, format_version: '0.0.7'
29
- }, apiKey);
30
- if (res.status !== 200) throw new Error(res.data?.error?.message || `API ${res.status}`);
31
- return res.data;
32
- },
33
-
34
- async execute(artifact, providerUrl, model, apiKey) {
35
- const res = await apiRequest(`${providerUrl}/execute`, 'POST', {
36
- artifact, format_version: '0.0.7'
37
- }, apiKey);
38
- if (res.status !== 200) throw new Error(res.data?.error?.message || `API ${res.status}`);
39
- return res.data;
40
- }
41
- };
@@ -1,140 +0,0 @@
1
- /**
2
- * [EXPERIMENTAL] Ollama Adapter
3
- * Adapts JOS Intentions to Local LLM Prompts
4
- */
5
- const { apiRequest } = require('../utils/request');
6
-
7
- const C = {
8
- dim: '\x1b[2m',
9
- reset: '\x1b[0m'
10
- };
11
-
12
- async function callOllama(baseUrl, model, messages) {
13
- // Ensure URL ends with /api/chat
14
- const endpoint = baseUrl.endsWith('/api/chat') ? baseUrl : `${baseUrl.replace(/\/$/, '')}/api/chat`;
15
-
16
- console.log(`${C.dim}Connecting to Ollama at ${endpoint}...${C.reset}`);
17
-
18
- return apiRequest(endpoint, 'POST', {
19
- model: model || 'llama3', // Default to llama3 if not specified
20
- messages: messages,
21
- stream: false,
22
- format: 'json' // Force JSON mode for structure
23
- });
24
- }
25
-
26
- module.exports = {
27
- type: 'ollama',
28
- experimental: true,
29
-
30
- async optimize(artifact, options, providerUrl, model) {
31
- const prompt = `You are an expert prompt engineer specializing in JOS (JSON Orchestration Spec).
32
- Your goal is to optimize the PROMPTS inside the following JOS artifact.
33
-
34
- GOALS: ${options.optimization_goals.join(', ')}
35
-
36
- INPUT ARTIFACT:
37
- ${JSON.stringify(artifact, null, 2)}
38
-
39
- INSTRUCTIONS:
40
- 1. Analyze the 'intention' and 'tasks'.
41
- 2. Rewrite task descriptions to be more precise, using Chain-of-Thought or clear directives.
42
- 3. Do NOT change the structure or keys. Only text content.
43
- 4. Return the COMPLETE logical JSON of the optimized artifact.
44
- `;
45
-
46
- const res = await callOllama(providerUrl, model, [{ role: 'user', content: prompt }]);
47
-
48
- if (res.status !== 200) throw new Error(`Ollama Error: ${res.status} ${JSON.stringify(res.data)}`);
49
-
50
- const optimizedArtifact = typeof res.data.message.content === 'string'
51
- ? JSON.parse(res.data.message.content)
52
- : res.data.message.content;
53
-
54
- // Mock quality score for local execution
55
- return {
56
- optimized_artifact: optimizedArtifact,
57
- quality_score: { before: 0.5, after: 0.9 },
58
- changes: [{ field: 'tasks', original: '...', optimized: '...', reason: 'Ollama Optimization' }]
59
- };
60
- },
61
-
62
- async validate(artifact, providerUrl, model) {
63
- const prompt = `You are a QA Validator for JOS Artifacts.
64
- Analyze the following artifact for logical consistency, missing context, or vague prompts.
65
-
66
- ARTIFACT:
67
- ${JSON.stringify(artifact, null, 2)}
68
-
69
- Return a JSON object with:
70
- - valid (boolean)
71
- - quality_score (0.0 to 1.0)
72
- - issues (array of objects { field, message, severity })
73
- - suggestions (array of objects { field, suggestion })
74
- `;
75
-
76
- const res = await callOllama(providerUrl, model, [{ role: 'user', content: prompt }]);
77
- if (res.status !== 200) throw new Error(`Ollama Error: ${res.status}`);
78
-
79
- return typeof res.data.message.content === 'string'
80
- ? JSON.parse(res.data.message.content)
81
- : res.data.message.content;
82
- },
83
-
84
- async generate(intention, options, providerUrl, model) {
85
- const prompt = `You are a JOS Architect. Generate a valid .jos JSON artifact based on this user intention.
86
-
87
- INTENTION: "${intention}"
88
-
89
- REQUIREMENTS:
90
- - Type: ${options.type || 'task'}
91
- - Format: JOS v0.0.7 (JSON)
92
- - Must include 'intention', 'tasks', and 'orchestration'.
93
-
94
- Return ONLY the JSON.
95
- `;
96
-
97
- const res = await callOllama(providerUrl, model, [{ role: 'user', content: prompt }]);
98
- if (res.status !== 200) throw new Error(`Ollama Error: ${res.status}`);
99
-
100
- const artifact = typeof res.data.message.content === 'string'
101
- ? JSON.parse(res.data.message.content)
102
- : res.data.message.content;
103
-
104
- return {
105
- confidence: 0.85,
106
- artifact: artifact,
107
- assumptions: ['Generated by Local Ollama']
108
- };
109
- },
110
-
111
- async execute(artifact, providerUrl, model) {
112
- // MAGIC JOSFOXAI PROMPT WRAPPER
113
- const systemPrompt = `[SYSTEM]
114
- You are an Intention-Native Agent powered by JOSFOXAI.
115
- MAGIC_KEY: JOSFOX_ALPHA_V1
116
-
117
- You are executing the following JOS Artifact.
118
- Strictly adhere to the 'orchestration' flow and 'requirements'.
119
-
120
- ARTIFACT:
121
- ${JSON.stringify(artifact, null, 2)}
122
- `;
123
-
124
- const userPrompt = `Execute the intention defined in the artifact. Report your status.`;
125
-
126
- const res = await callOllama(providerUrl, model, [
127
- { role: 'system', content: systemPrompt },
128
- { role: 'user', content: userPrompt }
129
- ]);
130
-
131
- if (res.status !== 200) throw new Error(`Ollama Error: ${res.status}`);
132
-
133
- // For non-native adapters, the result is text/chat, which we wrap in a JOS result structure
134
- return {
135
- status: 'completed',
136
- output: typeof res.data.message.content === 'string' ? res.data.message.content : JSON.stringify(res.data.message.content),
137
- provider: 'ollama'
138
- };
139
- }
140
- };
@@ -1,145 +0,0 @@
1
- /**
2
- * JOS ADD Command - Add task definitions to .jos artifacts
3
- * Format version v0.0.7 — Specification maturity v0.1.0 (Alpha)
4
- */
5
-
6
- const fs = require('fs');
7
- const path = require('path');
8
- const crypto = require('crypto');
9
-
10
- const C = {
11
- reset: '\x1b[0m', bold: '\x1b[1m',
12
- purple: '\x1b[38;5;135m', cyan: '\x1b[38;5;51m',
13
- green: '\x1b[38;5;78m', red: '\x1b[38;5;196m',
14
- yellow: '\x1b[38;5;220m', gray: '\x1b[38;5;245m'
15
- };
16
-
17
- exports.execute = async (args, home) => {
18
- const help = args.includes('--help') || args.includes('-h');
19
-
20
- if (help || args.length < 2) {
21
- console.log(`
22
- ${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
23
- ${C.bold}JOS ADD${C.reset} // Add task definitions to .jos artifacts
24
- ${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
25
-
26
- ${C.bold}Usage:${C.reset}
27
- jos add <task-name> <file.jos> [options]
28
-
29
- ${C.bold}Options:${C.reset}
30
- --type <shell|jos> Task type (default: shell)
31
- --command <cmd> Shell command to execute
32
- --artifact <path> Path to sub-artifact (for type=jos)
33
- --description <desc> Task description
34
- --flow <name> Also add to this flow's steps
35
-
36
- ${C.bold}Examples:${C.reset}
37
- jos add build my-artifact.jos --command "npm run build"
38
- jos add deploy my-artifact.jos --type jos --artifact ./deploy.jos
39
- jos add test my-artifact.jos --command "npm test" --flow main
40
- `);
41
- return;
42
- }
43
-
44
- const taskName = args[0];
45
- const targetFile = args[1];
46
-
47
- // Parse options
48
- const getArg = (flag) => {
49
- const idx = args.indexOf(flag);
50
- return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
51
- };
52
-
53
- const taskType = getArg('--type') || 'shell';
54
- const command = getArg('--command');
55
- const artifact = getArg('--artifact');
56
- const description = getArg('--description') || `Task: ${taskName}`;
57
- const flowName = getArg('--flow');
58
-
59
- // Validate
60
- if (taskType === 'shell' && !command) {
61
- console.log(`${C.red}✖ Error: --command is required for shell tasks${C.reset}`);
62
- process.exit(1);
63
- }
64
- if (taskType === 'jos' && !artifact) {
65
- console.log(`${C.red}✖ Error: --artifact is required for jos tasks${C.reset}`);
66
- process.exit(1);
67
- }
68
-
69
- // Load artifact
70
- let artifactPath = path.resolve(targetFile);
71
- if (!fs.existsSync(artifactPath)) {
72
- console.log(`${C.red}✖ Artifact not found: ${targetFile}${C.reset}`);
73
- process.exit(1);
74
- }
75
-
76
- let content;
77
- try {
78
- content = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
79
- } catch (e) {
80
- console.log(`${C.red}✖ Invalid JSON: ${e.message}${C.reset}`);
81
- process.exit(1);
82
- }
83
-
84
- // Ensure orchestration structure exists
85
- if (!content.orchestration) content.orchestration = {};
86
- if (!content.orchestration.definitions) content.orchestration.definitions = {};
87
- if (!content.orchestration.flows) content.orchestration.flows = {};
88
- if (!content.orchestration.flows.main) content.orchestration.flows.main = { steps: [] };
89
-
90
- // Check for duplicate
91
- if (content.orchestration.definitions[taskName]) {
92
- console.log(`${C.yellow}⚠ Task '${taskName}' already exists. Overwriting...${C.reset}`);
93
- }
94
-
95
- // Add task definition
96
- const taskDef = {
97
- type: taskType,
98
- description: description
99
- };
100
-
101
- if (taskType === 'shell') {
102
- taskDef.command = command;
103
- } else if (taskType === 'jos') {
104
- taskDef.artifact = artifact;
105
- }
106
-
107
- content.orchestration.definitions[taskName] = taskDef;
108
- console.log(`${C.green}✓ Added task definition: ${taskName}${C.reset}`);
109
-
110
- // Add to flow if specified
111
- if (flowName) {
112
- if (!content.orchestration.flows[flowName]) {
113
- content.orchestration.flows[flowName] = { steps: [] };
114
- }
115
- if (!content.orchestration.flows[flowName].steps.includes(taskName)) {
116
- content.orchestration.flows[flowName].steps.push(taskName);
117
- console.log(`${C.green}✓ Added to flow: ${flowName}${C.reset}`);
118
- }
119
- }
120
-
121
- // Write updated artifact
122
- fs.writeFileSync(artifactPath, JSON.stringify(content, null, 2));
123
- console.log(`${C.green}✓ Updated: ${targetFile}${C.reset}`);
124
-
125
- // Regenerate manifest
126
- const manifestPath = artifactPath + '.sig.json';
127
- const newContent = fs.readFileSync(artifactPath, 'utf8');
128
- const sha256 = crypto.createHash('sha256').update(newContent).digest('hex');
129
- const stats = fs.statSync(artifactPath);
130
-
131
- const manifest = {
132
- "$schema": "https://josfox.ai/schemas/jos-manifest-0.0.7.json",
133
- "manifest_version": "1.0.0",
134
- "artifact_ref": path.basename(artifactPath),
135
- "artifact_sha256": sha256,
136
- "artifact_size_bytes": stats.size,
137
- "timestamp": new Date().toISOString(),
138
- "generated_by": "jos add command",
139
- "canonical_version": "Format version v0.0.7 — Specification maturity v0.1.0 (Alpha)"
140
- };
141
-
142
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
143
- console.log(`${C.green}✓ Updated manifest: ${path.basename(manifestPath)}${C.reset}`);
144
- console.log(`${C.gray} SHA256: ${sha256}${C.reset}`);
145
- };
@@ -1,245 +0,0 @@
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
- };