@atom-js-org/cli 0.2.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ AtomJS Attribution License 1.0
2
+
3
+ Copyright (c) 2026 AtomJS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8
+ Software, and to permit persons to whom the Software is furnished to do so,
9
+ subject to the following conditions:
10
+
11
+ 1. Framework redistributions, forks, and modified framework distributions must
12
+ retain this license, the NOTICE file, and a reasonably visible attribution
13
+ that includes both the name "AtomJS" and the project link:
14
+ https://github.com/Atom-js-org/atom
15
+
16
+ 2. The attribution requirement applies to distributions of the AtomJS
17
+ framework itself or a derivative framework. It does not require applications
18
+ created with AtomJS to display AtomJS branding, notices, splash screens, or
19
+ user-interface credit.
20
+
21
+ 3. The names and logos of AtomJS may not be used to imply endorsement of a
22
+ modified distribution without prior permission. Forks may accurately state
23
+ that they are based on AtomJS.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # AtomJS CLI
2
+
3
+ Run and build AtomJS applications.
4
+
5
+ ```bash
6
+ atom run dev
7
+ atom run build
8
+ atom build windows
9
+ atom build macos
10
+ atom build linux
11
+ atom build all
12
+ ```
13
+
14
+ Project: https://github.com/Atom-js-org/atom
package/bin/atom.cjs ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { Command } = require('commander');
5
+ const pkg = require('../package.json');
6
+ const { runCommand } = require('../src/run.cjs');
7
+ const { buildCommand } = require('../src/build.cjs');
8
+ const { doctorCommand } = require('../src/doctor.cjs');
9
+ const { initCommand } = require('../src/init.cjs');
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name('atom')
15
+ .description('Build fast, lightweight desktop apps with the system WebView')
16
+ .version(pkg.version);
17
+
18
+ program
19
+ .command('run')
20
+ .description('Run an AtomJS project')
21
+ .argument('<mode>', 'dev or build')
22
+ .option('-p, --project <path>', 'project directory', process.cwd())
23
+ .action(async (mode, options) => runCommand(mode, options));
24
+
25
+ program
26
+ .command('build')
27
+ .description('Build an AtomJS project')
28
+ .argument('<target>', 'windows, macos, linux, or all')
29
+ .option('-p, --project <path>', 'project directory', process.cwd())
30
+ .option('--local', 'never use the remote build workflow')
31
+ .option('--remote', 'always use the remote build workflow')
32
+ .option('--skip-install', 'reuse staged node_modules when possible')
33
+ .action(async (target, options) => buildCommand(target, options));
34
+
35
+ program
36
+ .command('doctor')
37
+ .description('Check Node.js and native WebView prerequisites')
38
+ .option('-p, --project <path>', 'project directory', process.cwd())
39
+ .action(async (options) => doctorCommand(options));
40
+
41
+ program
42
+ .command('init')
43
+ .description('Create a new AtomJS project')
44
+ .argument('[directory]', 'new project directory', '.')
45
+ .option('--name <name>', 'package name')
46
+ .action(async (directory, options) => initCommand(directory, options));
47
+
48
+ program.parseAsync(process.argv).catch((error) => {
49
+ console.error(`\nAtomJS error: ${error.message}`);
50
+ if (process.env.ATOM_DEBUG === '1' && error.stack) console.error(error.stack);
51
+ process.exitCode = 1;
52
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@atom-js-org/cli",
3
+ "version": "0.2.0-alpha.0",
4
+ "description": "CLI for running and building AtomJS desktop applications.",
5
+ "bin": {
6
+ "atom": "bin/atom.cjs"
7
+ },
8
+ "main": "src/index.cjs",
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20.12"
17
+ },
18
+ "dependencies": {
19
+ "archiver": "7.0.1",
20
+ "commander": "^15.0.0",
21
+ "fs-extra": "^11.3.6",
22
+ "postject": "1.0.0-alpha.6"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/Atom-js-org/atom.git",
27
+ "directory": "packages/cli"
28
+ },
29
+ "homepage": "https://github.com/Atom-js-org/atom#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/Atom-js-org/atom/issues"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "tag": "alpha"
36
+ },
37
+ "license": "LicenseRef-AtomJS-1.0"
38
+ }
package/src/build.cjs ADDED
@@ -0,0 +1,505 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+ const crypto = require('node:crypto');
7
+ const { spawnSync } = require('node:child_process');
8
+ const fse = require('fs-extra');
9
+ const archiver = require('archiver');
10
+ const { inject } = require('postject');
11
+ const {
12
+ loadProject,
13
+ hostTarget,
14
+ run,
15
+ capture,
16
+ commandExists,
17
+ validateTarget,
18
+ sanitizeFilename
19
+ } = require('./utils.cjs');
20
+ const { resolveElectronCompatibilityRoot } = require('./electron-compat.cjs');
21
+
22
+ async function buildCommand(targetInput, options = {}) {
23
+ const target = validateTarget(targetInput);
24
+ const project = loadProject(options.project);
25
+ const host = hostTarget();
26
+
27
+ if (options.local && (target === 'all' || target !== host)) {
28
+ throw new Error(`A local ${host} machine cannot produce a complete '${target}' release. Remove --local to use GitHub Actions.`);
29
+ }
30
+
31
+ const needsRemote = options.remote || target === 'all' || target !== host;
32
+ if (needsRemote) return remoteBuild(project, target);
33
+ return localBuild(project, host, options);
34
+ }
35
+
36
+ async function localBuild(project, target, options = {}) {
37
+ if (target !== hostTarget()) {
38
+ throw new Error(`Local target mismatch: host is ${hostTarget()}, requested ${target}`);
39
+ }
40
+
41
+ const buildRoot = path.join(project.root, 'build', target);
42
+ const unpacked = path.join(buildRoot, 'unpacked');
43
+ const appDir = path.join(unpacked, 'app');
44
+ const productName = sanitizeFilename(project.config.productName);
45
+
46
+ console.log(`\nAtomJS build (${target})`);
47
+ console.log(`Project: ${project.root}`);
48
+ console.log(`Output: ${buildRoot}`);
49
+
50
+ await fse.remove(buildRoot);
51
+ await fse.ensureDir(unpacked);
52
+
53
+ const stageRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'atomjs-stage-'));
54
+ const stagedApp = path.join(stageRoot, 'app');
55
+ try {
56
+ await copyApplication(project.root, stagedApp);
57
+ await vendorFramework(stagedApp, project, target);
58
+ await installProductionDependencies(stagedApp, options.skipInstall, target);
59
+ await fse.move(stagedApp, appDir, { overwrite: true });
60
+ } finally {
61
+ await fse.remove(stageRoot);
62
+ }
63
+
64
+ const runtimeDir = path.join(unpacked, 'runtime');
65
+ await fse.ensureDir(runtimeDir);
66
+ const runtimeNodeName = target === 'windows' ? 'node.exe' : 'node';
67
+ const runtimeNodePath = path.join(runtimeDir, runtimeNodeName);
68
+ await fs.promises.copyFile(process.execPath, runtimeNodePath);
69
+ if (target !== 'windows') await fs.promises.chmod(runtimeNodePath, 0o755);
70
+
71
+ const executableName = target === 'windows' ? `${productName}.exe` : productName;
72
+ const executablePath = path.join(unpacked, executableName);
73
+ await createSeaLauncher({ executablePath, appDir, target });
74
+
75
+ const creditPath = path.join(unpacked, 'ATOMJS-CREDIT.txt');
76
+ await fs.promises.writeFile(
77
+ creditPath,
78
+ 'Built with AtomJS\nhttps://github.com/Atom-js-org/atom\nCredit is optional inside applications.\n',
79
+ 'utf8'
80
+ );
81
+
82
+ const outputs = [];
83
+ if (target === 'windows') outputs.push(...await packageWindows({ project, buildRoot, unpacked, executableName, productName }));
84
+ if (target === 'macos') outputs.push(...await packageMacOS({ project, buildRoot, unpacked, executableName, productName }));
85
+ if (target === 'linux') outputs.push(...await packageLinux({ project, buildRoot, unpacked, executableName, productName }));
86
+
87
+ const manifest = {
88
+ atomjsVersion: '0.2.0-alpha.0',
89
+ target,
90
+ productName,
91
+ appId: project.config.appId,
92
+ createdAt: new Date().toISOString(),
93
+ run: path.relative(buildRoot, executablePath),
94
+ unpacked: path.relative(buildRoot, unpacked),
95
+ outputs: outputs.map((file) => path.relative(buildRoot, file))
96
+ };
97
+ await fs.promises.writeFile(path.join(buildRoot, 'manifest.json'), JSON.stringify(manifest, null, 2));
98
+
99
+ console.log('\nBuild complete:');
100
+ console.log(` ${path.relative(project.root, unpacked)}`);
101
+ for (const output of outputs) console.log(` ${path.relative(project.root, output)}`);
102
+ return manifest;
103
+ }
104
+
105
+ async function copyApplication(projectRoot, appDir) {
106
+ await fse.copy(projectRoot, appDir, {
107
+ dereference: false,
108
+ filter(source) {
109
+ const relative = path.relative(projectRoot, source);
110
+ if (!relative) return true;
111
+ const first = relative.split(path.sep)[0];
112
+ return !['node_modules', 'build', '.git', '.atom'].includes(first);
113
+ }
114
+ });
115
+ }
116
+
117
+ async function vendorFramework(appDir, project, target) {
118
+ const frameworkRoot = resolveFrameworkRoot(project.root);
119
+ const electronCompatRoot = resolveElectronCompatibilityRoot(project.root);
120
+ const vendorRoot = path.join(appDir, 'vendor', 'atomjs');
121
+ const electronVendorRoot = path.join(appDir, 'vendor', 'electron-compat');
122
+ await fse.ensureDir(path.dirname(vendorRoot));
123
+ await fse.copy(frameworkRoot, vendorRoot, {
124
+ filter(source) {
125
+ const relative = path.relative(frameworkRoot, source);
126
+ return !relative.startsWith('node_modules') && !relative.startsWith('.git');
127
+ }
128
+ });
129
+ await fse.copy(electronCompatRoot, electronVendorRoot, {
130
+ filter(source) {
131
+ const relative = path.relative(electronCompatRoot, source);
132
+ return !relative.startsWith('node_modules') && !relative.startsWith('.git');
133
+ }
134
+ });
135
+
136
+ const packagePath = path.join(appDir, 'package.json');
137
+ const pkg = JSON.parse(await fs.promises.readFile(packagePath, 'utf8'));
138
+ pkg.dependencies = {
139
+ ...(pkg.dependencies || {}),
140
+ '@atom-js-org/runtime': 'file:vendor/atomjs',
141
+ electron: 'file:vendor/electron-compat'
142
+ };
143
+ if (target !== 'macos' && process.env.ATOM_SKIP_WEBVIEW_CHECK !== '1') {
144
+ pkg.dependencies['webview-nodejs'] = '0.5.0';
145
+ if (pkg.optionalDependencies) delete pkg.optionalDependencies['webview-nodejs'];
146
+ } else if (pkg.optionalDependencies) {
147
+ delete pkg.optionalDependencies['webview-nodejs'];
148
+ }
149
+ pkg.overrides = { ...(pkg.overrides || {}), tar: '7.5.20' };
150
+ delete pkg.devDependencies;
151
+ delete pkg.workspaces;
152
+ delete pkg.scripts?.prepare;
153
+ await fs.promises.writeFile(packagePath, JSON.stringify(pkg, null, 2));
154
+ await fse.remove(path.join(appDir, 'package-lock.json'));
155
+ await fse.remove(path.join(appDir, 'npm-shrinkwrap.json'));
156
+ }
157
+
158
+ function resolveFrameworkRoot(projectRoot) {
159
+ try {
160
+ return path.dirname(require.resolve('@atom-js-org/runtime/package.json', { paths: [projectRoot, process.cwd()] }));
161
+ } catch {
162
+ const sibling = path.resolve(__dirname, '..', '..', 'atomjs');
163
+ if (fs.existsSync(path.join(sibling, 'package.json'))) return sibling;
164
+ throw new Error('Could not locate @atom-js-org/runtime. Install the AtomJS runtime in the project first.');
165
+ }
166
+ }
167
+
168
+ async function installProductionDependencies(appDir, skipInstall, target) {
169
+ if (skipInstall && fs.existsSync(path.join(appDir, 'node_modules', '@atom-js-org', 'runtime'))) return;
170
+ console.log('Installing production dependencies for the target OS...');
171
+ await run(process.platform === 'win32' ? 'npm.cmd' : 'npm', [
172
+ 'install',
173
+ '--omit=dev',
174
+ '--no-audit',
175
+ '--no-fund'
176
+ ], { cwd: appDir });
177
+
178
+ const electronPackagePath = path.join(appDir, 'node_modules', 'electron', 'package.json');
179
+ if (!fs.existsSync(electronPackagePath)) {
180
+ throw new Error('The AtomJS Electron compatibility facade was not installed in the staged application.');
181
+ }
182
+
183
+ if (target !== 'macos') {
184
+ const bindingPath = path.join(appDir, 'node_modules', 'webview-nodejs');
185
+ if (!fs.existsSync(bindingPath) && process.env.ATOM_SKIP_WEBVIEW_CHECK !== '1') {
186
+ throw new Error('webview-nodejs was not installed. Run `atom doctor`, install the platform prerequisites, and retry the build.');
187
+ }
188
+ }
189
+ }
190
+
191
+ async function createSeaLauncher({ executablePath, appDir, target }) {
192
+ const work = path.join(path.dirname(executablePath), '.sea-' + crypto.randomBytes(5).toString('hex'));
193
+ await fse.ensureDir(work);
194
+ const launcherPath = path.join(work, 'launcher.cjs');
195
+ const blobPath = path.join(work, target === 'windows' ? 'sea-prep.blob.exe' : 'sea-prep.blob');
196
+ const configPath = path.join(work, 'sea-config.json');
197
+
198
+ const launcher = `
199
+ 'use strict';
200
+ const path = require('node:path');
201
+ const fs = require('node:fs');
202
+ const { createRequire } = require('node:module');
203
+
204
+ const executableDir = path.dirname(process.execPath);
205
+ const appDir = process.platform === 'darwin' && executableDir.includes('.app' + path.sep + 'Contents' + path.sep + 'MacOS')
206
+ ? path.resolve(executableDir, '..', 'Resources', 'app')
207
+ : path.join(executableDir, 'app');
208
+
209
+ const packagePath = path.join(appDir, 'package.json');
210
+ if (!fs.existsSync(packagePath)) {
211
+ console.error('AtomJS application files are missing:', appDir);
212
+ process.exit(1);
213
+ }
214
+
215
+ const runtimeNode = process.platform === 'darwin' && executableDir.includes('.app' + path.sep + 'Contents' + path.sep + 'MacOS')
216
+ ? path.resolve(executableDir, '..', 'Resources', 'runtime', 'node')
217
+ : path.join(executableDir, 'runtime', process.platform === 'win32' ? 'node.exe' : 'node');
218
+
219
+ process.chdir(appDir);
220
+ process.env.ATOM_PROJECT_ROOT = appDir;
221
+ process.env.ATOM_BUILD = '1';
222
+ process.env.ATOM_NODE_EXECUTABLE = runtimeNode;
223
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
224
+ const mainPath = path.resolve(appDir, pkg.main || 'main.js');
225
+ const load = createRequire(packagePath);
226
+ load(mainPath);
227
+ `;
228
+ await fs.promises.writeFile(launcherPath, launcher, 'utf8');
229
+
230
+ const seaConfig = {
231
+ main: launcherPath,
232
+ output: blobPath,
233
+ disableExperimentalSEAWarning: true,
234
+ useSnapshot: false,
235
+ useCodeCache: false
236
+ };
237
+ await fs.promises.writeFile(configPath, JSON.stringify(seaConfig, null, 2));
238
+
239
+ await run(process.execPath, ['--experimental-sea-config', configPath], { cwd: work });
240
+ await fs.promises.copyFile(process.execPath, executablePath);
241
+
242
+ if (target === 'macos' && commandExists('codesign', ['--version'])) {
243
+ spawnSync('codesign', ['--remove-signature', executablePath], { stdio: 'ignore' });
244
+ }
245
+
246
+ await inject(executablePath, 'NODE_SEA_BLOB', await fs.promises.readFile(blobPath), {
247
+ sentinelFuse: 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
248
+ machoSegmentName: 'NODE_SEA'
249
+ });
250
+
251
+ if (target !== 'windows') await fs.promises.chmod(executablePath, 0o755);
252
+ if (target === 'macos' && commandExists('codesign', ['--version'])) {
253
+ await run('codesign', ['--sign', '-', executablePath]);
254
+ }
255
+ await fse.remove(work);
256
+ }
257
+
258
+ async function packageWindows({ project, buildRoot, unpacked, executableName, productName }) {
259
+ const outputs = [];
260
+ const zipPath = path.join(buildRoot, `${productName}-windows.zip`);
261
+ await archiveDirectory(unpacked, zipPath, 'zip');
262
+ outputs.push(zipPath);
263
+
264
+ const nsisPath = path.join(buildRoot, 'installer.nsi');
265
+ const installerPath = path.join(buildRoot, `${productName} Installer.exe`);
266
+ const escapedSource = unpacked.replace(/\\/g, '\\\\');
267
+ const credit = project.config.installerCredit
268
+ ? `!define MUI_WELCOMEPAGE_TEXT "This installer will install ${escapeNsis(productName)}.$\\r$\\n$\\r$\\nPowered by AtomJS — https://github.com/Atom-js-org/atom"`
269
+ : '';
270
+ const script = `
271
+ Unicode true
272
+ !include "MUI2.nsh"
273
+ Name "${escapeNsis(productName)}"
274
+ OutFile "${installerPath.replace(/\\/g, '\\\\')}"
275
+ InstallDir "$LOCALAPPDATA\\Programs\\${escapeNsis(productName)}"
276
+ RequestExecutionLevel user
277
+ ${credit}
278
+ !insertmacro MUI_PAGE_WELCOME
279
+ !insertmacro MUI_PAGE_DIRECTORY
280
+ !insertmacro MUI_PAGE_INSTFILES
281
+ !insertmacro MUI_PAGE_FINISH
282
+ !insertmacro MUI_LANGUAGE "English"
283
+ Section "Install"
284
+ SetOutPath "$INSTDIR"
285
+ File /r "${escapedSource}\\*"
286
+ CreateDirectory "$SMPROGRAMS\\${escapeNsis(productName)}"
287
+ CreateShortcut "$SMPROGRAMS\\${escapeNsis(productName)}\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
288
+ CreateShortcut "$DESKTOP\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
289
+ WriteUninstaller "$INSTDIR\\Uninstall.exe"
290
+ SectionEnd
291
+ Section "Uninstall"
292
+ Delete "$DESKTOP\\${escapeNsis(productName)}.lnk"
293
+ RMDir /r "$SMPROGRAMS\\${escapeNsis(productName)}"
294
+ RMDir /r "$INSTDIR"
295
+ SectionEnd
296
+ `;
297
+ await fs.promises.writeFile(nsisPath, script.trimStart(), 'utf8');
298
+
299
+ if (commandExists('makensis', ['/VERSION'])) {
300
+ await run('makensis', [nsisPath]);
301
+ if (fs.existsSync(installerPath)) outputs.push(installerPath);
302
+ } else {
303
+ console.warn('NSIS was not found; installer.nsi was generated but the .exe installer was skipped.');
304
+ }
305
+ return outputs;
306
+ }
307
+
308
+ async function packageMacOS({ project, buildRoot, unpacked, executableName, productName }) {
309
+ const outputs = [];
310
+ const appBundle = path.join(buildRoot, `${productName}.app`);
311
+ const contents = path.join(appBundle, 'Contents');
312
+ const macosDir = path.join(contents, 'MacOS');
313
+ const resources = path.join(contents, 'Resources');
314
+ await fse.ensureDir(macosDir);
315
+ await fse.ensureDir(resources);
316
+ await fse.copy(path.join(unpacked, executableName), path.join(macosDir, productName));
317
+ await fse.copy(path.join(unpacked, 'app'), path.join(resources, 'app'));
318
+ await fse.copy(path.join(unpacked, 'runtime'), path.join(resources, 'runtime'));
319
+ await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(resources, 'ATOMJS-CREDIT.txt'));
320
+
321
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
322
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
323
+ <plist version="1.0"><dict>
324
+ <key>CFBundleExecutable</key><string>${xml(productName)}</string>
325
+ <key>CFBundleIdentifier</key><string>${xml(project.config.appId)}</string>
326
+ <key>CFBundleName</key><string>${xml(productName)}</string>
327
+ <key>CFBundleDisplayName</key><string>${xml(productName)}</string>
328
+ <key>CFBundlePackageType</key><string>APPL</string>
329
+ <key>CFBundleShortVersionString</key><string>${xml(project.packageJson.version || '0.0.0')}</string>
330
+ <key>NSHighResolutionCapable</key><true/>
331
+ </dict></plist>`;
332
+ await fs.promises.writeFile(path.join(contents, 'Info.plist'), plist, 'utf8');
333
+ await fs.promises.chmod(path.join(macosDir, productName), 0o755);
334
+
335
+ if (commandExists('codesign', ['--version'])) await run('codesign', ['--force', '--deep', '--sign', '-', appBundle]);
336
+
337
+ const zipPath = path.join(buildRoot, `${productName}-macos.zip`);
338
+ if (commandExists('ditto', ['-h'])) {
339
+ await run('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', appBundle, zipPath]);
340
+ } else {
341
+ await archiveDirectory(appBundle, zipPath, 'zip', `${productName}.app`);
342
+ }
343
+ outputs.push(zipPath);
344
+
345
+ if (commandExists('hdiutil', ['help'])) {
346
+ const dmgPath = path.join(buildRoot, `${productName}.dmg`);
347
+ await run('hdiutil', ['create', '-volname', productName, '-srcfolder', appBundle, '-ov', '-format', 'UDZO', dmgPath]);
348
+ outputs.push(dmgPath);
349
+ }
350
+ return outputs;
351
+ }
352
+
353
+ async function packageLinux({ project, buildRoot, unpacked, executableName, productName }) {
354
+ const outputs = [];
355
+ const tarPath = path.join(buildRoot, `${productName}-linux.tar.gz`);
356
+ await archiveDirectory(unpacked, tarPath, 'tar');
357
+ outputs.push(tarPath);
358
+
359
+ const appDir = path.join(buildRoot, `${productName}.AppDir`);
360
+ const usrBin = path.join(appDir, 'usr', 'bin');
361
+ const usrLibApp = path.join(appDir, 'usr', 'lib', sanitizeFilename(project.packageJson.name || productName));
362
+ await fse.ensureDir(usrBin);
363
+ await fse.ensureDir(usrLibApp);
364
+ await fse.copy(path.join(unpacked, executableName), path.join(usrBin, productName));
365
+ await fse.copy(path.join(unpacked, 'app'), path.join(usrBin, 'app'));
366
+ await fse.copy(path.join(unpacked, 'runtime'), path.join(usrBin, 'runtime'));
367
+ await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(usrLibApp, 'ATOMJS-CREDIT.txt'));
368
+
369
+ const appRun = `#!/bin/sh\nHERE="$(dirname "$(readlink -f "$0")")"\nexec "$HERE/usr/bin/${productName}" "$@"\n`;
370
+ await fs.promises.writeFile(path.join(appDir, 'AppRun'), appRun, { mode: 0o755 });
371
+ const desktopName = sanitizeFilename(project.packageJson.name || productName).toLowerCase().replace(/\s+/g, '-');
372
+ const desktop = `[Desktop Entry]\nType=Application\nName=${productName}\nExec=${productName}\nIcon=${desktopName}\nCategories=Development;Utility;\nTerminal=false\n`;
373
+ await fs.promises.writeFile(path.join(appDir, `${desktopName}.desktop`), desktop, 'utf8');
374
+
375
+ const iconSource = project.config.icon ? path.resolve(project.root, project.config.icon) : null;
376
+ if (iconSource && fs.existsSync(iconSource)) {
377
+ await fse.copy(iconSource, path.join(appDir, `${desktopName}.png`));
378
+ }
379
+
380
+ const tool = findAppImageTool();
381
+ if (tool) {
382
+ const appImagePath = path.join(buildRoot, `${productName}.AppImage`);
383
+ await run(tool, [appDir, appImagePath], { env: { ...process.env, ARCH: process.arch === 'arm64' ? 'aarch64' : 'x86_64' } });
384
+ outputs.push(appImagePath);
385
+ } else {
386
+ console.warn('appimagetool was not found; AppDir was created but AppImage generation was skipped.');
387
+ }
388
+ return outputs;
389
+ }
390
+
391
+ function findAppImageTool() {
392
+ for (const name of ['appimagetool', 'appimagetool.AppImage']) {
393
+ if (commandExists(name, ['--version'])) return name;
394
+ }
395
+ return null;
396
+ }
397
+
398
+ async function archiveDirectory(source, output, format, rootName) {
399
+ await fse.ensureDir(path.dirname(output));
400
+ await new Promise((resolve, reject) => {
401
+ const stream = fs.createWriteStream(output);
402
+ const archive = format === 'zip'
403
+ ? archiver('zip', { zlib: { level: 9 } })
404
+ : archiver('tar', { gzip: true, gzipOptions: { level: 9 } });
405
+ stream.on('close', resolve);
406
+ stream.on('error', reject);
407
+ archive.on('error', reject);
408
+ archive.pipe(stream);
409
+ archive.directory(source, rootName || false);
410
+ archive.finalize();
411
+ });
412
+ }
413
+
414
+ async function remoteBuild(project, target) {
415
+ if (!commandExists('gh', ['--version'])) {
416
+ throw new Error(
417
+ "Cross-platform builds require GitHub CLI. Install it, run 'gh auth login', and try again."
418
+ );
419
+ }
420
+
421
+ const repositoryRoot = resolveGitRepository(project.root);
422
+ const auth = spawnSync('gh', ['auth', 'status'], { cwd: repositoryRoot, stdio: 'ignore' });
423
+ if (auth.status !== 0) {
424
+ throw new Error("GitHub CLI is not authenticated. Run 'gh auth login' and try again.");
425
+ }
426
+
427
+ const workflow = project.config.github.workflow || 'atom-build.yml';
428
+ const branch = capture('git', ['branch', '--show-current'], { cwd: repositoryRoot }).trim() || 'main';
429
+ const relativeProject = path.relative(repositoryRoot, project.root).split(path.sep).join('/') || '.';
430
+ const dispatchTime = Date.now();
431
+
432
+ console.log(`Dispatching remote AtomJS build '${target}' through ${workflow}...`);
433
+ console.log(`Repository: ${repositoryRoot}`);
434
+ console.log(`Project: ${relativeProject}`);
435
+
436
+ await run('gh', [
437
+ 'workflow', 'run', workflow,
438
+ '--ref', branch,
439
+ '-f', `target=${target}`,
440
+ '-f', `project=${relativeProject}`
441
+ ], { cwd: repositoryRoot });
442
+
443
+ const runId = await waitForWorkflowRun(repositoryRoot, workflow, dispatchTime);
444
+ await run('gh', ['run', 'watch', String(runId), '--exit-status'], { cwd: repositoryRoot });
445
+
446
+ const temp = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'atomjs-build-'));
447
+ try {
448
+ await run('gh', ['run', 'download', String(runId), '--dir', temp], { cwd: repositoryRoot });
449
+ const buildRoot = path.join(project.root, 'build');
450
+ await fse.ensureDir(buildRoot);
451
+ for (const entry of await fs.promises.readdir(temp, { withFileTypes: true })) {
452
+ if (!entry.isDirectory()) continue;
453
+ const match = entry.name.match(/^atom-build-(windows|macos|linux)$/);
454
+ if (!match) continue;
455
+ const osTarget = match[1];
456
+ await fse.remove(path.join(buildRoot, osTarget));
457
+ await fse.copy(path.join(temp, entry.name), path.join(buildRoot, osTarget));
458
+ console.log(`Downloaded build/${osTarget}`);
459
+ }
460
+ } finally {
461
+ await fse.remove(temp);
462
+ }
463
+ }
464
+
465
+ function resolveGitRepository(startDirectory) {
466
+ const result = spawnSync('git', ['-C', startDirectory, 'rev-parse', '--show-toplevel'], {
467
+ encoding: 'utf8',
468
+ stdio: ['ignore', 'pipe', 'pipe']
469
+ });
470
+
471
+ if (result.status !== 0) {
472
+ throw new Error([
473
+ 'Cross-platform builds require the project to be inside a GitHub repository.',
474
+ `Not a Git repository: ${startDirectory}`,
475
+ '',
476
+ 'Create one, commit the project and push it to GitHub before running atom build all.'
477
+ ].join('\n'));
478
+ }
479
+
480
+ return result.stdout.trim();
481
+ }
482
+
483
+ async function waitForWorkflowRun(repositoryRoot, workflow, dispatchTime) {
484
+ for (let attempt = 0; attempt < 30; attempt += 1) {
485
+ const output = capture('gh', [
486
+ 'run', 'list', '--workflow', workflow, '--event', 'workflow_dispatch',
487
+ '--limit', '10', '--json', 'databaseId,createdAt'
488
+ ], { cwd: repositoryRoot });
489
+ const runs = JSON.parse(output);
490
+ const match = runs.find((run) => new Date(run.createdAt).getTime() >= dispatchTime - 5000);
491
+ if (match?.databaseId) return match.databaseId;
492
+ await new Promise((resolve) => setTimeout(resolve, 2000));
493
+ }
494
+ throw new Error('The workflow was dispatched, but AtomJS could not find the GitHub Actions run.');
495
+ }
496
+
497
+ function escapeNsis(value) {
498
+ return String(value).replace(/\$/g, '$$').replace(/"/g, '$\\"');
499
+ }
500
+
501
+ function xml(value) {
502
+ return String(value).replace(/[<>&'\"]/g, (char) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' })[char]);
503
+ }
504
+
505
+ module.exports = { buildCommand, localBuild };
package/src/doctor.cjs ADDED
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawnSync } = require('node:child_process');
6
+ const { loadProject, commandExists } = require('./utils.cjs');
7
+
8
+ async function doctorCommand(options = {}) {
9
+ const rows = [];
10
+ rows.push(check('Node.js >= 20.12', isNodeSupported(), process.version));
11
+ rows.push(check('npm', commandExists(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['--version'])));
12
+
13
+ if (process.platform === 'win32') {
14
+ rows.push(check('PowerShell', commandExists('powershell.exe', ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.ToString()'])));
15
+ rows.push(check('WebView2 Runtime', checkWindowsWebView2(), 'required by the native WebView'));
16
+ rows.push(check('NSIS (optional installer)', commandExists('makensis', ['/VERSION'])));
17
+ } else if (process.platform === 'darwin') {
18
+ rows.push(check('osascript JavaScript host', fs.existsSync('/usr/bin/osascript')));
19
+ rows.push(check('WebKit framework', fs.existsSync('/System/Library/Frameworks/WebKit.framework')));
20
+ rows.push(check('codesign', commandExists('codesign', ['--version'])));
21
+ rows.push(check('hdiutil', commandExists('hdiutil', ['help'])));
22
+ } else {
23
+ rows.push(check('pkg-config', commandExists('pkg-config', ['--version'])));
24
+ rows.push(check('GTK 3', pkgConfigExists('gtk+-3.0'), 'install libgtk-3-dev'));
25
+ rows.push(check('WebKitGTK 4.1', pkgConfigExists('webkit2gtk-4.1'), 'install libwebkit2gtk-4.1-dev'));
26
+ rows.push(check('zenity (dialogs)', commandExists('zenity', ['--version'])));
27
+ rows.push(check('appimagetool (optional)', commandExists('appimagetool', ['--version']) || commandExists('appimagetool.AppImage', ['--version'])));
28
+ }
29
+
30
+ if (process.platform === 'darwin') {
31
+ rows.push(check('macOS WKWebView backend', true, 'built in through JavaScript for Automation'));
32
+ } else {
33
+ try {
34
+ const project = loadProject(options.project);
35
+ const binding = require.resolve('webview-nodejs/package.json', { paths: [project.root, process.cwd()] });
36
+ rows.push(check('webview-nodejs package', true, path.dirname(binding)));
37
+ } catch {
38
+ rows.push(check('webview-nodejs package', false, 'install platform prerequisites, then run npm install webview-nodejs'));
39
+ }
40
+ }
41
+
42
+ console.log('\nAtomJS doctor\n');
43
+ let failed = false;
44
+ for (const row of rows) {
45
+ console.log(`${row.ok ? '✓' : '✗'} ${row.name}${row.detail ? ` — ${row.detail}` : ''}`);
46
+ if (!row.ok && !row.name.includes('optional')) failed = true;
47
+ }
48
+ console.log('');
49
+ if (failed) {
50
+ process.exitCode = 1;
51
+ console.log('One or more required checks failed.');
52
+ } else {
53
+ console.log('Required checks passed.');
54
+ }
55
+ }
56
+
57
+ function isNodeSupported() {
58
+ const [major, minor] = process.versions.node.split('.').map(Number);
59
+ return major > 20 || (major === 20 && minor >= 12);
60
+ }
61
+
62
+ function check(name, ok, detail = '') {
63
+ return { name, ok: Boolean(ok), detail };
64
+ }
65
+
66
+ function pkgConfigExists(name) {
67
+ const result = spawnSync('pkg-config', ['--exists', name]);
68
+ return !result.error && result.status === 0;
69
+ }
70
+
71
+ function checkWindowsWebView2() {
72
+ const script = `
73
+ $paths = @(
74
+ 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}',
75
+ 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}',
76
+ 'HKCU:\\Software\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}'
77
+ )
78
+ foreach ($p in $paths) { if (Test-Path $p) { exit 0 } }
79
+ exit 1
80
+ `;
81
+ const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', script], { stdio: 'ignore' });
82
+ return !result.error && result.status === 0;
83
+ }
84
+
85
+ module.exports = { doctorCommand };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const fse = require('fs-extra');
6
+
7
+ async function ensureElectronCompatibility(projectRoot) {
8
+ const target = path.join(projectRoot, 'node_modules', 'electron');
9
+ const targetPackage = path.join(target, 'package.json');
10
+
11
+ if (fs.existsSync(targetPackage)) {
12
+ const existing = readJson(targetPackage);
13
+ if (existing && existing.atomjsElectronCompatibility === true) return target;
14
+
15
+ throw new Error(
16
+ "A real or third-party 'electron' package already exists in node_modules. " +
17
+ "AtomJS cannot safely replace it automatically. Remove that package and run AtomJS again; " +
18
+ "AtomJS will install its lightweight Electron compatibility facade."
19
+ );
20
+ }
21
+
22
+ const source = resolveElectronCompatibilityRoot(projectRoot);
23
+ await fse.ensureDir(path.dirname(target));
24
+
25
+ try {
26
+ const resolvedSource = await fs.promises.realpath(source);
27
+
28
+ await fs.promises.symlink(
29
+ resolvedSource,
30
+ target,
31
+ process.platform === 'win32' ? 'junction' : 'dir'
32
+ );
33
+ } catch {
34
+ await fse.remove(target);
35
+ await fse.copy(source, target);
36
+ }
37
+
38
+ return target;
39
+ }
40
+
41
+ function resolveElectronCompatibilityRoot(projectRoot) {
42
+ const sibling = path.resolve(__dirname, '..', '..', 'electron-compat');
43
+ if (isAtomCompatPackage(sibling)) return sibling;
44
+
45
+ for (const searchRoot of [projectRoot, process.cwd()]) {
46
+ try {
47
+ const packagePath = require.resolve('electron/package.json', { paths: [searchRoot] });
48
+ const root = path.dirname(packagePath);
49
+ if (isAtomCompatPackage(root)) return root;
50
+ } catch {}
51
+ }
52
+
53
+ throw new Error(
54
+ 'AtomJS could not locate its Electron compatibility package. ' +
55
+ 'Reinstall the AtomJS distribution or add the compatibility package to the workspace.'
56
+ );
57
+ }
58
+
59
+ function isAtomCompatPackage(directory) {
60
+ const pkg = readJson(path.join(directory, 'package.json'));
61
+ return Boolean(
62
+ pkg &&
63
+ ['electron', '@atom-js-org/electron'].includes(pkg.name) &&
64
+ pkg.atomjsElectronCompatibility === true
65
+ );
66
+ }
67
+
68
+ function readJson(filePath) {
69
+ try {
70
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ module.exports = {
77
+ ensureElectronCompatibility,
78
+ resolveElectronCompatibilityRoot
79
+ };
package/src/index.cjs ADDED
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ ...require('./run.cjs'),
5
+ ...require('./build.cjs'),
6
+ ...require('./doctor.cjs'),
7
+ ...require('./init.cjs')
8
+ };
package/src/init.cjs ADDED
@@ -0,0 +1,254 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const fse = require('fs-extra');
6
+
7
+ async function initCommand(directory, options = {}) {
8
+ const root = path.resolve(directory);
9
+ await fse.ensureDir(root);
10
+ const existing = await fs.promises.readdir(root);
11
+ if (existing.length > 0 && !existing.every((name) => name === '.git')) {
12
+ throw new Error(`Directory is not empty: ${root}`);
13
+ }
14
+
15
+ const packageName = sanitizePackageName(options.name || path.basename(root));
16
+ const productName = humanize(packageName);
17
+
18
+ await fse.ensureDir(path.join(root, 'src'));
19
+ await fse.ensureDir(path.join(root, '.github', 'workflows'));
20
+
21
+ await fs.promises.writeFile(path.join(root, 'package.json'), JSON.stringify({
22
+ name: packageName,
23
+ version: '0.1.0',
24
+ private: true,
25
+ main: 'src/main.js',
26
+ scripts: {
27
+ dev: 'atom run dev',
28
+ build: 'atom build all',
29
+ 'build:current': `atom build ${hostTarget()}`,
30
+ start: 'atom run build'
31
+ },
32
+ dependencies: {
33
+ '@atom-js-org/runtime': '0.2.0-alpha.0',
34
+ electron: 'npm:@atom-js-org/electron@0.2.0-alpha.0'
35
+ },
36
+ optionalDependencies: {
37
+ 'webview-nodejs': '0.5.0'
38
+ },
39
+ devDependencies: {
40
+ '@atom-js-org/cli': '0.2.0-alpha.0'
41
+ },
42
+ overrides: {
43
+ tar: '7.5.20'
44
+ }
45
+ }, null, 2));
46
+
47
+ await fs.promises.writeFile(path.join(root, 'atom.config.json'), JSON.stringify({
48
+ appId: `com.example.${packageName.replace(/[^a-z0-9]+/g, '')}`,
49
+ productName,
50
+ main: 'src/main.js',
51
+ icon: 'assets/icon.png',
52
+ installerCredit: true,
53
+ github: {
54
+ workflow: 'atom-build.yml'
55
+ }
56
+ }, null, 2));
57
+
58
+ await fse.ensureDir(path.join(root, 'assets'));
59
+ await fs.promises.writeFile(path.join(root, 'src', 'main.js'), mainTemplate());
60
+ await fs.promises.writeFile(path.join(root, 'src', 'preload.js'), preloadTemplate());
61
+ await fs.promises.writeFile(path.join(root, 'src', 'index.html'), htmlTemplate(productName));
62
+ await fs.promises.writeFile(path.join(root, 'src', 'renderer.js'), rendererTemplate());
63
+ await fs.promises.writeFile(path.join(root, '.gitignore'), 'node_modules/\nbuild/\n.atom/\n');
64
+ await fs.promises.writeFile(path.join(root, '.github', 'workflows', 'atom-build.yml'), workflowTemplate());
65
+
66
+ console.log(`Created AtomJS project in ${root}`);
67
+ console.log('Next: npm install && npm run dev');
68
+ }
69
+
70
+ function mainTemplate() {
71
+ return `'use strict';
72
+
73
+ const path = require('node:path');
74
+ const fs = require('node:fs/promises');
75
+ const { app, BrowserWindow, ipcMain, dialog } = require('electron');
76
+
77
+ function createWindow() {
78
+ const win = new BrowserWindow({
79
+ width: 1000,
80
+ height: 700,
81
+ title: app.getName(),
82
+ webPreferences: {
83
+ preload: path.join(__dirname, 'preload.js')
84
+ }
85
+ });
86
+
87
+ win.loadFile(path.join(__dirname, 'index.html'));
88
+ }
89
+
90
+ ipcMain.handle('file:open', async () => {
91
+ const result = await dialog.showOpenDialog({ properties: ['openFile'] });
92
+ if (result.canceled) return null;
93
+ return {
94
+ path: result.filePaths[0],
95
+ content: await fs.readFile(result.filePaths[0], 'utf8')
96
+ };
97
+ });
98
+
99
+ app.whenReady().then(createWindow);
100
+ app.on('window-all-closed', () => app.quit());
101
+ `;
102
+ }
103
+
104
+ function preloadTemplate() {
105
+ return `'use strict';
106
+
107
+ const { contextBridge, ipcRenderer } = require('electron');
108
+
109
+ contextBridge.exposeInMainWorld('atom', {
110
+ openFile: () => ipcRenderer.invoke('file:open'),
111
+ onMessage: (listener) => ipcRenderer.on('app:message', (_event, message) => listener(message))
112
+ });
113
+ `;
114
+ }
115
+
116
+ function htmlTemplate(productName) {
117
+ return `<!doctype html>
118
+ <html lang="en">
119
+ <head>
120
+ <meta charset="UTF-8">
121
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
122
+ <title>${escapeHtml(productName)}</title>
123
+ <style>
124
+ :root { color-scheme: dark; font-family: Inter, system-ui, sans-serif; }
125
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #202124; color: #f5f7fb; }
126
+ main { width: min(720px, calc(100% - 48px)); }
127
+ h1 { font-size: clamp(2.5rem, 8vw, 5rem); margin: 0 0 12px; }
128
+ p { color: #aeb8c8; font-size: 1.1rem; }
129
+ button { border: 0; border-radius: 10px; padding: 12px 18px; font: inherit; cursor: pointer; background: #7ddff2; color: #14202a; font-weight: 700; }
130
+ pre { min-height: 160px; overflow: auto; padding: 18px; border-radius: 12px; background: #17181a; white-space: pre-wrap; }
131
+ </style>
132
+ </head>
133
+ <body>
134
+ <main>
135
+ <h1>${escapeHtml(productName)}</h1>
136
+ <p>Fast, lightweight desktop UI powered by the system WebView.</p>
137
+ <button id="open">Open a text file</button>
138
+ <pre id="output">Ready.</pre>
139
+ </main>
140
+ <script src="renderer.js"></script>
141
+ </body>
142
+ </html>
143
+ `;
144
+ }
145
+
146
+ function rendererTemplate() {
147
+ return `document.querySelector('#open').addEventListener('click', async () => {
148
+ const result = await window.atom.openFile();
149
+ document.querySelector('#output').textContent = result ? result.content : 'Cancelled.';
150
+ });
151
+ `;
152
+ }
153
+
154
+ function workflowTemplate() {
155
+ return `name: AtomJS Build
156
+
157
+ on:
158
+ workflow_dispatch:
159
+ inputs:
160
+ target:
161
+ description: windows, macos, linux, or all
162
+ required: true
163
+ default: all
164
+ type: choice
165
+ options: [all, windows, macos, linux]
166
+ project:
167
+ description: Project directory inside the repository
168
+ required: true
169
+ default: .
170
+
171
+ jobs:
172
+ windows:
173
+ if: inputs.target == 'all' || inputs.target == 'windows'
174
+ runs-on: windows-latest
175
+ steps:
176
+ - uses: actions/checkout@v4
177
+ - uses: actions/setup-node@v4
178
+ with:
179
+ node-version: 22
180
+ cache: npm
181
+ - run: choco install nsis -y
182
+ - run: npm ci
183
+ working-directory: \${{ inputs.project }}
184
+ - run: npx atom build windows --local
185
+ working-directory: \${{ inputs.project }}
186
+ - uses: actions/upload-artifact@v4
187
+ with:
188
+ name: atom-build-windows
189
+ path: \${{ inputs.project }}/build/windows
190
+ if-no-files-found: error
191
+
192
+ macos:
193
+ if: inputs.target == 'all' || inputs.target == 'macos'
194
+ runs-on: macos-latest
195
+ steps:
196
+ - uses: actions/checkout@v4
197
+ - uses: actions/setup-node@v4
198
+ with:
199
+ node-version: 22
200
+ cache: npm
201
+ - run: npm ci
202
+ working-directory: \${{ inputs.project }}
203
+ - run: npx atom build macos --local
204
+ working-directory: \${{ inputs.project }}
205
+ - uses: actions/upload-artifact@v4
206
+ with:
207
+ name: atom-build-macos
208
+ path: \${{ inputs.project }}/build/macos
209
+ if-no-files-found: error
210
+
211
+ linux:
212
+ if: inputs.target == 'all' || inputs.target == 'linux'
213
+ runs-on: ubuntu-latest
214
+ steps:
215
+ - uses: actions/checkout@v4
216
+ - run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev zenity
217
+ - uses: actions/setup-node@v4
218
+ with:
219
+ node-version: 22
220
+ cache: npm
221
+ - run: |
222
+ sudo curl -L -o /usr/local/bin/appimagetool https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
223
+ sudo chmod +x /usr/local/bin/appimagetool
224
+ - run: npm ci
225
+ working-directory: \${{ inputs.project }}
226
+ - run: npx atom build linux --local
227
+ working-directory: \${{ inputs.project }}
228
+ env:
229
+ APPIMAGE_EXTRACT_AND_RUN: '1'
230
+ - uses: actions/upload-artifact@v4
231
+ with:
232
+ name: atom-build-linux
233
+ path: \${{ inputs.project }}/build/linux
234
+ if-no-files-found: error
235
+ `;
236
+ }
237
+
238
+ function sanitizePackageName(value) {
239
+ return String(value).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'atomjs-app';
240
+ }
241
+
242
+ function humanize(value) {
243
+ return String(value).split(/[-_.]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(' ');
244
+ }
245
+
246
+ function hostTarget() {
247
+ return process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux';
248
+ }
249
+
250
+ function escapeHtml(value) {
251
+ return String(value).replace(/[&<>\"]/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[char]);
252
+ }
253
+
254
+ module.exports = { initCommand };
package/src/run.cjs ADDED
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const fs = require('node:fs');
5
+ const { spawn } = require('node:child_process');
6
+ const { loadProject, hostTarget } = require('./utils.cjs');
7
+ const { ensureElectronCompatibility } = require('./electron-compat.cjs');
8
+
9
+ async function runCommand(mode, options = {}) {
10
+ const normalized = String(mode).toLowerCase();
11
+ if (normalized === 'dev') return runDev(options);
12
+ if (normalized === 'build') return runBuild(options);
13
+ throw new Error(`Unknown run mode '${mode}'. Use dev or build.`);
14
+ }
15
+
16
+ async function runDev(options) {
17
+ const project = loadProject(options.project);
18
+ const mainPath = path.resolve(project.root, project.config.main);
19
+ if (!fs.existsSync(mainPath)) throw new Error(`Main file not found: ${mainPath}`);
20
+
21
+ console.log(`AtomJS dev: ${project.config.productName}`);
22
+ console.log(`Main: ${path.relative(project.root, mainPath)}`);
23
+
24
+ await ensureElectronCompatibility(project.root);
25
+
26
+ const child = spawn(process.execPath, [mainPath], {
27
+ cwd: project.root,
28
+ env: {
29
+ ...process.env,
30
+ ATOM_PROJECT_ROOT: project.root,
31
+ ATOM_DEV: '1'
32
+ },
33
+ stdio: 'inherit'
34
+ });
35
+
36
+ await new Promise((resolve, reject) => {
37
+ child.once('error', reject);
38
+ child.once('exit', (code, signal) => {
39
+ if (code === 0 || signal === 'SIGTERM' || signal === 'SIGINT') resolve();
40
+ else reject(new Error(`Application exited with code ${code}`));
41
+ });
42
+ });
43
+ }
44
+
45
+ async function runBuild(options) {
46
+ const project = loadProject(options.project);
47
+ const target = hostTarget();
48
+ const manifestPath = path.join(project.root, 'build', target, 'manifest.json');
49
+ if (!fs.existsSync(manifestPath)) {
50
+ throw new Error(`No ${target} build found. Run 'atom build ${target}' first.`);
51
+ }
52
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
53
+ const executable = path.resolve(project.root, 'build', target, manifest.run);
54
+ if (!fs.existsSync(executable)) throw new Error(`Built executable not found: ${executable}`);
55
+
56
+ console.log(`Running ${path.basename(executable)}`);
57
+ const child = spawn(executable, [], {
58
+ cwd: path.dirname(executable),
59
+ stdio: 'inherit'
60
+ });
61
+ await new Promise((resolve, reject) => {
62
+ child.once('error', reject);
63
+ child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`Build exited with code ${code}`)));
64
+ });
65
+ }
66
+
67
+ module.exports = { runCommand };
package/src/utils.cjs ADDED
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawn, spawnSync } = require('node:child_process');
6
+
7
+ const TARGETS = new Set(['windows', 'macos', 'linux', 'all']);
8
+
9
+ function hostTarget() {
10
+ if (process.platform === 'win32') return 'windows';
11
+ if (process.platform === 'darwin') return 'macos';
12
+ return 'linux';
13
+ }
14
+
15
+ function resolveProject(input) {
16
+ let current = path.resolve(input || process.cwd());
17
+ if (fs.existsSync(current) && fs.statSync(current).isFile()) current = path.dirname(current);
18
+
19
+ while (true) {
20
+ if (fs.existsSync(path.join(current, 'atom.config.json')) || fs.existsSync(path.join(current, 'package.json'))) {
21
+ return current;
22
+ }
23
+ const parent = path.dirname(current);
24
+ if (parent === current) break;
25
+ current = parent;
26
+ }
27
+ throw new Error(`Could not find an AtomJS project from ${input || process.cwd()}`);
28
+ }
29
+
30
+ function readJson(filePath) {
31
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
32
+ }
33
+
34
+ function loadProject(projectInput) {
35
+ const root = resolveProject(projectInput);
36
+ const packagePath = path.join(root, 'package.json');
37
+ if (!fs.existsSync(packagePath)) throw new Error(`Missing package.json in ${root}`);
38
+ const packageJson = readJson(packagePath);
39
+ const configPath = path.join(root, 'atom.config.json');
40
+ const config = fs.existsSync(configPath) ? readJson(configPath) : {};
41
+
42
+ return {
43
+ root,
44
+ packageJson,
45
+ config: {
46
+ appId: config.appId || `com.atomjs.${sanitizeId(packageJson.name || 'app')}`,
47
+ productName: config.productName || packageJson.productName || packageJson.name || 'AtomJS App',
48
+ main: config.main || packageJson.main || 'main.js',
49
+ icon: config.icon || null,
50
+ files: config.files || ['**/*'],
51
+ installerCredit: config.installerCredit !== false,
52
+ github: config.github || {}
53
+ }
54
+ };
55
+ }
56
+
57
+ function run(command, args, options = {}) {
58
+ return new Promise((resolve, reject) => {
59
+ const child = spawn(command, args, {
60
+ cwd: options.cwd || process.cwd(),
61
+ env: options.env || process.env,
62
+ stdio: options.stdio || 'inherit',
63
+ shell: false
64
+ });
65
+ child.once('error', reject);
66
+ child.once('exit', (code, signal) => {
67
+ if (code === 0) resolve({ code, signal });
68
+ else reject(new Error(`${command} exited with code ${code}${signal ? ` (${signal})` : ''}`));
69
+ });
70
+ });
71
+ }
72
+
73
+ function capture(command, args, options = {}) {
74
+ const result = spawnSync(command, args, {
75
+ cwd: options.cwd || process.cwd(),
76
+ env: options.env || process.env,
77
+ encoding: 'utf8',
78
+ shell: false
79
+ });
80
+ if (result.error) throw result.error;
81
+ if (result.status !== 0) {
82
+ const message = (result.stderr || result.stdout || '').trim();
83
+ throw new Error(message || `${command} exited with code ${result.status}`);
84
+ }
85
+ return (result.stdout || '').trim();
86
+ }
87
+
88
+ function commandExists(command, args = ['--version']) {
89
+ const result = spawnSync(command, args, { stdio: 'ignore', shell: false });
90
+ return !result.error && result.status === 0;
91
+ }
92
+
93
+ function validateTarget(target) {
94
+ const normalized = String(target).toLowerCase();
95
+ if (!TARGETS.has(normalized)) {
96
+ throw new Error(`Unknown build target '${target}'. Use windows, macos, linux, or all.`);
97
+ }
98
+ return normalized;
99
+ }
100
+
101
+ function sanitizeId(value) {
102
+ return String(value).toLowerCase().replace(/[^a-z0-9.-]+/g, '-').replace(/^-+|-+$/g, '') || 'app';
103
+ }
104
+
105
+ function sanitizeFilename(value) {
106
+ return String(value).replace(/[<>:"/\\|?*\x00-\x1F]/g, '-').replace(/\s+/g, ' ').trim() || 'AtomJS App';
107
+ }
108
+
109
+ module.exports = {
110
+ TARGETS,
111
+ hostTarget,
112
+ resolveProject,
113
+ readJson,
114
+ loadProject,
115
+ run,
116
+ capture,
117
+ commandExists,
118
+ validateTarget,
119
+ sanitizeId,
120
+ sanitizeFilename
121
+ };