@airmoney-degn/airmoney-cli 0.23.0 → 0.24.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.
@@ -0,0 +1,109 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.buildCommand = buildCommand;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const child_process_1 = require("child_process");
41
+ const metadata_1 = require("../util/metadata");
42
+ const tarball_1 = require("../util/tarball");
43
+ const LogService_1 = require("../service/log/LogService");
44
+ async function buildCommand({ locationFolder }) {
45
+ const projectPath = locationFolder ? path.resolve(process.cwd(), locationFolder) : process.cwd();
46
+ const meta = await (0, metadata_1.loadMetadata)(projectPath);
47
+ if (!meta) {
48
+ throw new Error('No metadata.json found.');
49
+ }
50
+ // Populate build metadata before building
51
+ (0, LogService_1.log)('Populating build metadata...').white();
52
+ (0, metadata_1.populateBuildMetadata)(meta, projectPath);
53
+ (0, metadata_1.saveMetadata)(meta, projectPath);
54
+ if (meta.type !== 'Native') {
55
+ (0, LogService_1.log)('Build is only required for Native (Rust) applications. For Web apps, use your preferred build tool (e.g., npm run build).').white();
56
+ return;
57
+ }
58
+ // Early validation of required assets
59
+ const requiredAssets = ['dapp-logo.png'];
60
+ for (const asset of requiredAssets) {
61
+ if (!fs.existsSync(path.join(projectPath, asset))) {
62
+ throw new Error(`Missing required asset: ${asset}. Please ensure it exists at the root of your project.`);
63
+ }
64
+ }
65
+ (0, LogService_1.log)(`Building native application: ${meta.displayName}...`).white();
66
+ try {
67
+ // 1. Run cross build
68
+ (0, LogService_1.log)('Executing cross build for aarch64-unknown-linux-gnu...').white();
69
+ // Ensure we are in the project directory for the exec call
70
+ (0, child_process_1.execSync)('cross build --target aarch64-unknown-linux-gnu --release', {
71
+ cwd: projectPath,
72
+ stdio: 'inherit',
73
+ });
74
+ // 2. Find the binary
75
+ // Using standardized binary name "user_app" to match degn-launcher expectations
76
+ const binName = "user_app";
77
+ const binPath = path.join(projectPath, 'target', 'aarch64-unknown-linux-gnu', 'release', binName);
78
+ if (!fs.existsSync(binPath)) {
79
+ // Fallback: try to find any executable in the release folder if the name doesn't match exactly
80
+ (0, LogService_1.log)(`Binary not found at expected path: ${binPath}`).white();
81
+ throw new Error(`Could not find native binary for app: ${binName}`);
82
+ }
83
+ (0, LogService_1.log)(`Native binary found: ${binPath}`).green();
84
+ // 3. Prepare packaging in a temp directory
85
+ (0, LogService_1.log)('Preparing package...').white();
86
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'airmoney-build-'));
87
+ try {
88
+ // Copy required files to temp dir
89
+ fs.copyFileSync(path.join(projectPath, 'metadata.json'), path.join(tempDir, 'metadata.json'));
90
+ const logoPath = path.join(projectPath, 'dapp-logo.png');
91
+ if (fs.existsSync(logoPath)) {
92
+ fs.copyFileSync(logoPath, path.join(tempDir, 'dapp-logo.png'));
93
+ }
94
+ // Copy the binary as "user_app" for degn-launcher compatibility
95
+ fs.copyFileSync(binPath, path.join(tempDir, "user_app"));
96
+ // 4. Pack the project
97
+ await (0, tarball_1.packProject)(meta, tempDir);
98
+ (0, LogService_1.log)('Build and packaging complete!').green();
99
+ }
100
+ finally {
101
+ // Cleanup temp dir
102
+ fs.rmSync(tempDir, { recursive: true, force: true });
103
+ }
104
+ }
105
+ catch (err) {
106
+ (0, LogService_1.log)(`Build failed: ${err.message}`).red();
107
+ throw err;
108
+ }
109
+ }
@@ -32,21 +32,95 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.createCommand = createCommand;
37
40
  const fs = __importStar(require("fs"));
38
41
  const path = __importStar(require("path"));
39
42
  const os = __importStar(require("os"));
43
+ const inquirer_1 = __importDefault(require("inquirer"));
40
44
  const types_1 = require("../types");
41
45
  const child_process_1 = require("child_process");
42
46
  const LogService_1 = require("../service/log/LogService");
