@josfox/jos 3.1.1 → 4.0.2

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,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
+ }