@hsusulist/luc 0.3.0 → 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
@@ -9,22 +9,23 @@ luc --version
9
9
 
10
10
  On Linux, the package compiles the runtime during installation. Install a C
11
11
  compiler such as GCC or Clang first; set `CC` if your compiler is not named
12
- `cc`. `luc --install` rebuilds the native runtime.
12
+ `cc`. Run `luc install` to rebuild the native runtime.
13
13
 
14
14
  On Windows, the package installs the runtime into
15
15
  `%LOCALAPPDATA%\Programs\LUC` (no PATH changes, no admin rights). If the
16
16
  automatic install is skipped, run:
17
17
 
18
18
  ```powershell
19
- luc --install
19
+ luc install
20
20
  ```
21
21
 
22
- `luc --install` opens the terminal installer (license, components, folder and a
23
- 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:
24
25
 
25
26
  ```powershell
26
- luc --install -y --no-path
27
- 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
28
29
  ```
29
30
 
30
31
  Available components: `window`, `ai`, `discord`, `lcode`, `vscode`, `source`.
@@ -39,3 +40,17 @@ LUC and LCode C sources.
39
40
 
40
41
  The editor-only package uses the separate, signed `lcode-portable.zip` release
41
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
+ ```
52
+
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,29 +5,12 @@ 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');
8
11
 
9
12
  const args = process.argv.slice(2);
10
13
 
11
- if (process.platform === 'linux') {
12
- const packageDir = path.join(__dirname, '..');
13
- const { buildLinux } = require('../scripts/build-linux');
14
- if (args[0] === '--install') {
15
- buildLinux(packageDir, 'luc');
16
- process.exit(0);
17
- }
18
- const binary = path.join(packageDir, 'native', 'luc');
19
- if (!fs.existsSync(binary)) {
20
- console.error('luc: Linux runtime is missing; run "luc --install" to build it.');
21
- process.exit(1);
22
- }
23
- const result = spawnSync(binary, args, { stdio: 'inherit' });
24
- if (result.error) {
25
- console.error('luc: ' + result.error.message);
26
- process.exit(1);
27
- }
28
- process.exit(result.status === null ? 1 : result.status);
29
- }
30
-
31
14
  function stateBinary() {
32
15
  const local = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
33
16
  try {
@@ -116,6 +99,294 @@ function mapInstallerArgs(args) {
116
99
  return args.map((arg) => (Object.prototype.hasOwnProperty.call(map, arg) ? map[arg] : arg));
117
100
  }
118
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
+
119
390
  if (process.platform !== 'win32') {
120
391
  console.error('luc: this package supports Windows and Linux only.');
121
392
  process.exit(1);
@@ -124,10 +395,8 @@ if (process.platform !== 'win32') {
124
395
  let binary = findBinary();
125
396
 
126
397
  if (!binary) {
127
- const isInstall = args[0] === '--install';
128
- const status = runInstaller(isInstall ? mapInstallerArgs(args.slice(1)) : []);
398
+ const status = runInstaller([]);
129
399
  if (status !== 0) process.exit(status);
130
- if (isInstall) process.exit(0);
131
400
  binary = findBinary();
132
401
  if (!binary) {
133
402
  console.error('luc: installation finished but luc.exe was not found.');