@yamo/cli 1.3.13 → 1.3.14

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 CHANGED
@@ -90,6 +90,44 @@ yamo audit block_001
90
90
  yamo audit block_001 --key "my-secret"
91
91
  ```
92
92
 
93
+ ### `yamo config <action> [key] [value]`
94
+ Manage local configuration and secrets persistently. Actions: `set`, `get`, `list`, `remove`.
95
+ Values are stored in `~/.yamo/config.json`. Sensitive keys (e.g., `PRIVATE_KEY`) are masked in `list`.
96
+
97
+ ```bash
98
+ # Set a configuration value
99
+ yamo config set PRIVATE_KEY 0x...
100
+
101
+ # List current configuration
102
+ yamo config list
103
+
104
+ # Get a specific value
105
+ yamo config get RPC_URL
106
+ ```
107
+
108
+ ### `yamo download-bundle <cid>`
109
+ Downloads a complete YAMO bundle (file + artifacts) from IPFS.
110
+
111
+ * `-o, --output <path>`: Output directory path.
112
+ * `-k, --key <string>`: Decryption key if the bundle is encrypted.
113
+
114
+ ```bash
115
+ yamo download-bundle Qm... -o ./downloads/my-bundle
116
+ ```
117
+
118
+ ### `yamo bridge <subcommand>`
119
+ Interact with the YAMO bridge/cluster. Requires `YAMO_BRIDGE_URL` to be set.
120
+
121
+ * `kernels`: List connected kernels and their capabilities.
122
+ * `status`: Show bridge cluster status.
123
+ * `invoke <skill> --payload <json>`: Invoke a skill via the bridge.
124
+
125
+ ```bash
126
+ yamo bridge status
127
+ yamo bridge kernels
128
+ yamo bridge invoke my-skill -p '{"input": "test"}'
129
+ ```
130
+
93
131
  ## 🔒 Encryption
94
132
 
95
133
  YAMO v1.0 supports optional client-side encryption for IPFS bundles.
@@ -104,4 +142,27 @@ yamo submit task.yamo --ipfs --encrypt --key "my-secret"
104
142
 
105
143
  # Audit encrypted block
106
144
  yamo audit block_001 --key "my-secret"
107
- ```
145
+ ```
146
+
147
+ ## 🧪 Testing
148
+
149
+ Run all tests:
150
+ ```bash
151
+ npm test
152
+ ```
153
+
154
+ Run specific test suite:
155
+ ```bash
156
+ npm test -- test/hash.test.js
157
+ ```
158
+
159
+ **Test Coverage:**
160
+ - Unit tests: 22 tests (hash, init, validation utilities, format utilities, constants)
161
+ - Integration tests: 8 tests (CLI interface, error handling)
162
+ - E2E tests: 2 tests (basic workflows)
163
+ - Security tests: 13 tests (path traversal protection)
164
+ - Validation tests: 3 tests (blockId format)
165
+ - Auto-fetch tests: 5 tests (previousBlock resolution)
166
+ - Health checks: 2 tests (test suite validation)
167
+
168
+ **Total: 54+ tests** with 100% pass rate
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.auditCommand = auditCommand;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const config_js_1 = require("../utils/config.js");
9
+ const format_js_1 = require("../utils/format.js");
10
+ const spinner_js_1 = require("../utils/spinner.js");
11
+ const constants_js_1 = require("../utils/constants.js");
12
+ /**
13
+ * Audit a block's integrity on the blockchain.
14
+ * @param blockId - Block ID to audit
15
+ * @param options - Command options
16
+ * @param deps - Injected dependencies
17
+ */
18
+ async function auditCommand(blockId, options, deps) {
19
+ try {
20
+ const chainSpinner = (0, spinner_js_1.createSpinner)(`Fetching Block ${blockId} from chain...`);
21
+ let block;
22
+ try {
23
+ block = await deps.chainClient.getBlock(blockId);
24
+ if (!block) {
25
+ chainSpinner.fail(`[ERROR] Block ${blockId} not found on-chain.`);
26
+ return;
27
+ }
28
+ chainSpinner.succeed(`[DONE] Found on-chain record for ${blockId}`);
29
+ }
30
+ catch (e) {
31
+ chainSpinner.fail(`[FAILED] Failed to fetch block ${blockId}`);
32
+ throw e;
33
+ }
34
+ format_js_1.format.detail('Record Details:');
35
+ console.log(` Agent: ${block.agentAddress}`);
36
+ console.log(` Hash: ${block.contentHash}`);
37
+ console.log(` IPFS: ${block.ipfsCID || 'None'}`);
38
+ if (!block.ipfsCID) {
39
+ format_js_1.format.warn('No IPFS CID. Cannot perform deep content audit.');
40
+ return;
41
+ }
42
+ const ipfsSpinner = (0, spinner_js_1.createSpinner)('Fetching content from IPFS...');
43
+ try {
44
+ const key = options.key || config_js_1.config.encryptionKey;
45
+ const content = await deps.ipfsClient.download(block.ipfsCID, key);
46
+ const hash = crypto_1.default.createHash(constants_js_1.CONSTANTS.HASH_ALGORITHM).update(content).digest('hex');
47
+ const calcHash = constants_js_1.CONSTANTS.HEX_PREFIX + hash;
48
+ ipfsSpinner.succeed('[DONE] Content retrieved and hashed');
49
+ console.log(` Calculated: ${calcHash}`);
50
+ if (calcHash === block.contentHash) {
51
+ format_js_1.format.success('✅ INTEGRITY VERIFIED: Content matches chain hash.');
52
+ }
53
+ else {
54
+ format_js_1.format.error('INTEGRITY FAILED: Hash mismatch!');
55
+ console.log(` Expected: ${block.contentHash}`);
56
+ console.log(` Got: ${calcHash}`);
57
+ }
58
+ }
59
+ catch (e) {
60
+ ipfsSpinner.fail('[FAILED] IPFS download or hash failed');
61
+ throw e;
62
+ }
63
+ }
64
+ catch (error) {
65
+ (0, format_js_1.handleCommandError)(error);
66
+ }
67
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bridgeCommand = bridgeCommand;
4
+ const commander_1 = require("commander");
5
+ const config_js_1 = require("../utils/config.js");
6
+ const format_js_1 = require("../utils/format.js");
7
+ // ── Helpers ────────────────────────────────────────────────────────────────
8
+ function getBridgeBaseUrl() {
9
+ const url = config_js_1.config.bridgeUrl;
10
+ if (!url) {
11
+ throw new Error('Bridge URL not configured. Set YAMO_BRIDGE_URL env var or run: yamo config set YAMO_BRIDGE_URL http://localhost:4001');
12
+ }
13
+ // Accept ws:// URLs (bridge WebSocket) and convert to http://
14
+ return url.replace(/^ws:\/\//, 'http://').replace(/^wss:\/\//, 'https://').replace(/\/$/, '');
15
+ }
16
+ let _rpcId = 0;
17
+ async function bridgeRpc(baseUrl, method, params = {}) {
18
+ const id = ++_rpcId;
19
+ const body = JSON.stringify({ jsonrpc: '2.0', method, params, id });
20
+ const resp = await fetch(`${baseUrl}/rpc`, {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json' },
23
+ body,
24
+ });
25
+ if (!resp.ok) {
26
+ throw new Error(`Bridge HTTP ${resp.status}: ${resp.statusText}`);
27
+ }
28
+ const json = (await resp.json());
29
+ if (json.error) {
30
+ throw new Error(`Bridge RPC error ${json.error.code}: ${json.error.message}`);
31
+ }
32
+ return json.result;
33
+ }
34
+ // ── Subcommand handlers ────────────────────────────────────────────────────
35
+ async function kernelsHandler() {
36
+ try {
37
+ const baseUrl = getBridgeBaseUrl();
38
+ const result = (await bridgeRpc(baseUrl, 'kernel.list'));
39
+ if (result.count === 0) {
40
+ format_js_1.format.warn('No connected kernels.');
41
+ return;
42
+ }
43
+ format_js_1.format.success(`Connected kernels: ${result.count}`);
44
+ for (const k of result.kernels) {
45
+ format_js_1.format.info(`\n Kernel: ${k.kernel_id}`);
46
+ format_js_1.format.detail(` Status: ${k.status || 'idle'}`);
47
+ format_js_1.format.detail(` Last seen: ${k.last_seen || 'unknown'}`);
48
+ format_js_1.format.detail(` Capabilities: ${k.capabilities?.length ? k.capabilities.join(', ') : 'none'}`);
49
+ }
50
+ }
51
+ catch (error) {
52
+ (0, format_js_1.handleCommandError)(error, 'bridge kernels');
53
+ }
54
+ }
55
+ async function statusHandler() {
56
+ try {
57
+ const baseUrl = getBridgeBaseUrl();
58
+ const result = (await bridgeRpc(baseUrl, 'cluster.status'));
59
+ format_js_1.format.success('Bridge cluster status');
60
+ format_js_1.format.detail(` Node: ${result.node}`);
61
+ format_js_1.format.detail(` Leader: ${result.leader ?? 'unknown'}`);
62
+ format_js_1.format.detail(` Members: ${result.members?.join(', ') || 'none'}`);
63
+ format_js_1.format.detail(` Connected kernels: ${result.connected_kernels}`);
64
+ }
65
+ catch (error) {
66
+ (0, format_js_1.handleCommandError)(error, 'bridge status');
67
+ }
68
+ }
69
+ async function invokeHandler(skill, options) {
70
+ try {
71
+ const baseUrl = getBridgeBaseUrl();
72
+ let payload = {};
73
+ if (options.payload) {
74
+ try {
75
+ payload = JSON.parse(options.payload);
76
+ }
77
+ catch {
78
+ // Treat as plain string if not valid JSON
79
+ payload = options.payload;
80
+ }
81
+ }
82
+ const timeoutMs = options.timeout ? parseInt(options.timeout, 10) : 30_000;
83
+ format_js_1.format.info(`Invoking skill: ${skill}`);
84
+ const result = (await bridgeRpc(baseUrl, 'skill.invoke', {
85
+ skill,
86
+ payload,
87
+ timeout_ms: timeoutMs,
88
+ }));
89
+ format_js_1.format.success(`Skill invocation complete`);
90
+ format_js_1.format.detail(` Skill: ${result.skill}`);
91
+ format_js_1.format.detail(` Handler: ${result.handler_id}`);
92
+ format_js_1.format.value(` Result: ${JSON.stringify(result.result, null, 2)}`);
93
+ }
94
+ catch (error) {
95
+ (0, format_js_1.handleCommandError)(error, 'bridge invoke');
96
+ }
97
+ }
98
+ // ── Command builder ────────────────────────────────────────────────────────
99
+ function bridgeCommand() {
100
+ const bridge = new commander_1.Command('bridge').description('Interact with the YAMO bridge (requires YAMO_BRIDGE_URL)');
101
+ bridge
102
+ .command('kernels')
103
+ .description('List connected kernels and their capabilities')
104
+ .action(kernelsHandler);
105
+ bridge
106
+ .command('status')
107
+ .description('Show bridge cluster status')
108
+ .action(statusHandler);
109
+ bridge
110
+ .command('invoke <skill>')
111
+ .description('Invoke a skill via the bridge')
112
+ .option('-p, --payload <json>', 'JSON payload to pass to the skill handler')
113
+ .option('-t, --timeout <ms>', 'Timeout in milliseconds', '30000')
114
+ .action(invokeHandler);
115
+ return bridge;
116
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configCommand = configCommand;
4
+ const storage_js_1 = require("../utils/storage.js");
5
+ const format_js_1 = require("../utils/format.js");
6
+ /**
7
+ * Handles the 'config' command sub-actions.
8
+ */
9
+ function configCommand(action, key, value) {
10
+ try {
11
+ switch (action) {
12
+ case 'set': {
13
+ if (!key || !value) {
14
+ throw new Error('Usage: yamo config set <key> <value>');
15
+ }
16
+ storage_js_1.storage.set(key, value);
17
+ format_js_1.format.success(`Set ${key} successfully.`);
18
+ break;
19
+ }
20
+ case 'get': {
21
+ if (!key) {
22
+ throw new Error('Usage: yamo config get <key>');
23
+ }
24
+ const val = storage_js_1.storage.get(key);
25
+ if (val) {
26
+ console.log(val);
27
+ }
28
+ else {
29
+ format_js_1.format.warn(`Key '${key}' not found.`);
30
+ }
31
+ break;
32
+ }
33
+ case 'list': {
34
+ const configData = storage_js_1.storage.read();
35
+ const keys = Object.keys(configData);
36
+ if (keys.length === 0) {
37
+ format_js_1.format.info('No configuration found.');
38
+ }
39
+ else {
40
+ format_js_1.format.detail('Current Configuration:');
41
+ keys.forEach((k) => {
42
+ const lowerKey = k.toLowerCase();
43
+ const displayValue = lowerKey.includes('key') ||
44
+ lowerKey.includes('jwt') ||
45
+ lowerKey.includes('secret') ||
46
+ lowerKey.includes('pass')
47
+ ? '********'
48
+ : configData[k];
49
+ console.log(` ${k}: ${displayValue}`);
50
+ });
51
+ }
52
+ break;
53
+ }
54
+ case 'remove': {
55
+ if (!key) {
56
+ throw new Error('Usage: yamo config remove <key>');
57
+ }
58
+ storage_js_1.storage.remove(key);
59
+ format_js_1.format.success(`Removed ${key} successfully.`);
60
+ break;
61
+ }
62
+ default:
63
+ throw new Error('Invalid action. Use set, get, list, or remove.');
64
+ }
65
+ }
66
+ catch (error) {
67
+ if (error instanceof Error) {
68
+ format_js_1.format.error(error.message);
69
+ }
70
+ else {
71
+ format_js_1.format.error('Unknown error occurred');
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.downloadBundleCommand = downloadBundleCommand;
40
+ const fs_1 = __importDefault(require("fs"));
41
+ const path_1 = __importDefault(require("path"));
42
+ const dotenv = __importStar(require("dotenv"));
43
+ const core_1 = require("@yamo/core");
44
+ const format_js_1 = require("../utils/format.js");
45
+ const constants_js_1 = require("../utils/constants.js");
46
+ dotenv.config();
47
+ /**
48
+ * Download a bundle from IPFS.
49
+ * @param cid - IPFS content identifier
50
+ * @param options - Command options
51
+ */
52
+ async function downloadBundleCommand(cid, options) {
53
+ try {
54
+ format_js_1.format.info(`Downloading bundle ${cid}...`);
55
+ const ipfs = new core_1.IpfsManager();
56
+ const key = options.key || process.env.YAMO_ENCRYPTION_KEY;
57
+ const bundle = await ipfs.downloadBundle(cid, key);
58
+ // Create output directory
59
+ const outputDir = options.output.replace('<cid>', cid.substring(0, 8));
60
+ if (!fs_1.default.existsSync(outputDir)) {
61
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
62
+ }
63
+ // Write block.yamo
64
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, constants_js_1.CONSTANTS.DEFAULT_FILENAME), bundle.block);
65
+ format_js_1.format.success(`✓ Downloaded ${constants_js_1.CONSTANTS.DEFAULT_FILENAME}`);
66
+ // Write metadata
67
+ if (bundle.metadata) {
68
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, 'metadata.json'), JSON.stringify(bundle.metadata, null, 2));
69
+ format_js_1.format.success('✓ Downloaded metadata.json');
70
+ }
71
+ // Write artifact files
72
+ for (const [filename, content] of Object.entries(bundle.files)) {
73
+ const filePath = path_1.default.join(outputDir, filename);
74
+ fs_1.default.writeFileSync(filePath, content);
75
+ format_js_1.format.success(`✓ Downloaded ${filename}`);
76
+ }
77
+ format_js_1.format.success(`\nBundle saved to: ${outputDir}`);
78
+ format_js_1.format.detail(`Files: ${1 + Object.keys(bundle.files).length} total`);
79
+ if (bundle.metadata?.hasEncryption) {
80
+ format_js_1.format.warn('🔒 Bundle was decrypted using provided key');
81
+ }
82
+ }
83
+ catch (error) {
84
+ (0, format_js_1.handleCommandError)(error);
85
+ format_js_1.format.detail('\nIf the bundle is encrypted, provide --key or set YAMO_ENCRYPTION_KEY');
86
+ }
87
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.hashCommand = hashCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const constants_js_1 = require("../utils/constants.js");
10
+ const format_js_1 = require("../utils/format.js");
11
+ /**
12
+ * Calculate SHA256 hash of a file.
13
+ * @param file - Path to file
14
+ */
15
+ function hashCommand(file) {
16
+ try {
17
+ if (!fs_1.default.existsSync(file)) {
18
+ throw new Error(`File not found: ${file}`);
19
+ }
20
+ const content = fs_1.default.readFileSync(file, 'utf-8').trim();
21
+ const hash = crypto_1.default.createHash(constants_js_1.CONSTANTS.HASH_ALGORITHM).update(content).digest('hex');
22
+ const bytes32 = constants_js_1.CONSTANTS.HEX_PREFIX + hash;
23
+ format_js_1.format.success('Block Content Hash:');
24
+ format_js_1.format.value(bytes32);
25
+ }
26
+ catch (error) {
27
+ (0, format_js_1.handleCommandError)(error);
28
+ }
29
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.initCommand = initCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const constants_js_1 = require("../utils/constants.js");
10
+ const format_js_1 = require("../utils/format.js");
11
+ /**
12
+ * Initialize a new YAMO block template.
13
+ * @param agentName - Name of the agent
14
+ * @param options - Command options
15
+ */
16
+ function initCommand(agentName, options) {
17
+ try {
18
+ const template = `
19
+ agent: ${agentName};
20
+ intent: ${options.intent};
21
+ context:
22
+ platform;yamo_v0.5;
23
+ constraints:
24
+ - human_readable;true;
25
+ priority: medium;
26
+ output: result.json;
27
+ meta: hypothesis;Initial hypothesis;
28
+ meta: confidence;0.9;
29
+ log: session_start;timestamp;${new Date().toISOString()};
30
+ handoff: User;
31
+ `.trim();
32
+ fs_1.default.writeFileSync(constants_js_1.CONSTANTS.DEFAULT_FILENAME, template);
33
+ format_js_1.format.success(`Created YAMO template: ${chalk_1.default.bold(constants_js_1.CONSTANTS.DEFAULT_FILENAME)}`);
34
+ }
35
+ catch (error) {
36
+ (0, format_js_1.handleCommandError)(error);
37
+ }
38
+ }
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.submitCommand = submitCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ const config_js_1 = require("../utils/config.js");
12
+ const format_js_1 = require("../utils/format.js");
13
+ const spinner_js_1 = require("../utils/spinner.js");
14
+ const validation_js_1 = require("../utils/validation.js");
15
+ const constants_js_1 = require("../utils/constants.js");
16
+ /**
17
+ * Validate bytes32 hash format
18
+ */
19
+ function validateBytes32Hash(value, fieldName) {
20
+ if (!(0, validation_js_1.validateBytes32)(value)) {
21
+ throw new Error(`${fieldName} must be a valid bytes32 hash (0x + 64 hex chars). ` +
22
+ `Received: ${value.substring(0, 20)}...` +
23
+ `\nDo NOT include algorithm prefixes like "sha256:"`);
24
+ }
25
+ }
26
+ /**
27
+ * Validate block ID format
28
+ */
29
+ function validateBlockIdFormat(blockId) {
30
+ if (!blockId)
31
+ throw new Error('blockId is required');
32
+ if (!(0, validation_js_1.validateBlockId)(blockId)) {
33
+ throw new Error(`blockId must follow format {origin}_{workflow} (e.g., 'claude_chain'). Received: ${blockId}`);
34
+ }
35
+ }
36
+ /**
37
+ * Validate encryption key strength
38
+ */
39
+ async function validateEncryptionKey(key) {
40
+ const { validatePasswordStrength } = await import('@yamo/core');
41
+ try {
42
+ validatePasswordStrength(key);
43
+ }
44
+ catch (e) {
45
+ const errorMessage = e instanceof Error ? e.message : 'Unknown validation error';
46
+ format_js_1.format.error('Password validation failed:');
47
+ format_js_1.format.error(errorMessage);
48
+ format_js_1.format.warn('\nKey requirements:');
49
+ console.error(' • Minimum 12 characters');
50
+ console.error(' • Mix of uppercase, lowercase, numbers, symbols');
51
+ console.error(' • Avoid common patterns (password, 123456, qwerty)');
52
+ throw e;
53
+ }
54
+ }
55
+ /**
56
+ * Prepare files for IPFS upload
57
+ */
58
+ function prepareIpfsFiles(content, file) {
59
+ const files = [
60
+ { name: constants_js_1.CONSTANTS.DEFAULT_FILENAME, content },
61
+ ];
62
+ const outputMatch = content.match(/output:\s*([^;]+);/);
63
+ if (outputMatch) {
64
+ const artifactName = outputMatch[1].trim();
65
+ const artifactPath = path_1.default.join(path_1.default.dirname(file), artifactName);
66
+ const resolvedPath = path_1.default.resolve(artifactPath);
67
+ const inputDir = path_1.default.resolve(path_1.default.dirname(file));
68
+ // Security: Validate artifact path
69
+ (0, validation_js_1.validateArtifactPath)(artifactName, resolvedPath, inputDir);
70
+ if (fs_1.default.existsSync(resolvedPath)) {
71
+ format_js_1.format.info(`Bundling output: ${artifactName}`);
72
+ files.push({ name: artifactName, content: fs_1.default.readFileSync(resolvedPath, 'utf8') });
73
+ }
74
+ }
75
+ return files;
76
+ }
77
+ /**
78
+ * Resolve previous block hash
79
+ */
80
+ async function resolvePreviousBlock(chainClient, prev) {
81
+ if (prev) {
82
+ validateBytes32Hash(prev, 'previousBlock');
83
+ return prev;
84
+ }
85
+ format_js_1.format.info('[INFO] No previousBlock provided, fetching latest block from chain...');
86
+ const latestHash = await chainClient.getLatestBlockHash();
87
+ if (latestHash && latestHash !== constants_js_1.CONSTANTS.GENESIS_HASH) {
88
+ format_js_1.format.success(`[INFO] Using latest block's contentHash: ${latestHash}`);
89
+ return latestHash;
90
+ }
91
+ format_js_1.format.warn('[INFO] No existing blocks found, using genesis');
92
+ return constants_js_1.CONSTANTS.GENESIS_HASH;
93
+ }
94
+ /**
95
+ * Submit a YAMO block to the blockchain.
96
+ * @param file - Path to YAMO file
97
+ * @param options - Command options
98
+ * @param deps - Injected dependencies (Chain and IPFS clients)
99
+ */
100
+ async function submitCommand(file, options, deps) {
101
+ try {
102
+ let blockId = options.id;
103
+ // Interactive fallback for blockId
104
+ if (!blockId && process.stdout.isTTY) {
105
+ const answers = await inquirer_1.default.prompt([
106
+ {
107
+ type: 'input',
108
+ name: 'blockId',
109
+ message: 'Enter Unique Block ID (format: {origin}_{workflow}):',
110
+ validate: (input) => ((0, validation_js_1.validateBlockId)(input) ? true : 'Invalid format. Use {origin}_{workflow}'),
111
+ },
112
+ ]);
113
+ blockId = answers.blockId;
114
+ }
115
+ if (!blockId) {
116
+ throw new Error('blockId is required. Provide it via --id or interactively.');
117
+ }
118
+ // Validate inputs
119
+ validateBlockIdFormat(blockId);
120
+ // Validate encryption key if needed
121
+ let encryptionKey;
122
+ if (options.encrypt) {
123
+ let key = options.key || config_js_1.config.encryptionKey;
124
+ if (!key && process.stdout.isTTY) {
125
+ const answers = await inquirer_1.default.prompt([
126
+ {
127
+ type: 'password',
128
+ name: 'key',
129
+ message: 'Enter Encryption Key:',
130
+ mask: '*',
131
+ },
132
+ ]);
133
+ key = answers.key;
134
+ }
135
+ encryptionKey = config_js_1.validateConfig.requireEncryptionKey(key);
136
+ await validateEncryptionKey(encryptionKey);
137
+ }
138
+ // Calculate content hash
139
+ const content = fs_1.default.readFileSync(file, 'utf8').trim();
140
+ const hash = crypto_1.default.createHash(constants_js_1.CONSTANTS.HASH_ALGORITHM).update(content).digest('hex');
141
+ const contentHash = constants_js_1.CONSTANTS.HEX_PREFIX + hash;
142
+ format_js_1.format.info(`Calculated Hash: ${contentHash}`);
143
+ // Handle IPFS upload
144
+ let ipfsCID;
145
+ if (options.ipfs) {
146
+ const spinner = (0, spinner_js_1.createSpinner)('Preparing and uploading to IPFS...');
147
+ try {
148
+ const files = prepareIpfsFiles(content, file);
149
+ ipfsCID = await deps.ipfsClient.upload({ content, files, encryptionKey });
150
+ spinner.succeed(`[DONE] Uploaded to IPFS`);
151
+ format_js_1.format.info(`IPFS Bundle CID: ${ipfsCID}`);
152
+ }
153
+ catch (e) {
154
+ spinner.fail('[FAILED] IPFS upload failed');
155
+ throw e;
156
+ }
157
+ }
158
+ // Resolve previous block
159
+ const resolvedPreviousBlock = await resolvePreviousBlock(deps.chainClient, options.prev);
160
+ // Submit to blockchain
161
+ const txSpinner = (0, spinner_js_1.createSpinner)(`Submitting Block ${blockId} to blockchain...`);
162
+ try {
163
+ await deps.chainClient.submitBlock(blockId, resolvedPreviousBlock, contentHash, options.consensus, options.ledger, ipfsCID);
164
+ txSpinner.succeed(`[DONE] Block ${blockId} anchored to chain`);
165
+ }
166
+ catch (e) {
167
+ txSpinner.fail('[FAILED] Blockchain submission failed');
168
+ throw e;
169
+ }
170
+ }
171
+ catch (error) {
172
+ (0, format_js_1.handleCommandError)(error);
173
+ }
174
+ }