quapp 1.0.5 โ†’ 1.1.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.
package/README.md CHANGED
@@ -1,29 +1,194 @@
1
- # Quapp CLI ๐Ÿงช
2
-
3
- **Quapp CLI** is a developer-friendly CLI tool to serve and Build quapps in a quick and Easy manner
4
-
5
- ---
6
-
7
- ## ๐Ÿš€ Features
8
-
9
- - โšก Create Quapp's Quickly and Efficently
10
- - ๐Ÿ“ฑ Serve locally and share via LAN QR code
11
- - ๐Ÿ“ Lightweight and extensible project setup
12
-
13
- ---
14
-
15
- ## ๐Ÿ“ฆ Installation
16
-
17
- ```bash
18
- npm install quapp
19
-
20
- ```
21
- ## ๐Ÿ”ง Usage
22
-
23
- ```bash
24
- quapp serve
25
-
26
- quapp build
27
-
28
- quapp
29
- ```
1
+ # quapp
2
+
3
+ Development CLI for Quapp projects - start a dev server with LAN QR code and build `.qpp` packages.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -D quapp
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### `quapp init`
14
+
15
+ Initialize Quapp in an existing project. Creates config and adds scripts.
16
+
17
+ ```bash
18
+ quapp init [options]
19
+ ```
20
+
21
+ **Options:**
22
+ | Flag | Short | Description |
23
+ |------|-------|-------------|
24
+ | `--yes` | `-y` | Skip confirmation prompt |
25
+ | `--force` | `-f` | Overwrite existing config/scripts |
26
+ | `--dry-run` | | Preview changes without applying |
27
+
28
+ ### `quapp serve`
29
+
30
+ Start development server with LAN access and QR code for mobile testing.
31
+
32
+ ```bash
33
+ quapp serve [options]
34
+ ```
35
+
36
+ **Options:**
37
+ | Flag | Short | Description |
38
+ |------|-------|-------------|
39
+ | `--port <port>` | `-p` | Port to run on (default: 5173) |
40
+ | `--host <host>` | | Host to bind to |
41
+ | `--open` | | Open browser automatically |
42
+ | `--no-qr` | | Disable QR code display |
43
+ | `--https` | | Enable HTTPS |
44
+
45
+ ### `quapp build`
46
+
47
+ Build for production and create `.qpp` package.
48
+
49
+ ```bash
50
+ quapp build [options]
51
+ ```
52
+
53
+ **Options:**
54
+ | Flag | Short | Description |
55
+ |------|-------|-------------|
56
+ | `--output <file>` | `-o` | Output filename (default: dist.qpp) |
57
+ | `--no-clean` | | Keep dist folder after build |
58
+ | `--skip-prompts` | | Skip interactive prompts |
59
+
60
+ ## Global Options
61
+
62
+ | Flag | Short | Description |
63
+ |------|-------|-------------|
64
+ | `--json` | | Output as JSON (for automation/AI) |
65
+ | `--no-color` | | Disable colored output |
66
+ | `--verbose` | | Show detailed logs |
67
+ | `--version` | `-v` | Show version |
68
+ | `--help` | `-h` | Show help |
69
+
70
+ ## Configuration
71
+
72
+ Create `quapp.config.json` in your project root:
73
+
74
+ ```json
75
+ {
76
+ "server": {
77
+ "port": 5173,
78
+ "qr": true,
79
+ "network": "private",
80
+ "openBrowser": false,
81
+ "https": false,
82
+ "fallbackPort": true,
83
+ "autoRetry": true,
84
+ "strictPort": false
85
+ },
86
+ "build": {
87
+ "outDir": "dist",
88
+ "outputFile": "dist.qpp"
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## Examples
94
+
95
+ ```bash
96
+ # Initialize Quapp in existing project
97
+ quapp init
98
+
99
+ # Initialize without prompts (AI-friendly)
100
+ quapp init --yes --json
101
+
102
+ # Start dev server
103
+ quapp serve
104
+
105
+ # Start on specific port
106
+ quapp serve -p 3000
107
+
108
+ # Start and open browser
109
+ quapp serve --open
110
+
111
+ # Build for production
112
+ quapp build
113
+
114
+ # Build with custom output name
115
+ quapp build -o my-app.qpp
116
+
117
+ # Build with JSON output (AI-friendly)
118
+ quapp build --json --skip-prompts
119
+ ```
120
+
121
+ ## AI/Automation Usage
122
+
123
+ For non-interactive environments:
124
+
125
+ ```bash
126
+ # Initialize without prompts
127
+ quapp init --yes --json
128
+
129
+ # Preview init changes
130
+ quapp init --dry-run --json
131
+
132
+ # Build without prompts
133
+ quapp build --skip-prompts --json
134
+
135
+ # Build with custom output
136
+ quapp build -o myapp.qpp --skip-prompts --json
137
+ ```
138
+
139
+ ### JSON Output Examples
140
+
141
+ Init success:
142
+ ```json
143
+ {
144
+ "success": true,
145
+ "changes": ["quapp.config.json", "script:dev", "script:qbuild", "devDependency:quapp"],
146
+ "nextSteps": ["npm install", "npm run dev"]
147
+ }
148
+ ```
149
+
150
+ Build success:
151
+ ```json
152
+ {
153
+ "success": true,
154
+ "outputFile": "dist.qpp",
155
+ "outputPath": "/path/to/project/dist.qpp",
156
+ "manifest": {
157
+ "package_name": "com.author.myapp",
158
+ "version": "1.0.0",
159
+ "version_code": 10000
160
+ },
161
+ "duration": 5230
162
+ }
163
+ ```
164
+
165
+ Error (with suggestion for AI):
166
+ ```json
167
+ {
168
+ "success": false,
169
+ "errorCode": "NO_BUILD_SCRIPT",
170
+ "error": "No build script",
171
+ "suggestion": "Add to package.json: \"scripts\": { \"build\": \"vite build\" }"
172
+ }
173
+ ```
174
+
175
+ ## Exit Codes
176
+
177
+ | Code | Description |
178
+ |------|-------------|
179
+ | 0 | Success |
180
+ | 1 | General error |
181
+ | 2 | Invalid arguments |
182
+ | 3 | Build failed |
183
+ | 4 | Configuration error |
184
+ | 5 | Missing dependency |
185
+ | 130 | User cancelled |
186
+
187
+ ## Requirements
188
+
189
+ - Node.js >= 18.0.0
190
+ - Vite (in your project's dependencies)
191
+
192
+ ## License
193
+
194
+ MIT
package/bin/cli.js CHANGED
@@ -1,48 +1,134 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawn } from "child_process";
4
- import path from "path";
5
- import { fileURLToPath } from "url";
6
-
7
- // Resolve __dirname in ESM
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
-
11
- // Get user command
12
- const args = process.argv.slice(2);
13
- const command = args[0];
14
-
15
- // Path to files inside your package
16
- const buildPath = path.join(__dirname, "../build.js");
17
- const servePath = path.join(__dirname, "../server.js");
18
-
19
- // Helper to run a file with Node
20
- function runScript(scriptPath) {
21
- const child = spawn("node", [scriptPath], { stdio: "inherit" });
22
- child.on("close", (code) => process.exit(code));
23
- }
24
-
25
- switch (command) {
26
- case "build":
27
- runScript(buildPath);
28
- break;
29
- case "serve":
30
- runScript(servePath);
31
- break;
32
- default:
33
- console.log(`
34
- \x1b[1m\x1b[34mQuapp CLI\x1b[0m
35
-
36
- Usage:
37
- quapp build Run production build and compress to dist.quapp
38
- quapp serve Start local server for testing your app
39
-
40
- Options:
41
- -h, --help Show this help message
42
-
43
- Examples:
44
- quapp build
45
- quapp serve
46
- `);
47
- break;
48
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * quapp CLI - Development and build tools for Quapp projects
5
+ *
6
+ * Commands:
7
+ * quapp serve - Start development server with LAN access
8
+ * quapp build - Build for production and create .qpp package
9
+ */
10
+
11
+ import { parseArgs, getHelpText, getVersion } from '../lib/args.js';
12
+ import { initColors } from '../lib/colors.js';
13
+ import * as logger from '../lib/logger.js';
14
+ import { EXIT_CODES } from '../lib/constants.js';
15
+ import { runServe } from '../commands/serve.js';
16
+ import { runBuild } from '../commands/build.js';
17
+ import { runInit } from '../commands/init.js';
18
+
19
+ // ============================================================================
20
+ // Main Entry Point
21
+ // ============================================================================
22
+
23
+ async function main() {
24
+ // Parse arguments
25
+ const args = parseArgs(process.argv.slice(2));
26
+
27
+ // Handle --help
28
+ if (args.help) {
29
+ console.log(getHelpText());
30
+ process.exit(EXIT_CODES.SUCCESS);
31
+ }
32
+
33
+ // Handle --version
34
+ if (args.version) {
35
+ console.log(getVersion());
36
+ process.exit(EXIT_CODES.SUCCESS);
37
+ }
38
+
39
+ // Initialize colors
40
+ initColors(args.noColor);
41
+
42
+ // Initialize logger
43
+ logger.initLogger({ json: args.json, verbose: args.verbose });
44
+
45
+ // Check for argument errors
46
+ if (args.errors.length > 0) {
47
+ for (const err of args.errors) {
48
+ logger.error(err);
49
+ }
50
+ if (args.json) {
51
+ logger.outputJson({ success: false, errors: args.errors });
52
+ }
53
+ process.exit(EXIT_CODES.INVALID_ARGS);
54
+ }
55
+
56
+ // Handle missing command
57
+ if (!args.command) {
58
+ console.log(getHelpText());
59
+ process.exit(EXIT_CODES.SUCCESS);
60
+ }
61
+
62
+ // Route to command
63
+ let result;
64
+
65
+ switch (args.command) {
66
+ case 'serve':
67
+ result = await runServe({
68
+ port: args.port,
69
+ host: args.host,
70
+ qr: args.qr,
71
+ open: args.open,
72
+ https: args.https,
73
+ extra: args.extra,
74
+ _attempt: 0,
75
+ });
76
+ break;
77
+
78
+ case 'build':
79
+ result = await runBuild({
80
+ output: args.output,
81
+ clean: args.clean,
82
+ skipPrompts: args.skipPrompts,
83
+ });
84
+ break;
85
+
86
+ case 'init':
87
+ result = await runInit({
88
+ force: args.force,
89
+ yes: args.yes,
90
+ dryRun: args.dryRun,
91
+ skipPrompts: args.skipPrompts,
92
+ });
93
+ break;
94
+
95
+ default:
96
+ logger.error(`Unknown command: ${args.command}`);
97
+ logger.info('Run "quapp --help" for available commands');
98
+
99
+ if (args.json) {
100
+ logger.outputJson({ success: false, error: `Unknown command: ${args.command}` });
101
+ }
102
+ process.exit(EXIT_CODES.INVALID_ARGS);
103
+ }
104
+
105
+ // Output JSON result if requested
106
+ if (args.json && result) {
107
+ logger.outputJson(result);
108
+ }
109
+
110
+ // Exit with appropriate code
111
+ process.exit(result?.success ? EXIT_CODES.SUCCESS : (result?.exitCode || EXIT_CODES.GENERAL_ERROR));
112
+ }
113
+
114
+ // ============================================================================
115
+ // Run
116
+ // ============================================================================
117
+
118
+ main().catch((err) => {
119
+ console.error(`\x1b[31mโœ– Unexpected error: ${err.message}\x1b[0m`);
120
+
121
+ if (process.argv.includes('--verbose')) {
122
+ console.error(err.stack);
123
+ }
124
+
125
+ if (process.argv.includes('--json')) {
126
+ console.log(JSON.stringify({
127
+ success: false,
128
+ error: err.message,
129
+ stack: process.argv.includes('--verbose') ? err.stack : undefined,
130
+ }, null, 2));
131
+ }
132
+
133
+ process.exit(EXIT_CODES.GENERAL_ERROR);
134
+ });
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Build command - Create production .qpp package
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import { rm } from 'fs/promises';
7
+ import path from 'path';
8
+ import { execSync } from 'child_process';
9
+ import archiver from 'archiver';
10
+ import prompts from 'prompts';
11
+ import * as logger from '../lib/logger.js';
12
+ import { loadConfig, loadPackageJson, updatePackageJson, hasBuildScript } from '../lib/config.js';
13
+ import { generateManifest, writeManifest } from '../lib/manifest.js';
14
+ import { EXIT_CODES } from '../lib/constants.js';
15
+
16
+ /**
17
+ * Prompt for missing package.json fields
18
+ * @param {Object} pkg - Current package.json
19
+ * @param {string[]} missing - List of missing fields
20
+ * @returns {Promise<Object>} Updated values
21
+ */
22
+ async function promptMissingFields(pkg, missing) {
23
+ const questions = [];
24
+
25
+ if (missing.includes('name')) {
26
+ questions.push({
27
+ type: 'text',
28
+ name: 'name',
29
+ message: 'Enter project name:',
30
+ validate: (x) => x.trim() !== '' ? true : 'Name is required',
31
+ });
32
+ }
33
+
34
+ if (missing.includes('version')) {
35
+ questions.push({
36
+ type: 'text',
37
+ name: 'version',
38
+ message: 'Enter version (e.g., 1.0.0):',
39
+ initial: '1.0.0',
40
+ validate: (x) => x.trim() !== '' ? true : 'Version is required',
41
+ });
42
+ }
43
+
44
+ // Always ask for author if not present
45
+ if (!pkg.author) {
46
+ questions.push({
47
+ type: 'text',
48
+ name: 'author',
49
+ message: 'Enter author name:',
50
+ validate: (x) => x.trim() !== '' ? true : 'Author is required',
51
+ });
52
+ }
53
+
54
+ if (questions.length === 0) {
55
+ return {};
56
+ }
57
+
58
+ return await prompts(questions);
59
+ }
60
+
61
+ /**
62
+ * Compress directory to .qpp file
63
+ * @param {string} sourceDir - Directory to compress
64
+ * @param {string} outputPath - Output file path
65
+ * @returns {Promise<Object>} Result with file size
66
+ */
67
+ async function compressToQpp(sourceDir, outputPath) {
68
+ return new Promise((resolve, reject) => {
69
+ const output = fs.createWriteStream(outputPath);
70
+ const archive = archiver('zip', { zlib: { level: 9 } });
71
+
72
+ output.on('close', () => {
73
+ resolve({
74
+ success: true,
75
+ size: archive.pointer(),
76
+ path: outputPath,
77
+ });
78
+ });
79
+
80
+ archive.on('error', (err) => {
81
+ reject(err);
82
+ });
83
+
84
+ archive.pipe(output);
85
+ archive.directory(sourceDir, false);
86
+ archive.finalize();
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Format bytes to human readable string
92
+ * @param {number} bytes
93
+ * @returns {string}
94
+ */
95
+ function formatSize(bytes) {
96
+ if (bytes < 1024) return `${bytes} B`;
97
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
98
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
99
+ }
100
+
101
+ /**
102
+ * Run the build command
103
+ * @param {Object} options - Command options
104
+ * @returns {Promise<Object>} Result
105
+ */
106
+ export async function runBuild(options = {}) {
107
+ const cwd = process.cwd();
108
+ const startTime = Date.now();
109
+
110
+ // Load config
111
+ const { config, configError } = loadConfig(cwd);
112
+
113
+ if (configError) {
114
+ logger.warn(configError);
115
+ }
116
+
117
+ // Load package.json
118
+ const pkgResult = loadPackageJson(cwd);
119
+
120
+ if (!pkgResult.success) {
121
+ logger.error(pkgResult.error);
122
+ return {
123
+ success: false,
124
+ errorCode: 'PACKAGE_JSON_ERROR',
125
+ error: pkgResult.error,
126
+ suggestion: 'Make sure you are in a Quapp project directory',
127
+ exitCode: EXIT_CODES.CONFIG_ERROR
128
+ };
129
+ }
130
+
131
+ let pkg = pkgResult.package;
132
+
133
+ // Handle missing fields
134
+ if (pkgResult.missingFields.length > 0 || !pkg.author) {
135
+ if (options.skipPrompts) {
136
+ // In non-interactive mode, use defaults for missing fields
137
+ const updates = {};
138
+
139
+ if (pkgResult.missingFields.includes('name')) {
140
+ logger.error('Missing required field: name');
141
+ return { success: false, error: 'Missing name in package.json', exitCode: EXIT_CODES.CONFIG_ERROR };
142
+ }
143
+
144
+ if (pkgResult.missingFields.includes('version')) {
145
+ updates.version = '1.0.0';
146
+ logger.warn('No version specified, using 1.0.0');
147
+ }
148
+
149
+ if (!pkg.author) {
150
+ updates.author = 'developer';
151
+ logger.warn('No author specified, using "developer"');
152
+ }
153
+
154
+ if (Object.keys(updates).length > 0) {
155
+ const updateResult = updatePackageJson(cwd, updates);
156
+ if (updateResult.success) {
157
+ pkg = updateResult.package;
158
+ }
159
+ }
160
+ } else {
161
+ // Interactive mode - prompt for missing fields
162
+ const answers = await promptMissingFields(pkg, pkgResult.missingFields);
163
+
164
+ if (Object.keys(answers).length > 0) {
165
+ const updateResult = updatePackageJson(cwd, answers);
166
+ if (!updateResult.success) {
167
+ logger.error(updateResult.error);
168
+ return { success: false, error: updateResult.error, exitCode: EXIT_CODES.CONFIG_ERROR };
169
+ }
170
+ pkg = updateResult.package;
171
+ logger.success('Updated package.json');
172
+ }
173
+ }
174
+ }
175
+
176
+ // Check for build script
177
+ if (!hasBuildScript(pkg)) {
178
+ logger.error('No "build" script found in package.json');
179
+ logger.info('Add a build script to your package.json, e.g.: "build": "vite build"');
180
+ return {
181
+ success: false,
182
+ errorCode: 'NO_BUILD_SCRIPT',
183
+ error: 'No build script',
184
+ suggestion: 'Add to package.json: "scripts": { "build": "vite build" }',
185
+ exitCode: EXIT_CODES.CONFIG_ERROR
186
+ };
187
+ }
188
+
189
+ // Paths
190
+ const distDir = path.join(cwd, config.build.outDir);
191
+ let outputFile = options.output || config.build.outputFile;
192
+ // Ensure .qpp extension
193
+ if (!outputFile.endsWith('.qpp')) {
194
+ outputFile = `${outputFile}.qpp`;
195
+ }
196
+ const outputPath = path.join(cwd, outputFile);
197
+
198
+ // Step 1: Run build
199
+ logger.step('๐Ÿ“ฆ', 'Building for production...');
200
+
201
+ try {
202
+ execSync('npm run build', {
203
+ cwd,
204
+ stdio: logger.isJsonMode() ? 'pipe' : 'inherit'
205
+ });
206
+ logger.success('Build completed');
207
+ } catch (err) {
208
+ logger.error('Build failed');
209
+ return { success: false, error: 'Build failed', exitCode: EXIT_CODES.BUILD_FAILED };
210
+ }
211
+
212
+ // Step 2: Verify dist folder exists
213
+ if (!fs.existsSync(distDir)) {
214
+ logger.error(`Build output directory "${config.build.outDir}" not found`);
215
+ logger.info('Make sure your build script outputs to the correct directory');
216
+ return { success: false, error: 'Build output not found', exitCode: EXIT_CODES.BUILD_FAILED };
217
+ }
218
+
219
+ // Step 3: Generate and write manifest
220
+ logger.step('๐Ÿ“‹', 'Generating manifest...');
221
+
222
+ const manifest = generateManifest(pkg);
223
+ const manifestResult = writeManifest(distDir, manifest);
224
+
225
+ if (!manifestResult.success) {
226
+ logger.error(manifestResult.error);
227
+ return { success: false, error: manifestResult.error, exitCode: EXIT_CODES.GENERAL_ERROR };
228
+ }
229
+
230
+ logger.success('Manifest created');
231
+ logger.debug(`Package: ${manifest.package_name}`);
232
+ logger.debug(`Version: ${manifest.version} (code: ${manifest.version_code})`);
233
+
234
+ // Step 4: Compress to .qpp
235
+ logger.step('๐Ÿ—œ๏ธ', `Compressing to ${outputFile}...`);
236
+
237
+ try {
238
+ const compressResult = await compressToQpp(distDir, outputPath);
239
+ logger.success(`Created ${outputFile} (${formatSize(compressResult.size)})`);
240
+ } catch (err) {
241
+ logger.error(`Failed to create ${outputFile}: ${err.message}`);
242
+ return { success: false, error: 'Compression failed', exitCode: EXIT_CODES.GENERAL_ERROR };
243
+ }
244
+
245
+ // Step 5: Clean up dist folder
246
+ if (options.clean !== false) {
247
+ try {
248
+ await rm(distDir, { recursive: true, force: true });
249
+ logger.debug('Cleaned up dist folder');
250
+ } catch (err) {
251
+ logger.warn(`Could not remove dist folder: ${err.message}`);
252
+ }
253
+ }
254
+
255
+ // Done
256
+ const duration = Date.now() - startTime;
257
+ logger.newline();
258
+ logger.success(`Build complete in ${(duration / 1000).toFixed(1)}s`);
259
+
260
+ return {
261
+ success: true,
262
+ outputFile,
263
+ outputPath,
264
+ manifest,
265
+ duration,
266
+ };
267
+ }