@hsusulist/luc 0.2.5 → 0.3.1

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,25 +1,31 @@
1
1
  # @hsusulist/luc
2
2
 
3
- Install and launch the [LUC](https://github.com/hsusulist/luc) language on Windows.
3
+ Install and launch the [LUC](https://github.com/hsusulist/luc) language on Windows or Linux.
4
4
 
5
5
  ```powershell
6
6
  npm install -g @hsusulist/luc
7
7
  luc --version
8
8
  ```
9
9
 
10
- The package installs the runtime into `%LOCALAPPDATA%\Programs\LUC` (no PATH
11
- changes, no admin rights). If the automatic install is skipped, run:
10
+ On Linux, the package compiles the runtime during installation. Install a C
11
+ compiler such as GCC or Clang first; set `CC` if your compiler is not named
12
+ `cc`. Run `luc install` to rebuild the native runtime.
13
+
14
+ On Windows, the package installs the runtime into
15
+ `%LOCALAPPDATA%\Programs\LUC` (no PATH changes, no admin rights). If the
16
+ automatic install is skipped, run:
12
17
 
13
18
  ```powershell
14
- luc --install
19
+ luc install
15
20
  ```
16
21
 
17
- `luc --install` opens the terminal installer (license, components, folder and a
18
- Continue button). Unattended installs:
22
+ `luc install` opens the full-screen terminal installer. When LUC is already
23
+ installed, it offers component management and repair; installs made with the
24
+ Windows installer also include an uninstall option. Unattended installs:
19
25
 
20
26
  ```powershell
21
- luc --install -y --no-path
22
- luc --install -c window,ai -d "D:\Tools\LUC" -y
27
+ luc install -y --no-path
28
+ luc install -c window,ai -d "D:\Tools\LUC" -y
23
29
  ```
24
30
 
25
31
  Available components: `window`, `ai`, `discord`, `lcode`, `vscode`, `source`.
@@ -27,10 +33,24 @@ LCode is optional and is not selected by default. Selecting it installs
27
33
  `lcode.exe` alongside the interpreter. To install only the editor without the
28
34
  LUC command, use `npm install -g @hsusulist/lcode`.
29
35
 
30
- The npm package is a small launcher; the runtime is downloaded from the signed
31
- GitHub release and verified with SHA-256 and Ed25519 (minisign format) before it
32
- is installed.
36
+ On Windows, the npm package is a small launcher; the runtime is downloaded from
37
+ the signed GitHub release and verified with SHA-256 and Ed25519 (minisign
38
+ format) before it is installed. On Linux, the package builds from its bundled
39
+ LUC and LCode C sources.
40
+
41
+ The editor-only package uses the separate, signed `lcode-portable.zip` release
42
+ asset on Windows. On Linux it compiles the included native terminal editor.
43
+
44
+ ## Registry
45
+
46
+ ```powershell
47
+ luc login
48
+ luc whoami
49
+ luc install -p @username/packagename
50
+ luc publish -p
51
+ ```
33
52
 
34
- The editor-only npm package uses the separate, signed `lcode-portable.zip`
35
- release asset. Generate it with `tools/make_portable.ps1` and attach the ZIP,
36
- `.sha256`, and `.sha256.minisig` files to each GitHub release.
53
+ `luc publish -p` uploads the `.luic` file and optional `README.md` from the
54
+ current folder, then opens a browser to enter the package name and summary.
55
+ Login state is scoped to the current terminal session. Set `LUC_REGISTRY_URL`
56
+ to use a different registry (defaults to `http://localhost:3000`).
package/bin/luc.js CHANGED
@@ -5,6 +5,11 @@ const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
7
  const { spawnSync } = require('child_process');
8
+ const http = require('http');
9
+ const https = require('https');
10
+ const { promisify } = require('util');
11
+
12
+ const args = process.argv.slice(2);
8
13
 
9
14
  function stateBinary() {
10
15
  const local = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
@@ -94,19 +99,304 @@ function mapInstallerArgs(args) {
94
99
  return args.map((arg) => (Object.prototype.hasOwnProperty.call(map, arg) ? map[arg] : arg));
95
100
  }
96
101
 
102
+ const sleep = promisify(setTimeout);
103
+ const registry = (process.env.LUC_REGISTRY_URL || 'http://localhost:3000').replace(/\/+$/, '');
104
+
105
+ function request(url, options = {}) {
106
+ return new Promise((resolve, reject) => {
107
+ const target = new URL(url);
108
+ const client = target.protocol === 'https:' ? https : http;
109
+ const req = client.request(target, {
110
+ method: options.method || 'GET',
111
+ headers: options.headers || {}
112
+ }, (res) => {
113
+ const chunks = [];
114
+ res.on('data', (chunk) => chunks.push(chunk));
115
+ res.on('end', () => resolve({
116
+ status: res.statusCode || 0,
117
+ headers: res.headers,
118
+ body: Buffer.concat(chunks)
119
+ }));
120
+ });
121
+ req.on('error', reject);
122
+ if (options.body) req.write(options.body);
123
+ req.end();
124
+ });
125
+ }
126
+
127
+ async function requestJson(pathname, options = {}) {
128
+ const headers = Object.assign({}, options.headers || {});
129
+ let body = options.body;
130
+ if (body && !Buffer.isBuffer(body) && typeof body !== 'string') {
131
+ body = JSON.stringify(body);
132
+ headers['Content-Type'] = 'application/json';
133
+ }
134
+ const response = await request(registry + pathname, { method: options.method, headers, body });
135
+ let payload;
136
+ try {
137
+ payload = JSON.parse(response.body.toString('utf8'));
138
+ } catch (_) {
139
+ payload = { error: response.body.toString('utf8') || 'Unexpected server response.' };
140
+ }
141
+ if (response.status < 200 || response.status >= 300) {
142
+ throw new Error(payload.error || 'Request failed with status ' + response.status + '.');
143
+ }
144
+ return payload;
145
+ }
146
+
147
+ function authPath() {
148
+ const base = process.env.LOCALAPPDATA || process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
149
+ const terminal = process.env.LUC_TERMINAL_SESSION || String(process.ppid);
150
+ const key = require('crypto').createHash('sha256').update(terminal).digest('hex').slice(0, 24);
151
+ const dir = path.join(base, 'LUC', 'auth');
152
+ fs.mkdirSync(dir, { recursive: true });
153
+ return path.join(dir, key + '.json');
154
+ }
155
+
156
+ function readAuth() {
157
+ try {
158
+ return JSON.parse(fs.readFileSync(authPath(), 'utf8'));
159
+ } catch (_) {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ function saveAuth(auth) {
165
+ fs.writeFileSync(authPath(), JSON.stringify(auth), { mode: 0o600 });
166
+ }
167
+
168
+ function clearAuth() {
169
+ try { fs.unlinkSync(authPath()); } catch (error) {
170
+ if (error.code !== 'ENOENT') throw error;
171
+ }
172
+ }
173
+
174
+ function openBrowser(url) {
175
+ let command;
176
+ let args;
177
+ if (process.platform === 'win32') {
178
+ command = 'rundll32.exe';
179
+ args = ['url.dll,FileProtocolHandler', url];
180
+ } else if (process.platform === 'darwin') {
181
+ command = 'open';
182
+ args = [url];
183
+ } else {
184
+ command = 'xdg-open';
185
+ args = [url];
186
+ }
187
+ const child = spawnSync(command, args, { stdio: 'ignore' });
188
+ if (child.error || child.status !== 0) {
189
+ console.error('luc: could not open a browser. Open this URL manually:\n' + url);
190
+ }
191
+ }
192
+
193
+ async function login() {
194
+ const started = await requestJson('/api/auth/cli/start', { method: 'POST', body: {} });
195
+ console.log('Waiting for LUC browser sign-in…');
196
+ openBrowser(started.loginUrl);
197
+ const deadline = Date.now() + 10 * 60 * 1000;
198
+ while (Date.now() < deadline) {
199
+ await sleep(1500);
200
+ const response = await request(registry + '/api/auth/cli/poll', {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify({ code: started.code })
204
+ });
205
+ if (response.status === 202) continue;
206
+ let data;
207
+ try { data = JSON.parse(response.body.toString('utf8')); } catch (_) { data = {}; }
208
+ if (response.status < 200 || response.status >= 300) throw new Error(data.error || 'Sign-in failed.');
209
+ saveAuth({ token: data.token, username: data.user.username });
210
+ console.log('Logged in as @' + data.user.username + ' for this terminal.');
211
+ return;
212
+ }
213
+ throw new Error('Sign-in timed out. Run luc login again.');
214
+ }
215
+
216
+ async function whoami() {
217
+ const auth = readAuth();
218
+ if (!auth || !auth.token) throw new Error('Not logged in. Run "luc login" in this terminal.');
219
+ try {
220
+ const data = await requestJson('/api/auth/me', { headers: { Authorization: 'Bearer ' + auth.token } });
221
+ console.log('@' + data.user.username);
222
+ } catch (error) {
223
+ clearAuth();
224
+ throw error;
225
+ }
226
+ }
227
+
228
+ async function installPackage(identifier) {
229
+ const match = identifier.match(/^@([a-z0-9_-]{3,32})\/([a-z][a-z0-9-]{1,38}[a-z0-9])$/i);
230
+ if (!match) throw new Error('Use a package name like @username/packagename.');
231
+ const username = match[1].toLowerCase();
232
+ const name = match[2].toLowerCase();
233
+ const data = await requestJson('/api/packages?q=' + encodeURIComponent(name) + '&sort=downloads');
234
+ const pkg = (data.packages || []).find((item) =>
235
+ item.name.toLowerCase() === name && String(item.author.handle || '').toLowerCase() === username
236
+ );
237
+ if (!pkg) throw new Error('No package named ' + identifier + ' was found.');
238
+ const response = await request(registry + '/api/packages/' + encodeURIComponent(pkg.slug) + '/file');
239
+ if (response.status < 200 || response.status >= 300) {
240
+ let message = response.body.toString('utf8');
241
+ try { message = JSON.parse(message).error || message; } catch (_) {}
242
+ if (pkg.official && ['libs', 'net', 'json'].includes(pkg.name)) {
243
+ await requestJson('/api/packages/' + encodeURIComponent(pkg.slug) + '/install', { method: 'POST', body: {} });
244
+ console.log(pkg.name + ' is already included with the LUC runtime.');
245
+ return;
246
+ }
247
+ if (pkg.official && ['window', 'ai', 'discord'].includes(pkg.name)) {
248
+ const binary = process.platform === 'linux'
249
+ ? path.join(__dirname, '..', 'native', 'luc')
250
+ : findBinary();
251
+ if (!binary || !fs.existsSync(binary)) throw new Error('The LUC runtime is missing. Run "luc install" first.');
252
+ const result = spawnSync(binary, ['install', pkg.name], { stdio: 'inherit' });
253
+ if (result.error || result.status !== 0) throw new Error(result.error ? result.error.message : 'LUC could not install ' + pkg.name + '.');
254
+ await requestJson('/api/packages/' + encodeURIComponent(pkg.slug) + '/install', { method: 'POST', body: {} });
255
+ return;
256
+ }
257
+ throw new Error(message || 'The package has no downloadable .luic file.');
258
+ }
259
+ const destination = path.resolve(process.cwd(), pkg.name + '.luic');
260
+ if (fs.existsSync(destination)) throw new Error(pkg.name + '.luic already exists in this folder.');
261
+ fs.writeFileSync(destination, response.body, { flag: 'wx' });
262
+ await requestJson('/api/packages/' + encodeURIComponent(pkg.slug) + '/install', { method: 'POST', body: {} });
263
+ console.log('Installed ' + identifier + ' to ' + destination);
264
+ }
265
+
266
+ async function publishPackage() {
267
+ const auth = readAuth();
268
+ if (!auth || !auth.token) throw new Error('Not logged in. Run "luc login" in this terminal first.');
269
+ const files = fs.readdirSync(process.cwd()).filter((name) => name.toLowerCase().endsWith('.luic'));
270
+ if (files.length === 0) throw new Error('No .luic file found in the current folder.');
271
+ if (files.length > 1) throw new Error('Found multiple .luic files. Keep only the package you want to publish in this folder.');
272
+ const filename = files[0];
273
+ const bytes = fs.readFileSync(path.join(process.cwd(), filename));
274
+ const upload = await requestJson('/api/auth/cli/uploads', {
275
+ method: 'POST',
276
+ headers: {
277
+ Authorization: 'Bearer ' + auth.token,
278
+ 'Content-Type': 'application/octet-stream',
279
+ 'X-File-Name': filename
280
+ },
281
+ body: bytes
282
+ });
283
+ const readmePath = ['README.md', 'readme.md', 'README'].map((name) => path.join(process.cwd(), name)).find(fs.existsSync);
284
+ const readme = readmePath ? fs.readFileSync(readmePath, 'utf8') : '';
285
+ if (readme.length > 60000) throw new Error('README.md must be 60,000 characters or fewer.');
286
+ await requestJson('/api/auth/cli/uploads/' + encodeURIComponent(upload.id) + '/readme', {
287
+ method: 'POST',
288
+ headers: { Authorization: 'Bearer ' + auth.token },
289
+ body: { readme }
290
+ });
291
+ const publishUrl = registry + '/#/publish?upload=' + encodeURIComponent(upload.id);
292
+ console.log('Package file and README attached. Finish the package name and description in your browser.');
293
+ openBrowser(publishUrl);
294
+ }
295
+
296
+ async function handleSiteCommand() {
297
+ const [command, ...rest] = args;
298
+ if (command === 'login') {
299
+ if (rest.length) throw new Error('Usage: luc login');
300
+ return login();
301
+ }
302
+ if (command === 'whoami') {
303
+ if (rest.length) throw new Error('Usage: luc whoami');
304
+ return whoami();
305
+ }
306
+ if (command === 'logout') {
307
+ if (rest.length) throw new Error('Usage: luc logout');
308
+ const auth = readAuth();
309
+ if (auth && auth.token) {
310
+ try {
311
+ await requestJson('/api/auth/logout', {
312
+ method: 'POST',
313
+ headers: { Authorization: 'Bearer ' + auth.token },
314
+ body: {}
315
+ });
316
+ } catch (_) {
317
+ }
318
+ }
319
+ clearAuth();
320
+ console.log('Logged out of this terminal.');
321
+ return;
322
+ }
323
+ if (command === 'publish') {
324
+ if (rest.length !== 1 || rest[0] !== '-p') throw new Error('Usage: luc publish -p');
325
+ return publishPackage();
326
+ }
327
+ if (command === 'install' && rest[0] === '-p') {
328
+ if (rest.length !== 2 || !rest[1]) throw new Error('Usage: luc install -p @username/packagename');
329
+ return installPackage(rest[1]);
330
+ }
331
+ return null;
332
+ }
333
+
334
+ async function runSiteCommand() {
335
+ try {
336
+ const handled = await handleSiteCommand();
337
+ if (handled !== null) return true;
338
+ return false;
339
+ } catch (error) {
340
+ console.error('luc: ' + (error.message || error));
341
+ process.exitCode = 1;
342
+ return true;
343
+ }
344
+ }
345
+
346
+ if (args[0] === 'login' || args[0] === 'whoami' || args[0] === 'logout' || args[0] === 'publish') {
347
+ runSiteCommand().then((handled) => { if (!handled) process.exitCode = 1; });
348
+ return;
349
+ }
350
+
351
+ if (args[0] === 'install' && args[1] === '-p') {
352
+ runSiteCommand().then((handled) => { if (!handled) process.exitCode = 1; });
353
+ return;
354
+ }
355
+
356
+ if (args[0] === 'install' && (args.length === 1 || args[1].startsWith('-'))) {
357
+ if (process.platform === 'linux') {
358
+ try {
359
+ const packageDir = path.join(__dirname, '..');
360
+ const { buildLinux } = require('../scripts/build-linux');
361
+ buildLinux(packageDir, 'luc');
362
+ } catch (error) {
363
+ console.error('luc: ' + error.message);
364
+ process.exitCode = 1;
365
+ }
366
+ } else if (process.platform === 'win32') {
367
+ process.exitCode = runInstaller(mapInstallerArgs(args.slice(1)));
368
+ } else {
369
+ console.error('luc: this package supports Windows and Linux only.');
370
+ process.exitCode = 1;
371
+ }
372
+ return;
373
+ }
374
+
375
+ if (process.platform === 'linux') {
376
+ const packageDir = path.join(__dirname, '..');
377
+ const binary = path.join(packageDir, 'native', 'luc');
378
+ if (!fs.existsSync(binary)) {
379
+ console.error('luc: Linux runtime is missing; run "luc install" to build it.');
380
+ process.exit(1);
381
+ }
382
+ const result = spawnSync(binary, args, { stdio: 'inherit' });
383
+ if (result.error) {
384
+ console.error('luc: ' + result.error.message);
385
+ process.exit(1);
386
+ }
387
+ process.exit(result.status === null ? 1 : result.status);
388
+ }
389
+
97
390
  if (process.platform !== 'win32') {
98
- console.error('luc: this package currently supports Windows only.');
391
+ console.error('luc: this package supports Windows and Linux only.');
99
392
  process.exit(1);
100
393
  }
101
394
 
102
- const args = process.argv.slice(2);
103
395
  let binary = findBinary();
104
396
 
105
397
  if (!binary) {
106
- const isInstall = args[0] === '--install';
107
- const status = runInstaller(isInstall ? mapInstallerArgs(args.slice(1)) : []);
398
+ const status = runInstaller([]);
108
399
  if (status !== 0) process.exit(status);
109
- if (isInstall) process.exit(0);
110
400
  binary = findBinary();
111
401
  if (!binary) {
112
402
  console.error('luc: installation finished but luc.exe was not found.');