47
+ const env_1 = require("../util/env");
48
+ const DappService_1 = require("../service/dapp/DappService");
43
49
  async function createCommand({ name, template, locationFolder, quiet, }) {
44
50
  try {
51
+ const questions = [
52
+ {
53
+ type: 'input',
54
+ name: 'displayName',
55
+ message: 'Display Name:',
56
+ default: name,
57
+ validate: (input) => input.trim() !== '' || 'Display Name is required',
58
+ },
59
+ {
60
+ type: 'input',
61
+ name: 'description',
62
+ message: 'Description:',
63
+ validate: (input) => input.trim() !== '' || 'Description is required',
64
+ },
65
+ {
66
+ type: 'input',
67
+ name: 'author',
68
+ message: 'Author:',
69
+ validate: (input) => input.trim() !== '' || 'Author is required',
70
+ },
71
+ {
72
+ type: 'list',
73
+ name: 'type',
74
+ message: 'App Type:',
75
+ choices: [
76
+ { name: 'Web (Standard)', value: 'Web' },
77
+ { name: 'Native (Rust/Slint) - Experimental', value: 'Native' },
78
+ ],
79
+ default: 'Web',
80
+ },
81
+ {
82
+ type: 'input',
83
+ name: 'version',
84
+ message: 'Version:',
85
+ default: '0.0.1',
86
+ },
87
+ ];
88
+ const answers = await inquirer_1.default.prompt(questions);
45
89
  const identifier = `com.degn.${name}`;
46
- (0, LogService_1.log)(`Initializing project: ${name}`).white();
90
+ (0, LogService_1.log)(`Initializing project: ${name} [${answers.type}]`).white();
91
+ // Duplicate Check
92
+ try {
93
+ const { userId, apiKey, network } = (0, env_1.validateCredential)();
94
+ const dappService = new DappService_1.DappService(userId, apiKey, network);
95
+ const isAvailable = await dappService.checkDappName(identifier);
96
+ if (!isAvailable) {
97
+ (0, LogService_1.log)(`\nWARNING: The identifier '${identifier}' is already registered on-chain.`).red();
98
+ (0, LogService_1.log)(`You may not be able to publish this DApp unless you own it.`).red();
99
+ const { proceed } = await inquirer_1.default.prompt([
100
+ {
101
+ type: 'confirm',
102
+ name: 'proceed',
103
+ message: 'Do you want to proceed anyway?',
104
+ default: false,
105
+ },
106
+ ]);
107
+ if (!proceed) {
108
+ (0, LogService_1.log)('Aborting project creation.').yellow();
109
+ return;
110
+ }
111
+ }
112
+ }
113
+ catch (err) {
114
+ // If credentials aren't setup, we can't check, so we just proceed with a warning
115
+ (0, LogService_1.log)('Notice: Could not verify identifier availability (credentials not found).').yellow();
116
+ }
47
117
  const folderName = locationFolder || name;
48
118
  const projectPath = path.join(process.cwd(), folderName);
49
119
  if (template) {
120
+ // ... (existing template clone logic)
121
+ // Note: Templates are currently Web-only.
122
+ // If user selected Native but used --template, we might want to warn or handle it.
123
+ // For now, let's assume template flag overrides the scaffolding.
50
124
  (0, LogService_1.log)('cloning project').white();
51
125
  const tempGitDir = path.join(os.tmpdir(), `airmoney-git-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`);
52
126
  // Clean up if it already exists (shouldn't happen, but just in case)
@@ -72,8 +146,122 @@ async function createCommand({ name, template, locationFolder, quiet, }) {
72
146
  }
73
147
  else {
74
148
  fs.mkdirSync(projectPath, { recursive: true });
75
- // replicate Rust logic
76
- const indexHtml = `
149
+ if (answers.type === 'Native') {
150
+ // Native Rust/Slint Scaffolding
151
+ // Cargo.toml
152
+ const cargoToml = `[package]
153
+ name = "user_app"
154
+ version = "0.1.0"
155
+ edition = "2021"
156
+
157
+ [dependencies]
158
+ slint = "1.0.0"
159
+
160
+ [workspace]
161
+ `;
162
+ fs.writeFileSync(path.join(projectPath, 'Cargo.toml'), cargoToml, 'utf8');
163
+ // src/main.rs
164
+ const srcDir = path.join(projectPath, 'src');
165
+ fs.mkdirSync(srcDir, { recursive: true });
166
+ const mainRs = `slint::slint! {
167
+ export component MainWindow inherits Window {
168
+ callback close_app();
169
+ VerticalLayout {
170
+ alignment: center;
171
+ Text {
172
+ text: "Hello from ${name} (Native)";
173
+ color: green;
174
+ horizontal-alignment: center;
175
+ }
176
+ Rectangle {
177
+ height: 50px;
178
+ width: 150px;
179
+ background: red;
180
+ border-radius: 4px;
181
+ TouchArea {
182
+ clicked => { root.close_app(); }
183
+ }
184
+ Text {
185
+ text: "Close App";
186
+ color: white;
187
+ x: (parent.width - self.width) / 2;
188
+ y: (parent.height - self.height) / 2;
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ fn main() {
196
+ let main_window = MainWindow::new().unwrap();
197
+ main_window.window().set_fullscreen(true);
198
+
199
+ let weak_window = main_window.as_weak();
200
+ main_window.on_close_app(move || {
201
+ // Call degn-launcher to close this app
202
+ // We use a simple TCP connection to avoid adding reqwest dependency for just this
203
+ if let Ok(mut stream) = std::net::TcpStream::connect("127.0.0.1:7070") {
204
+ use std::io::Write;
205
+ let _ = stream.write_all(b"POST /close HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n");
206
+ } else {
207
+ eprintln!("Failed to connect to launcher control server");
208
+ std::process::exit(0);
209
+ }
210
+ });
211
+
212
+ main_window.run().unwrap();
213
+ }
214
+ `;
215
+ fs.writeFileSync(path.join(srcDir, 'main.rs'), mainRs, 'utf8');
216
+ // .gitignore
217
+ fs.writeFileSync(path.join(projectPath, '.gitignore'), '/target\n', 'utf8');
218
+ // Cross.toml
219
+ const crossToml = `[target.aarch64-unknown-linux-gnu]
220
+ dockerfile = "./cross/Dockerfile"
221
+
222
+ [target.aarch64-unknown-linux-gnu.env]
223
+ PASSTHROUGH = ["PKG_CONFIG_ALLOW_CROSS"]
224
+
225
+ [build]
226
+ docker-default-platform = "linux/amd64"
227
+ `;
228
+ fs.writeFileSync(path.join(projectPath, 'Cross.toml'), crossToml, 'utf8');
229
+ // cross/Dockerfile
230
+ const crossDir = path.join(projectPath, 'cross');
231
+ fs.mkdirSync(crossDir, { recursive: true });
232
+ const dockerfile = `FROM --platform=linux/amd64 ubuntu:22.04
233
+
234
+ ENV DEBIAN_FRONTEND=noninteractive
235
+
236
+ RUN apt-get update && apt-get install -y \\
237
+ build-essential \\
238
+ gcc-aarch64-linux-gnu \\
239
+ libc6-dev-arm64-cross \\
240
+ curl \\
241
+ git \\
242
+ pkg-config \\
243
+ cmake \\
244
+ libwayland-dev \\
245
+ libxkbcommon-dev \\
246
+ libfontconfig1-dev \\
247
+ libssl-dev \\
248
+ && rm -rf /var/lib/apt/lists/*
249
+
250
+ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
251
+ ENV PATH="/root/.cargo/bin:\${PATH}"
252
+
253
+ RUN rustup target add aarch64-unknown-linux-gnu
254
+
255
+ ENV PKG_CONFIG_ALLOW_CROSS=1
256
+ ENV TARGET_CC=aarch64-linux-gnu-gcc
257
+ ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
258
+ `;
259
+ fs.writeFileSync(path.join(crossDir, 'Dockerfile'), dockerfile, 'utf8');
260
+ }
261
+ else {
262
+ // Web Scaffolding
263
+ // replicate Rust logic
264
+ const indexHtml = `
77
265
  <html>
78
266
  <head>
79
267
  <title>Sample ${name}</title>
@@ -83,17 +271,28 @@ async function createCommand({ name, template, locationFolder, quiet, }) {
83
271
  </body>
84
272
  </html>
85
273
  `.trim();
86
- fs.writeFileSync(path.join(projectPath, 'index.html'), indexHtml, 'utf8');
87
- // assets
88
- const assetsDir = path.join(projectPath, 'assets');
89
- fs.mkdirSync(assetsDir, { recursive: true });
90
- fs.writeFileSync(path.join(assetsDir, 'main.js'), '// sample main.js\n', 'utf8');
91
- fs.writeFileSync(path.join(assetsDir, 'style.css'), '/* sample style.css */\n', 'utf8');
274
+ fs.writeFileSync(path.join(projectPath, 'index.html'), indexHtml, 'utf8');
275
+ // assets
276
+ const assetsDir = path.join(projectPath, 'assets');
277
+ fs.mkdirSync(assetsDir, { recursive: true });
278
+ fs.writeFileSync(path.join(assetsDir, 'main.js'), '// sample main.js\n', 'utf8');
279
+ fs.writeFileSync(path.join(assetsDir, 'style.css'), '/* sample style.css */\n', 'utf8');
280
+ }
92
281
  }
93
282
  // metadata
94
283
  const pkg = (0, types_1.createPackage)(name, identifier);
284
+ pkg.displayName = answers.displayName;
285
+ pkg.description = answers.description;
286
+ pkg.author = answers.author;
287
+ pkg.version = answers.version;
288
+ pkg.type = answers.type;
289
+ pkg.maintainer = answers.author; // Default maintainer to author
95
290
  const metaStr = JSON.stringify(pkg, null, 2);
96
291
  fs.writeFileSync(path.join(projectPath, 'metadata.json'), metaStr, 'utf8');
292
+ // Create a default placeholder logo
293
+ const placeholderLogoBase64 = 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAuSURBVHgB7cExAQAAAMKg9U9tCj8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALwCReAAAd7O664AAAAASUVORK5CYII=';
294
+ const logoBuffer = Buffer.from(placeholderLogoBase64, 'base64');
295
+ fs.writeFileSync(path.join(projectPath, 'dapp-logo.png'), logoBuffer);
97
296
  (0, LogService_1.log)(`Project '${name}' created successfully.`).green();
98
297
  if (!quiet) {
99
298
  (0, LogService_1.log)(`\nNext steps:`).white();
@@ -107,6 +306,10 @@ async function createCommand({ name, template, locationFolder, quiet, }) {
107
306
  (0, LogService_1.log)(' npm run build').white();
108
307
  (0, LogService_1.log)(' airmoney-cli serve -f dist').white();
109
308
  }
309
+ else if (answers.type === 'Native') {
310
+ (0, LogService_1.log)(' cargo run').white();
311
+ (0, LogService_1.log)(' # To build for device, ensure you have the correct target installed.').white();
312
+ }
110
313
  else {
111
314
  (0, LogService_1.log)(' airmoney-cli serve').white();
112
315
  }
@@ -38,15 +38,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.uploadCommand = uploadCommand;
40
40
  const fs = __importStar(require("fs"));
41
+ const crypto = __importStar(require("crypto"));
41
42
  const node_fetch_1 = __importDefault(require("node-fetch"));
42
- const md5_1 = __importDefault(require("md5"));
43
43
  const path_1 = __importDefault(require("path"));
44
- const inquirer_1 = __importDefault(require("inquirer"));
45
44
  const metadata_1 = require("../util/metadata");
46
45
  const tarball_1 = require("../util/tarball");
47
46
  const network_1 = require("../util/network");
48
47
  const env_1 = require("../util/env");
49
48
  const LogService_1 = require("../service/log/LogService");
49
+ const DappService_1 = require("../service/dapp/DappService");
50
+ const os = __importStar(require("os"));
51
+ const inquirer_1 = __importDefault(require("inquirer"));
50
52
  const format_1 = require("../util/format");
51
53
  /**
52
54
  * Gets the project path based on location folder
@@ -100,23 +102,63 @@ function formatFileSize(bytes) {
100
102
  }
101
103
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
102
104
  }
105
+ /**
106
+ * Calculates the MD5 hash of a file using streams to avoid OOM
107
+ */
108
+ async function calculateFileHash(filePath) {
109
+ return new Promise((resolve, reject) => {
110
+ const hash = crypto.createHash('md5');
111
+ const input = fs.createReadStream(filePath);
112
+ input.on('error', reject);
113
+ input.on('data', chunk => hash.update(chunk));
114
+ input.on('end', () => {
115
+ resolve(hash.digest('hex'));
116
+ });
117
+ });
118
+ }
103
119
  /**
104
120
  * Prepares the package by packing and reading it
105
121
  */
106
122
  async function preparePackage(meta, projectPath) {
107
123
  const pkgName = (0, metadata_1.getPackageName)(meta);
108
- (0, LogService_1.log)(`Packing ${pkgName}...`).white();
109
- await (0, tarball_1.packProject)(meta, projectPath);
124
+ if (meta.type === 'Native') {
125
+ (0, LogService_1.log)(`Preparing Native package for ${meta.displayName}...`).white();
126
+ // 1. Find the binary
127
+ const binName = "user_app";
128
+ const binPath = path_1.default.join(projectPath, 'target', 'aarch64-unknown-linux-gnu', 'release', binName);
129
+ if (!fs.existsSync(binPath)) {
130
+ throw new Error('Native binary not found. Please run "airmoney-cli build" first to compile your application.');
131
+ }
132
+ // 2. Prepare packaging in a temp directory to avoid including target/ and other junk
133
+ const tempDir = fs.mkdtempSync(path_1.default.join(os.tmpdir(), 'airmoney-upload-'));
134
+ try {
135
+ fs.copyFileSync(path_1.default.join(projectPath, 'metadata.json'), path_1.default.join(tempDir, 'metadata.json'));
136
+ const logoPath = path_1.default.join(projectPath, 'dapp-logo.png');
137
+ if (fs.existsSync(logoPath)) {
138
+ fs.copyFileSync(logoPath, path_1.default.join(tempDir, 'dapp-logo.png'));
139
+ }
140
+ fs.copyFileSync(binPath, path_1.default.join(tempDir, "user_app"));
141
+ (0, LogService_1.log)(`Packing ${pkgName}...`).white();
142
+ await (0, tarball_1.packProject)(meta, tempDir);
143
+ }
144
+ finally {
145
+ fs.rmSync(tempDir, { recursive: true, force: true });
146
+ }
147
+ }
148
+ else {
149
+ (0, LogService_1.log)(`Packing Web package ${pkgName}...`).white();
150
+ await (0, tarball_1.packProject)(meta, projectPath);
151
+ }
110
152
  const pkgPath = path_1.default.join(process.cwd(), pkgName);
111
- if (!fs.existsSync(pkgPath)) {
153
+ const stats = fs.statSync(pkgPath);
154
+ if (!stats.isFile()) {
112
155
  throw new Error(`Package file not found at ${pkgPath}`);
113
156
  }
114
- const fileBuffer = fs.readFileSync(pkgPath);
115
- const fileHash = (0, md5_1.default)(fileBuffer);
116
- const fileSize = fileBuffer.length;
157
+ const fileHash = await calculateFileHash(pkgPath);
158
+ const fileSize = stats.size;
117
159
  (0, LogService_1.log)(`Package Hash: ${fileHash}`).white();
118
160
  (0, LogService_1.log)(`Package Size: ${formatFileSize(fileSize)}`).white();
119
- return { fileBuffer, fileHash, pkgPath };
161
+ return { fileHash, pkgPath, fileSize };
120
162
  }
121
163
  /**
122
164
  * Creates the JSON-RPC request body for upload
@@ -129,16 +171,17 @@ async function preparePackage(meta, projectPath) {
129
171
  /**
130
172
  * Uploads the package to the server
131
173
  */
132
- async function uploadPackageToServer(network, userId, apiKey, meta, fileBuffer) {
174
+ async function uploadPackageToServer(network, userId, apiKey, meta, pkgPath, fileSize) {
133
175
  const origin = (0, network_1.networkToRpcUrl)(network).replace(/\/+$/, '');
134
176
  const metaParam = encodeURIComponent(JSON.stringify(meta));
135
177
  const url = `${origin}/upload?user=${userId}&apiKey=${apiKey}&meta=${metaParam}`;
136
178
  const res = await (0, node_fetch_1.default)(url, {
137
179
  method: 'POST',
138
- // raw bytes, no base64 overhead
139
- body: fileBuffer,
180
+ // Stream the file directly from disk
181
+ body: fs.createReadStream(pkgPath),
140
182
  headers: {
141
183
  'Content-Type': 'application/octet-stream',
184
+ 'Content-Length': fileSize.toString(),
142
185
  },
143
186
  });
144
187
  const text = await res.text();
@@ -184,9 +227,24 @@ async function uploadCommand({ network, locationFolder, }) {
184
227
  (0, LogService_1.log)(`Upload folder: ${path_1.default.resolve(projectPath)}`).white();
185
228
  }
186
229
  const meta = await loadAndValidateMetadata(locationFolder);
230
+ // Version Conflict Check
231
+ const dappService = new DappService_1.DappService(userId, apiKey, network);
232
+ (0, LogService_1.log)(`Checking for existing versions of ${meta.name}...`).white();
233
+ const existingBuilds = await dappService.fetchDappVersions(meta.name);
234
+ const conflict = existingBuilds.find(b => b.version === meta.version);
235
+ if (conflict) {
236
+ (0, LogService_1.log)(`\nERROR: Version ${meta.version} already exists on the server.`).red();
237
+ (0, LogService_1.log)(`Status: ${conflict.status || 'Unknown'}`).red();
238
+ (0, LogService_1.log)(`Please bump your version in metadata.json before uploading.`).yellow();
239
+ throw new Error(`Version conflict: ${meta.version} already exists.`);
240
+ }
241
+ // Populate build metadata before upload/packaging
242
+ (0, LogService_1.log)('Populating build metadata...').white();
243
+ (0, metadata_1.populateBuildMetadata)(meta, projectPath);
244
+ (0, metadata_1.saveMetadata)(meta, projectPath);
187
245
  packageData = await preparePackage(meta, projectPath);
188
246
  (0, LogService_1.log)('Publishing package to DEGN Dapp Store...').white();
189
- await uploadPackageToServer(network || "devnet", userId, apiKey, meta, packageData.fileBuffer);
247
+ await uploadPackageToServer(network || "devnet", userId, apiKey, meta, packageData.pkgPath, packageData.fileSize);
190
248
  }
191
249
  catch (err) {
192
250
  const errorMessage = err instanceof Error ? err.message : String(err);
package/dist/config.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.23.0"
2
+ "version": "0.24.0"
3
3
  }
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ const commander_1 = require("commander");
5
5
  const demo_1 = require("./cli/demo");
6
6
  const env_1 = require("./util/env");
7
7
  const create_1 = require("./cli/create");
8
+ const build_1 = require("./cli/build");
8
9
  const serve_1 = require("./cli/serve");
9
10
  const upload_1 = require("./cli/upload");
10
11
  const setup_1 = require("./cli/setup");
@@ -49,6 +50,19 @@ program
49
50
  process.exit(1);
50
51
  }
51
52
  });
53
+ program
54
+ .command('build')
55
+ .description('Build and package the project')
56
+ .option('-f, --app-path <string>', 'path to the project')
57
+ .action(async (opts) => {
58
+ try {
59
+ const { appPath } = opts;
60
+ await (0, build_1.buildCommand)({ locationFolder: appPath });
61
+ }
62
+ catch (err) {
63
+ process.exit(1);
64
+ }
65
+ });
52
66
  program
53
67
  .command('serve')
54
68
  .description('Serve locally in the simulator')
@@ -87,6 +87,12 @@ class DappService {
87
87
  async validateApiKey() {
88
88
  return await this.makeJsonRpcRequest('checkApiKey', [this.userId, this.apiKey]);
89
89
  }
90
+ /**
91
+ * Checks if a dapp name (identifier) is available
92
+ */
93
+ async checkDappName(name) {
94
+ return await this.makeJsonRpcRequest('checkDappName', [this.userId, this.apiKey, name]);
95
+ }
90
96
  /**
91
97
  * Fetches dapp list from API
92
98
  */
@@ -43,17 +43,17 @@ class LogBuilder {
43
43
  this.execute();
44
44
  }
45
45
  /**
46
- * Sets the color to white and logs
46
+ * Sets the color to yellow and logs
47
47
  */
48
- white() {
49
- this.color = 'white';
48
+ yellow() {
49
+ this.color = 'yellow';
50
50
  this.execute();
51
51
  }
52
52
  /**
53
- * Sets the color to yellow and logs
53
+ * Sets the color to white and logs
54
54
  */
55
- yellow() {
56
- this.color = 'yellow';
55
+ white() {
56
+ this.color = 'white';
57
57
  this.execute();
58
58
  }
59
59
  /**
package/dist/types.js CHANGED
@@ -8,6 +8,8 @@ function createPackage(name, identifier) {
8
8
  displayName: '',
9
9
  author: '',
10
10
  maintainer: '',
11
+ description: '',
12
+ type: 'Web',
11
13
  url: '',
12
14
  themeColor: '',
13
15
  version: '0.0.1',
@@ -36,9 +36,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadMetadata = loadMetadata;
37
37
  exports.saveMetadata = saveMetadata;
38
38
  exports.getPackageName = getPackageName;
39
+ exports.populateBuildMetadata = populateBuildMetadata;
39
40
  const fs = __importStar(require("fs"));
40
41
  const path = __importStar(require("path"));
41
42
  const LogService_1 = require("../service/log/LogService");
43
+ const child_process_1 = require("child_process");
42
44
  const remote_1 = require("./remote");
43
45
  async function loadMetadata(projectPath = '.', appUrl) {
44
46
  try {
@@ -82,6 +84,34 @@ function saveMetadata(pkg, projectPath = '.') {
82
84
  fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2), 'utf8');
83
85
  }
84
86
  function getPackageName(pkg) {
85
- // e.g. com.degn.myApp-0.1.0.tar.gz
86
- return `${pkg.identifier}-${pkg.version}.tar.gz`;
87
+ // e.g. myApp-0.1.0.zip
88
+ return `${pkg.name}-${pkg.version}.zip`;
89
+ }
90
+ function populateBuildMetadata(pkg, projectPath) {
91
+ // 1. Build Number: Increment if numeric, or use timestamp
92
+ const currentBuild = parseInt(pkg.buildNumber || '0', 10);
93
+ if (!isNaN(currentBuild)) {
94
+ pkg.buildNumber = (currentBuild + 1).toString();
95
+ }
96
+ else {
97
+ pkg.buildNumber = Math.floor(Date.now() / 1000).toString();
98
+ }
99
+ // 2. Commit Hash
100
+ try {
101
+ const hash = (0, child_process_1.execSync)('git rev-parse HEAD', { cwd: projectPath, stdio: ['ignore', 'pipe', 'ignore'] })
102
+ .toString()
103
+ .trim();
104
+ pkg.commitHash = hash;
105
+ }
106
+ catch (err) {
107
+ pkg.commitHash = 'n/a';
108
+ }
109
+ // 3. Build Date: YYYYMMDD-HHmm (local)
110
+ const now = new Date();
111
+ const year = now.getFullYear();
112
+ const month = String(now.getMonth() + 1).padStart(2, '0');
113
+ const day = String(now.getDate()).padStart(2, '0');
114
+ const hours = String(now.getHours()).padStart(2, '0');
115
+ const minutes = String(now.getMinutes()).padStart(2, '0');
116
+ pkg.buildDate = `${year}${month}${day}-${hours}${minutes}`;
87
117
  }
@@ -39,11 +39,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.packProject = packProject;
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
- const tar = __importStar(require("tar"));
43
- const md5_1 = __importDefault(require("md5"));
42
+ const crypto = __importStar(require("crypto"));
43
+ const archiver_1 = __importDefault(require("archiver"));
44
44
  const metadata_1 = require("./metadata");
45
45
  const LogService_1 = require("../service/log/LogService");
46
- const REQUIRED_FILES = ['metadata.json', 'dapp-logo.png', 'index.html'];
46
+ const REQUIRED_FILES_WEB = ['metadata.json', 'dapp-logo.png', 'index.html'];
47
+ const REQUIRED_FILES_NATIVE = ['metadata.json', 'dapp-logo.png'];
47
48
  /**
48
49
  * Validates that the project path exists
49
50
  */
@@ -55,9 +56,10 @@ function validateProjectPath(projectPath) {
55
56
  /**
56
57
  * Validates that all required files exist at the root of the project
57
58
  */
58
- function validateRequiredFiles(projectPath) {
59
+ function validateRequiredFiles(projectPath, pkg) {
59
60
  const missingFiles = [];
60
- REQUIRED_FILES.forEach(file => {
61
+ const required = pkg.type === 'Native' ? REQUIRED_FILES_NATIVE : REQUIRED_FILES_WEB;
62
+ required.forEach(file => {
61
63
  const filePath = path.join(projectPath, file);
62
64
  if (!fs.existsSync(filePath)) {
63
65
  missingFiles.push(file);
@@ -68,78 +70,77 @@ function validateRequiredFiles(projectPath) {
68
70
  }
69
71
  }
70
72
  /**
71
- * Recursively gets all files from a directory
73
+ * Creates the zip file
72
74
  */
73
- function getAllFiles(dir, baseDir, fileList = []) {
74
- const items = fs.readdirSync(dir);
75
- items.forEach(item => {
76
- const fullPath = path.join(dir, item);
77
- const stat = fs.statSync(fullPath);
78
- if (stat.isDirectory()) {
79
- getAllFiles(fullPath, baseDir, fileList);
80
- }
81
- else {
82
- const relativePath = path.relative(baseDir, fullPath);
83
- fileList.push(relativePath);
84
- }
85
- });
86
- return fileList;
87
- }
88
- /**
89
- * Collects all files from the project directory
90
- */
91
- function collectProjectFiles(projectPath) {
92
- const allFiles = getAllFiles(projectPath, projectPath);
93
- if (allFiles.length === 0) {
94
- throw new Error(`No files found in ${projectPath}`);
95
- }
96
- return allFiles;
97
- }
98
- /**
99
- * Creates the tarball file
100
- */
101
- async function createTarball(outputPath, projectPath, files) {
75
+ async function createZip(outputPath, projectPath) {
102
76
  // Clean up any existing output file
103
77
  if (fs.existsSync(outputPath)) {
104
78
  fs.rmSync(outputPath);
105
79
  }
106
- await tar.create({
107
- file: outputPath,
108
- gzip: true,
109
- cwd: projectPath,
110
- prefix: '',
111
- portable: true,
112
- }, files);
113
- if (!fs.existsSync(outputPath)) {
114
- throw new Error('Tarball was not created successfully');
115
- }
80
+ return new Promise((resolve, reject) => {
81
+ const output = fs.createWriteStream(outputPath);
82
+ const archive = (0, archiver_1.default)('zip', {
83
+ zlib: { level: 9 } // Sets the compression level.
84
+ });
85
+ output.on('close', function () {
86
+ resolve();
87
+ });
88
+ archive.on('warning', function (err) {
89
+ if (err.code === 'ENOENT') {
90
+ // log warning
91
+ }
92
+ else {
93
+ reject(err);
94
+ }
95
+ });
96
+ archive.on('error', function (err) {
97
+ reject(err);
98
+ });
99
+ archive.pipe(output);
100
+ // append files from a directory, putting its contents at the root of archive
101
+ // ignore common junk files
102
+ archive.directory(projectPath, false, (data) => {
103
+ const ignoredFolders = ['node_modules', '.git', 'target', '.DS_Store'];
104
+ if (ignoredFolders.some(folder => data.name.startsWith(folder))) {
105
+ return false;
106
+ }
107
+ return data;
108
+ });
109
+ archive.finalize();
110
+ });
116
111
  }
117
112
  /**
118
- * Calculates and logs the MD5 hash of the tarball
113
+ * Calculates and logs the MD5 hash of the zip using streams to avoid OOM
119
114
  */
120
- function logTarballHash(outputPath) {
121
- const buffer = fs.readFileSync(outputPath);
122
- const digest = (0, md5_1.default)(buffer);
123
- (0, LogService_1.log)(`MD5: ${digest}`).white();
115
+ async function logPackageHash(outputPath) {
116
+ return new Promise((resolve, reject) => {
117
+ const hash = crypto.createHash('md5');
118
+ const input = fs.createReadStream(outputPath);
119
+ input.on('error', reject);
120
+ input.on('data', chunk => hash.update(chunk));
121
+ input.on('end', () => {
122
+ const digest = hash.digest('hex');
123
+ (0, LogService_1.log)(`MD5: ${digest}`).white();
124
+ resolve();
125
+ });
126
+ });
124
127
  }
125
128
  /**
126
- * Main function to pack a project into a tarball
129
+ * Main function to pack a project into a zip file
127
130
  */
128
131
  async function packProject(pkg, projectPath) {
129
132
  const outputFilename = (0, metadata_1.getPackageName)(pkg);
130
133
  const absOutputPath = path.join(process.cwd(), outputFilename);
131
134
  try {
132
135
  validateProjectPath(projectPath);
133
- validateRequiredFiles(projectPath);
134
- const allFiles = collectProjectFiles(projectPath);
135
- (0, LogService_1.log)(`Found ${allFiles.length} files to pack`).white();
136
- (0, LogService_1.log)('Creating tarball...').white();
137
- await createTarball(absOutputPath, projectPath, allFiles);
138
- logTarballHash(absOutputPath);
139
- (0, LogService_1.log)(`Tarball created at ${absOutputPath}`).white();
136
+ validateRequiredFiles(projectPath, pkg);
137
+ (0, LogService_1.log)('Creating package...').white();
138
+ await createZip(absOutputPath, projectPath);
139
+ await logPackageHash(absOutputPath);
140
+ (0, LogService_1.log)(`Package created at ${absOutputPath}`).white();
140
141
  }
141
142
  catch (err) {
142
- (0, LogService_1.log)(`Failed to create tarball: ${err.message}`).red();
143
+ (0, LogService_1.log)(`Failed to create package: ${err.message}`).red();
143
144
  if (fs.existsSync(absOutputPath)) {
144
145
  fs.rmSync(absOutputPath);
145
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airmoney-degn/airmoney-cli",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "airmoney-cli is a command-line interface tool designed to facilitate the development and management of decentralized applications (DApps) for Airmoney.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -31,6 +31,8 @@
31
31
  "dependencies": {
32
32
  "@solana/signers": "2.1.1",
33
33
  "@solana/web3.js": "1.98.2",
34
+ "@types/archiver": "^7.0.0",
35
+ "archiver": "^7.0.1",
34
36
  "base64-js": "^1.5.1",
35
37
  "bs58": "5.0.0",
36
38
  "commander": "^11.0.0",
@@ -69,4 +71,4 @@
69
71
  "typescript": "^5.0.4"
70
72
  },
71
73
  "packageManager": "npm@>=10.9.3 <11"
72
- }
74
+ }
package/readme.md CHANGED
@@ -7,18 +7,40 @@ It simplifies the process of creating, serving, and publishing applications to t
7
7
  ## Features
8
8
 
9
9
  - **Setup**: Setup your development environment with ease.
10
- - **Create**: Initialize new projects with a standard template structure.
10
+ - **Create**: Initialize new projects with standard Web or Native (Rust/Slint) templates.
11
11
  - **Serve**: Test your project locally for development and testing on Simulator.
12
+ - **Build**: Cross-compile native applications for AirMoney ARM devices.
12
13
  - **Upload**: Package and send your project to the DAPP store.
13
14
 
14
15
  ## Getting Started
15
- install `airmoney-cli` using `npm`.
16
16
 
17
- ### Installation with npm
18
- Before you begin, ensure you have Node.js and npm installed on your machine.
19
-
17
+ ### Installation
18
+
19
+ Install `airmoney-cli` using `npm`:
20
+
21
+ ```bash
22
+ npm install -g @airmoney-degn/airmoney-cli
23
+ ```
24
+
25
+ ### Native App Development Requirements
26
+
27
+ To build and package **Native Applications**, you must have the following installed:
28
+ 1. **Docker**: Used by `cross` for cross-compilation environments.
29
+ 2. **Rust & Cargo**: Standard Rust toolchain.
30
+ 3. **Cross**: Install via `cargo install cross --git https://github.com/cross-rs/cross`.
31
+
32
+ ## Commands
33
+
34
+ ### 1. Setup
35
+ Configure your developer address and API key.
36
+ ```bash
37
+ airmoney-cli setup -u <YOUR_WALLET_ADDRESS> -k <YOUR_API_KEY>
38
+ ```
39
+
40
+ ### 2. Create Project
41
+ Initialize a new project.
20
42
  ```bash
21
- npm install -g airmoney-cli
43
+ airmoney-cli create -N my-app
22
44
  ```
23
45
 
24
46
  ## Release Process