nextdesk 0.1.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.
- package/dist/commands/build.js +57 -0
- package/dist/commands/dev.js +82 -0
- package/dist/commands/init.js +87 -0
- package/dist/index.js +27 -0
- package/dist/utils/nextjs.js +62 -0
- package/package.json +23 -0
- package/src/commands/build.ts +64 -0
- package/src/commands/dev.ts +94 -0
- package/src/commands/init.ts +101 -0
- package/src/index.ts +30 -0
- package/src/utils/nextjs.ts +60 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,57 @@
|
|
|
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.build = build;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const execa_1 = __importDefault(require("execa"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const nextjs_1 = require("../utils/nextjs");
|
|
13
|
+
async function build(options) {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
if (!(0, nextjs_1.isNextJsProject)(cwd)) {
|
|
16
|
+
console.error(chalk_1.default.red('✗ Not a Next.js project. Make sure next.config.js exists.'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const appName = (0, nextjs_1.getAppName)(cwd);
|
|
20
|
+
console.log(chalk_1.default.bold(`\n📦 Building ${appName} for desktop...\n`));
|
|
21
|
+
const nextSpinner = (0, ora_1.default)('Building Next.js app...').start();
|
|
22
|
+
try {
|
|
23
|
+
await (0, execa_1.default)('npm', ['run', 'build'], { cwd, stdio: 'pipe' });
|
|
24
|
+
nextSpinner.succeed('Next.js app built');
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
nextSpinner.fail(`Next.js build failed: ${err.message}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const bundleSpinner = (0, ora_1.default)('Bundling desktop runtime...').start();
|
|
31
|
+
try {
|
|
32
|
+
const outputDir = path_1.default.join(cwd, 'dist-desktop');
|
|
33
|
+
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
34
|
+
const runtime = (0, nextjs_1.getRuntimeBinary)();
|
|
35
|
+
const outputBinary = path_1.default.join(outputDir, appName);
|
|
36
|
+
fs_1.default.copyFileSync(runtime, outputBinary);
|
|
37
|
+
fs_1.default.chmodSync(outputBinary, 0o755);
|
|
38
|
+
const nextOut = path_1.default.join(cwd, '.next');
|
|
39
|
+
fs_1.default.cpSync(nextOut, path_1.default.join(outputDir, '.next'), { recursive: true });
|
|
40
|
+
fs_1.default.writeFileSync(path_1.default.join(outputDir, 'run.sh'), `#!/bin/bash
|
|
41
|
+
PORT=3000
|
|
42
|
+
npm run start &
|
|
43
|
+
sleep 2
|
|
44
|
+
./${appName} http://localhost:$PORT "${appName}"
|
|
45
|
+
`);
|
|
46
|
+
fs_1.default.chmodSync(path_1.default.join(outputDir, 'run.sh'), 0o755);
|
|
47
|
+
bundleSpinner.succeed('Runtime bundled');
|
|
48
|
+
console.log(chalk_1.default.bold('\n✨ Build complete!\n'));
|
|
49
|
+
console.log(chalk_1.default.gray(` Output: ${chalk_1.default.cyan(outputDir)}`));
|
|
50
|
+
console.log(chalk_1.default.gray(` Binary: ${chalk_1.default.cyan(outputBinary)}`));
|
|
51
|
+
console.log(chalk_1.default.gray(` Run: ${chalk_1.default.cyan('cd ' + outputDir + ' && ./run.sh')}\n`));
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
bundleSpinner.fail(`Bundling failed: ${err.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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.dev = dev;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const execa_1 = __importDefault(require("execa"));
|
|
10
|
+
const nextjs_1 = require("../utils/nextjs");
|
|
11
|
+
async function dev(options) {
|
|
12
|
+
const port = options.port;
|
|
13
|
+
const url = `http://localhost:${port}`;
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
// Check if Next.js project
|
|
16
|
+
if (!(0, nextjs_1.isNextJsProject)(cwd)) {
|
|
17
|
+
console.error(chalk_1.default.red('✗ Not a Next.js project. Make sure next.config.js exists.'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const appName = (0, nextjs_1.getAppName)(cwd);
|
|
21
|
+
console.log(chalk_1.default.bold(`\n🚀 MyDesk — ${appName}\n`));
|
|
22
|
+
// Start Next.js dev server
|
|
23
|
+
const nextSpinner = (0, ora_1.default)('Starting Next.js dev server...').start();
|
|
24
|
+
const nextProcess = (0, execa_1.default)('npm', ['run', 'dev'], {
|
|
25
|
+
cwd,
|
|
26
|
+
env: { ...process.env, PORT: port },
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
});
|
|
29
|
+
nextProcess.stdout?.on('data', (data) => {
|
|
30
|
+
const line = data.toString();
|
|
31
|
+
if (line.includes('Ready') || line.includes('ready')) {
|
|
32
|
+
nextSpinner.succeed(`Next.js ready on ${chalk_1.default.cyan(url)}`);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
nextProcess.stderr?.on('data', (data) => {
|
|
36
|
+
const line = data.toString();
|
|
37
|
+
if (line.toLowerCase().includes('error')) {
|
|
38
|
+
console.error(chalk_1.default.red(line.trim()));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// Wait for server to be ready
|
|
42
|
+
try {
|
|
43
|
+
await (0, nextjs_1.waitForServer)(url);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
nextSpinner.fail('Next.js dev server failed to start');
|
|
47
|
+
nextProcess.kill();
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
// Launch desktop window
|
|
51
|
+
const windowSpinner = (0, ora_1.default)('Opening desktop window...').start();
|
|
52
|
+
try {
|
|
53
|
+
const runtime = (0, nextjs_1.getRuntimeBinary)();
|
|
54
|
+
const runtimeProcess = (0, execa_1.default)(runtime, [], {
|
|
55
|
+
env: {
|
|
56
|
+
...process.env,
|
|
57
|
+
MYDESK_URL: url,
|
|
58
|
+
MYDESK_TITLE: appName,
|
|
59
|
+
GDK_BACKEND: 'x11',
|
|
60
|
+
WAYLAND_DISPLAY: '',
|
|
61
|
+
},
|
|
62
|
+
stdio: 'inherit',
|
|
63
|
+
});
|
|
64
|
+
windowSpinner.succeed(`${chalk_1.default.green('✨ Ready!')} Desktop window opened`);
|
|
65
|
+
console.log(chalk_1.default.gray(`\n App: ${chalk_1.default.cyan(url)}`));
|
|
66
|
+
console.log(chalk_1.default.gray(` Runtime: ${runtime}\n`));
|
|
67
|
+
// Clean shutdown
|
|
68
|
+
const cleanup = () => {
|
|
69
|
+
nextProcess.kill();
|
|
70
|
+
runtimeProcess.kill();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
};
|
|
73
|
+
process.on('SIGINT', cleanup);
|
|
74
|
+
process.on('SIGTERM', cleanup);
|
|
75
|
+
await runtimeProcess;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
windowSpinner.fail(`Failed to open desktop window: ${err.message}`);
|
|
79
|
+
nextProcess.kill();
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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.init = init;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const execa_1 = __importDefault(require("execa"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
async function init(options) {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const appName = options.name || path_1.default.basename(cwd);
|
|
15
|
+
console.log(chalk_1.default.bold(`\n🚀 Initializing MyDesk project: ${appName}\n`));
|
|
16
|
+
const spinner = (0, ora_1.default)('Setting up project...').start();
|
|
17
|
+
try {
|
|
18
|
+
// Check if Next.js project exists
|
|
19
|
+
const hasNextConfig = fs_1.default.existsSync(path_1.default.join(cwd, 'next.config.js')) ||
|
|
20
|
+
fs_1.default.existsSync(path_1.default.join(cwd, 'next.config.mjs')) ||
|
|
21
|
+
fs_1.default.existsSync(path_1.default.join(cwd, 'next.config.ts'));
|
|
22
|
+
if (!hasNextConfig) {
|
|
23
|
+
spinner.info('No Next.js project found. Creating one...');
|
|
24
|
+
// Create a basic Next.js app
|
|
25
|
+
await (0, execa_1.default)('npx', ['create-next-app@latest', '.', '--typescript', '--tailwind', '--eslint', '--app', '--src-dir', '--no-import-alias'], {
|
|
26
|
+
cwd,
|
|
27
|
+
stdio: 'inherit'
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// Create .mydesk directory
|
|
31
|
+
const mydeskDir = path_1.default.join(cwd, '.mydesk');
|
|
32
|
+
if (!fs_1.default.existsSync(mydeskDir)) {
|
|
33
|
+
fs_1.default.mkdirSync(mydeskDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
// Copy runtime binary
|
|
36
|
+
const runtimeSrc = path_1.default.join(__dirname, '../../target/release/mydesk-poc');
|
|
37
|
+
const runtimeDst = path_1.default.join(mydeskDir, 'mydesk-poc');
|
|
38
|
+
if (fs_1.default.existsSync(runtimeSrc)) {
|
|
39
|
+
fs_1.default.copyFileSync(runtimeSrc, runtimeDst);
|
|
40
|
+
fs_1.default.chmodSync(runtimeDst, 0o755);
|
|
41
|
+
}
|
|
42
|
+
// Create mydesk.config.js
|
|
43
|
+
const configContent = `export default {
|
|
44
|
+
name: "${appName}",
|
|
45
|
+
version: "1.0.0",
|
|
46
|
+
window: {
|
|
47
|
+
width: 900,
|
|
48
|
+
height: 640,
|
|
49
|
+
title: "${appName}",
|
|
50
|
+
},
|
|
51
|
+
permissions: [
|
|
52
|
+
"fs:read",
|
|
53
|
+
"fs:write",
|
|
54
|
+
"shell:open",
|
|
55
|
+
"clipboard:read",
|
|
56
|
+
"clipboard:write",
|
|
57
|
+
"notification:show",
|
|
58
|
+
],
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
fs_1.default.writeFileSync(path_1.default.join(cwd, 'mydesk.config.js'), configContent);
|
|
62
|
+
// Update package.json scripts
|
|
63
|
+
const packageJsonPath = path_1.default.join(cwd, 'package.json');
|
|
64
|
+
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
65
|
+
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
|
|
66
|
+
if (!packageJson.scripts) {
|
|
67
|
+
packageJson.scripts = {};
|
|
68
|
+
}
|
|
69
|
+
if (!packageJson.scripts['mydesk:dev']) {
|
|
70
|
+
packageJson.scripts['mydesk:dev'] = 'mydesk dev';
|
|
71
|
+
}
|
|
72
|
+
if (!packageJson.scripts['mydesk:build']) {
|
|
73
|
+
packageJson.scripts['mydesk:build'] = 'mydesk build';
|
|
74
|
+
}
|
|
75
|
+
fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
76
|
+
}
|
|
77
|
+
spinner.succeed('MyDesk initialized!');
|
|
78
|
+
console.log(chalk_1.default.bold('\n✨ All set!\n'));
|
|
79
|
+
console.log(chalk_1.default.gray(' Run these commands:'));
|
|
80
|
+
console.log(chalk_1.default.cyan(' npm run mydesk:dev # Start desktop app in dev mode'));
|
|
81
|
+
console.log(chalk_1.default.cyan(' npm run mydesk:build # Build desktop app for production\n'));
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
spinner.fail(`Initialization failed: ${err.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const dev_1 = require("./commands/dev");
|
|
6
|
+
const build_1 = require("./commands/build");
|
|
7
|
+
const init_1 = require("./commands/init");
|
|
8
|
+
commander_1.program
|
|
9
|
+
.name('mydesk')
|
|
10
|
+
.description('Turn your Next.js app into a desktop app in 30 seconds')
|
|
11
|
+
.version('0.1.0');
|
|
12
|
+
commander_1.program
|
|
13
|
+
.command('init')
|
|
14
|
+
.description('Initialize MyDesk in your Next.js project')
|
|
15
|
+
.option('-n, --name <name>', 'App name')
|
|
16
|
+
.action(init_1.init);
|
|
17
|
+
commander_1.program
|
|
18
|
+
.command('dev')
|
|
19
|
+
.description('Start your Next.js app as a desktop app')
|
|
20
|
+
.option('-p, --port <port>', 'Next.js dev server port', '3000')
|
|
21
|
+
.action(dev_1.dev);
|
|
22
|
+
commander_1.program
|
|
23
|
+
.command('build')
|
|
24
|
+
.description('Build your app for production')
|
|
25
|
+
.option('-t, --target <target>', 'Target platform (windows, macos, linux)', process.platform)
|
|
26
|
+
.action(build_1.build);
|
|
27
|
+
commander_1.program.parse();
|
|
@@ -0,0 +1,62 @@
|
|
|
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.isNextJsProject = isNextJsProject;
|
|
7
|
+
exports.getPackageJson = getPackageJson;
|
|
8
|
+
exports.getAppName = getAppName;
|
|
9
|
+
exports.waitForServer = waitForServer;
|
|
10
|
+
exports.getRuntimeBinary = getRuntimeBinary;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const http_1 = __importDefault(require("http"));
|
|
14
|
+
function isNextJsProject(cwd = process.cwd()) {
|
|
15
|
+
return (fs_1.default.existsSync(path_1.default.join(cwd, 'next.config.js')) ||
|
|
16
|
+
fs_1.default.existsSync(path_1.default.join(cwd, 'next.config.ts')) ||
|
|
17
|
+
fs_1.default.existsSync(path_1.default.join(cwd, 'next.config.mjs')));
|
|
18
|
+
}
|
|
19
|
+
function getPackageJson(cwd = process.cwd()) {
|
|
20
|
+
const pkgPath = path_1.default.join(cwd, 'package.json');
|
|
21
|
+
if (!fs_1.default.existsSync(pkgPath))
|
|
22
|
+
return null;
|
|
23
|
+
return JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
function getAppName(cwd = process.cwd()) {
|
|
26
|
+
const pkg = getPackageJson(cwd);
|
|
27
|
+
return pkg?.name ?? path_1.default.basename(cwd);
|
|
28
|
+
}
|
|
29
|
+
function waitForServer(url, timeout = 60000) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const start = Date.now();
|
|
32
|
+
const check = () => {
|
|
33
|
+
http_1.default.get(url, (res) => {
|
|
34
|
+
if (res.statusCode && res.statusCode < 500) {
|
|
35
|
+
resolve();
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
retry();
|
|
39
|
+
}
|
|
40
|
+
}).on('error', retry);
|
|
41
|
+
};
|
|
42
|
+
const retry = () => {
|
|
43
|
+
if (Date.now() - start > timeout) {
|
|
44
|
+
reject(new Error(`Server at ${url} did not start within ${timeout}ms`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setTimeout(check, 500);
|
|
48
|
+
};
|
|
49
|
+
check();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function getRuntimeBinary() {
|
|
53
|
+
// In dev: use the compiled binary from the rust project
|
|
54
|
+
const devBinary = path_1.default.join(__dirname, '../../../target/debug/mydesk-poc');
|
|
55
|
+
if (fs_1.default.existsSync(devBinary))
|
|
56
|
+
return devBinary;
|
|
57
|
+
// In production: binary is bundled next to the CLI
|
|
58
|
+
const prodBinary = path_1.default.join(__dirname, '../bin/mydesk-runtime');
|
|
59
|
+
if (fs_1.default.existsSync(prodBinary))
|
|
60
|
+
return prodBinary;
|
|
61
|
+
throw new Error('MyDesk runtime binary not found. Run `cargo build` first.');
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextdesk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Turn your Next.js app into a 5MB desktop app in 30 seconds",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nextdesk": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
10
|
+
"dev": "tsc --watch",
|
|
11
|
+
"start": "node dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"commander": "^12.0.0",
|
|
15
|
+
"chalk": "^4.1.2",
|
|
16
|
+
"ora": "^5.4.1",
|
|
17
|
+
"execa": "^5.1.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.0.0",
|
|
21
|
+
"@types/node": "^20.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
import execa from 'execa'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import { isNextJsProject, getAppName, getRuntimeBinary } from '../utils/nextjs'
|
|
7
|
+
|
|
8
|
+
interface BuildOptions {
|
|
9
|
+
target: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function build(options: BuildOptions) {
|
|
13
|
+
const cwd = process.cwd()
|
|
14
|
+
|
|
15
|
+
if (!isNextJsProject(cwd)) {
|
|
16
|
+
console.error(chalk.red('✗ Not a Next.js project. Make sure next.config.js exists.'))
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const appName = getAppName(cwd)
|
|
21
|
+
console.log(chalk.bold(`\n📦 Building ${appName} for desktop...\n`))
|
|
22
|
+
|
|
23
|
+
const nextSpinner = ora('Building Next.js app...').start()
|
|
24
|
+
try {
|
|
25
|
+
await execa('npm', ['run', 'build'], { cwd, stdio: 'pipe' })
|
|
26
|
+
nextSpinner.succeed('Next.js app built')
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
nextSpinner.fail(`Next.js build failed: ${err.message}`)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const bundleSpinner = ora('Bundling desktop runtime...').start()
|
|
33
|
+
try {
|
|
34
|
+
const outputDir = path.join(cwd, 'dist-desktop')
|
|
35
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
36
|
+
|
|
37
|
+
const runtime = getRuntimeBinary()
|
|
38
|
+
const outputBinary = path.join(outputDir, appName)
|
|
39
|
+
fs.copyFileSync(runtime, outputBinary)
|
|
40
|
+
fs.chmodSync(outputBinary, 0o755)
|
|
41
|
+
|
|
42
|
+
const nextOut = path.join(cwd, '.next')
|
|
43
|
+
fs.cpSync(nextOut, path.join(outputDir, '.next'), { recursive: true })
|
|
44
|
+
|
|
45
|
+
fs.writeFileSync(path.join(outputDir, 'run.sh'), `#!/bin/bash
|
|
46
|
+
PORT=3000
|
|
47
|
+
npm run start &
|
|
48
|
+
sleep 2
|
|
49
|
+
./${appName} http://localhost:$PORT "${appName}"
|
|
50
|
+
`)
|
|
51
|
+
fs.chmodSync(path.join(outputDir, 'run.sh'), 0o755)
|
|
52
|
+
|
|
53
|
+
bundleSpinner.succeed('Runtime bundled')
|
|
54
|
+
|
|
55
|
+
console.log(chalk.bold('\n✨ Build complete!\n'))
|
|
56
|
+
console.log(chalk.gray(` Output: ${chalk.cyan(outputDir)}`))
|
|
57
|
+
console.log(chalk.gray(` Binary: ${chalk.cyan(outputBinary)}`))
|
|
58
|
+
console.log(chalk.gray(` Run: ${chalk.cyan('cd ' + outputDir + ' && ./run.sh')}\n`))
|
|
59
|
+
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
bundleSpinner.fail(`Bundling failed: ${err.message}`)
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
import execa from 'execa'
|
|
4
|
+
import { isNextJsProject, waitForServer, getRuntimeBinary, getAppName } from '../utils/nextjs'
|
|
5
|
+
|
|
6
|
+
interface DevOptions {
|
|
7
|
+
port: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function dev(options: DevOptions) {
|
|
11
|
+
const port = options.port
|
|
12
|
+
const url = `http://localhost:${port}`
|
|
13
|
+
const cwd = process.cwd()
|
|
14
|
+
|
|
15
|
+
// Check if Next.js project
|
|
16
|
+
if (!isNextJsProject(cwd)) {
|
|
17
|
+
console.error(chalk.red('✗ Not a Next.js project. Make sure next.config.js exists.'))
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const appName = getAppName(cwd)
|
|
22
|
+
console.log(chalk.bold(`\n🚀 MyDesk — ${appName}\n`))
|
|
23
|
+
|
|
24
|
+
// Start Next.js dev server
|
|
25
|
+
const nextSpinner = ora('Starting Next.js dev server...').start()
|
|
26
|
+
|
|
27
|
+
const nextProcess = execa('npm', ['run', 'dev'], {
|
|
28
|
+
cwd,
|
|
29
|
+
env: { ...process.env, PORT: port },
|
|
30
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
nextProcess.stdout?.on('data', (data: Buffer) => {
|
|
34
|
+
const line = data.toString()
|
|
35
|
+
if (line.includes('Ready') || line.includes('ready')) {
|
|
36
|
+
nextSpinner.succeed(`Next.js ready on ${chalk.cyan(url)}`)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
nextProcess.stderr?.on('data', (data: Buffer) => {
|
|
41
|
+
const line = data.toString()
|
|
42
|
+
if (line.toLowerCase().includes('error')) {
|
|
43
|
+
console.error(chalk.red(line.trim()))
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Wait for server to be ready
|
|
48
|
+
try {
|
|
49
|
+
await waitForServer(url)
|
|
50
|
+
} catch (err) {
|
|
51
|
+
nextSpinner.fail('Next.js dev server failed to start')
|
|
52
|
+
nextProcess.kill()
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Launch desktop window
|
|
57
|
+
const windowSpinner = ora('Opening desktop window...').start()
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const runtime = getRuntimeBinary()
|
|
61
|
+
|
|
62
|
+
const runtimeProcess = execa(runtime, [], {
|
|
63
|
+
env: {
|
|
64
|
+
...process.env,
|
|
65
|
+
MYDESK_URL: url,
|
|
66
|
+
MYDESK_TITLE: appName,
|
|
67
|
+
GDK_BACKEND: 'x11',
|
|
68
|
+
WAYLAND_DISPLAY: '',
|
|
69
|
+
},
|
|
70
|
+
stdio: 'inherit',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
windowSpinner.succeed(`${chalk.green('✨ Ready!')} Desktop window opened`)
|
|
74
|
+
console.log(chalk.gray(`\n App: ${chalk.cyan(url)}`))
|
|
75
|
+
console.log(chalk.gray(` Runtime: ${runtime}\n`))
|
|
76
|
+
|
|
77
|
+
// Clean shutdown
|
|
78
|
+
const cleanup = () => {
|
|
79
|
+
nextProcess.kill()
|
|
80
|
+
runtimeProcess.kill()
|
|
81
|
+
process.exit(0)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.on('SIGINT', cleanup)
|
|
85
|
+
process.on('SIGTERM', cleanup)
|
|
86
|
+
|
|
87
|
+
await runtimeProcess
|
|
88
|
+
|
|
89
|
+
} catch (err: any) {
|
|
90
|
+
windowSpinner.fail(`Failed to open desktop window: ${err.message}`)
|
|
91
|
+
nextProcess.kill()
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
import execa from 'execa'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
|
|
7
|
+
interface InitOptions {
|
|
8
|
+
name?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function init(options: InitOptions) {
|
|
12
|
+
const cwd = process.cwd()
|
|
13
|
+
const appName = options.name || path.basename(cwd)
|
|
14
|
+
|
|
15
|
+
console.log(chalk.bold(`\n🚀 Initializing MyDesk project: ${appName}\n`))
|
|
16
|
+
|
|
17
|
+
const spinner = ora('Setting up project...').start()
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Check if Next.js project exists
|
|
21
|
+
const hasNextConfig = fs.existsSync(path.join(cwd, 'next.config.js')) ||
|
|
22
|
+
fs.existsSync(path.join(cwd, 'next.config.mjs')) ||
|
|
23
|
+
fs.existsSync(path.join(cwd, 'next.config.ts'))
|
|
24
|
+
|
|
25
|
+
if (!hasNextConfig) {
|
|
26
|
+
spinner.info('No Next.js project found. Creating one...')
|
|
27
|
+
|
|
28
|
+
// Create a basic Next.js app
|
|
29
|
+
await execa('npx', ['create-next-app@latest', '.', '--typescript', '--tailwind', '--eslint', '--app', '--src-dir', '--no-import-alias'], {
|
|
30
|
+
cwd,
|
|
31
|
+
stdio: 'inherit'
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Create .mydesk directory
|
|
36
|
+
const mydeskDir = path.join(cwd, '.mydesk')
|
|
37
|
+
if (!fs.existsSync(mydeskDir)) {
|
|
38
|
+
fs.mkdirSync(mydeskDir, { recursive: true })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Copy runtime binary
|
|
42
|
+
const runtimeSrc = path.join(__dirname, '../../target/release/mydesk-poc')
|
|
43
|
+
const runtimeDst = path.join(mydeskDir, 'mydesk-poc')
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(runtimeSrc)) {
|
|
46
|
+
fs.copyFileSync(runtimeSrc, runtimeDst)
|
|
47
|
+
fs.chmodSync(runtimeDst, 0o755)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create mydesk.config.js
|
|
51
|
+
const configContent = `export default {
|
|
52
|
+
name: "${appName}",
|
|
53
|
+
version: "1.0.0",
|
|
54
|
+
window: {
|
|
55
|
+
width: 900,
|
|
56
|
+
height: 640,
|
|
57
|
+
title: "${appName}",
|
|
58
|
+
},
|
|
59
|
+
permissions: [
|
|
60
|
+
"fs:read",
|
|
61
|
+
"fs:write",
|
|
62
|
+
"shell:open",
|
|
63
|
+
"clipboard:read",
|
|
64
|
+
"clipboard:write",
|
|
65
|
+
"notification:show",
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
`
|
|
69
|
+
fs.writeFileSync(path.join(cwd, 'mydesk.config.js'), configContent)
|
|
70
|
+
|
|
71
|
+
// Update package.json scripts
|
|
72
|
+
const packageJsonPath = path.join(cwd, 'package.json')
|
|
73
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
74
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
|
75
|
+
|
|
76
|
+
if (!packageJson.scripts) {
|
|
77
|
+
packageJson.scripts = {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!packageJson.scripts['mydesk:dev']) {
|
|
81
|
+
packageJson.scripts['mydesk:dev'] = 'mydesk dev'
|
|
82
|
+
}
|
|
83
|
+
if (!packageJson.scripts['mydesk:build']) {
|
|
84
|
+
packageJson.scripts['mydesk:build'] = 'mydesk build'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
spinner.succeed('MyDesk initialized!')
|
|
91
|
+
|
|
92
|
+
console.log(chalk.bold('\n✨ All set!\n'))
|
|
93
|
+
console.log(chalk.gray(' Run these commands:'))
|
|
94
|
+
console.log(chalk.cyan(' npm run mydesk:dev # Start desktop app in dev mode'))
|
|
95
|
+
console.log(chalk.cyan(' npm run mydesk:build # Build desktop app for production\n'))
|
|
96
|
+
|
|
97
|
+
} catch (err: any) {
|
|
98
|
+
spinner.fail(`Initialization failed: ${err.message}`)
|
|
99
|
+
process.exit(1)
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander'
|
|
3
|
+
import { dev } from './commands/dev'
|
|
4
|
+
import { build } from './commands/build'
|
|
5
|
+
import { init } from './commands/init'
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name('mydesk')
|
|
9
|
+
.description('Turn your Next.js app into a desktop app in 30 seconds')
|
|
10
|
+
.version('0.1.0')
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.command('init')
|
|
14
|
+
.description('Initialize MyDesk in your Next.js project')
|
|
15
|
+
.option('-n, --name <name>', 'App name')
|
|
16
|
+
.action(init)
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.command('dev')
|
|
20
|
+
.description('Start your Next.js app as a desktop app')
|
|
21
|
+
.option('-p, --port <port>', 'Next.js dev server port', '3000')
|
|
22
|
+
.action(dev)
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('build')
|
|
26
|
+
.description('Build your app for production')
|
|
27
|
+
.option('-t, --target <target>', 'Target platform (windows, macos, linux)', process.platform)
|
|
28
|
+
.action(build)
|
|
29
|
+
|
|
30
|
+
program.parse()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import http from 'http'
|
|
4
|
+
|
|
5
|
+
export function isNextJsProject(cwd: string = process.cwd()): boolean {
|
|
6
|
+
return (
|
|
7
|
+
fs.existsSync(path.join(cwd, 'next.config.js')) ||
|
|
8
|
+
fs.existsSync(path.join(cwd, 'next.config.ts')) ||
|
|
9
|
+
fs.existsSync(path.join(cwd, 'next.config.mjs'))
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getPackageJson(cwd: string = process.cwd()): Record<string, any> | null {
|
|
14
|
+
const pkgPath = path.join(cwd, 'package.json')
|
|
15
|
+
if (!fs.existsSync(pkgPath)) return null
|
|
16
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getAppName(cwd: string = process.cwd()): string {
|
|
20
|
+
const pkg = getPackageJson(cwd)
|
|
21
|
+
return pkg?.name ?? path.basename(cwd)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function waitForServer(url: string, timeout = 60000): Promise<void> {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const start = Date.now()
|
|
27
|
+
|
|
28
|
+
const check = () => {
|
|
29
|
+
http.get(url, (res) => {
|
|
30
|
+
if (res.statusCode && res.statusCode < 500) {
|
|
31
|
+
resolve()
|
|
32
|
+
} else {
|
|
33
|
+
retry()
|
|
34
|
+
}
|
|
35
|
+
}).on('error', retry)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const retry = () => {
|
|
39
|
+
if (Date.now() - start > timeout) {
|
|
40
|
+
reject(new Error(`Server at ${url} did not start within ${timeout}ms`))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
setTimeout(check, 500)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
check()
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getRuntimeBinary(): string {
|
|
51
|
+
// In dev: use the compiled binary from the rust project
|
|
52
|
+
const devBinary = path.join(__dirname, '../../../target/debug/mydesk-poc')
|
|
53
|
+
if (fs.existsSync(devBinary)) return devBinary
|
|
54
|
+
|
|
55
|
+
// In production: binary is bundled next to the CLI
|
|
56
|
+
const prodBinary = path.join(__dirname, '../bin/mydesk-runtime')
|
|
57
|
+
if (fs.existsSync(prodBinary)) return prodBinary
|
|
58
|
+
|
|
59
|
+
throw new Error('MyDesk runtime binary not found. Run `cargo build` first.')
|
|
60
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|