lecodes-cli 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +93 -47
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2007,7 +2007,7 @@ var require_main = __commonJS((exports, module) => {
2007
2007
  // package.json
2008
2008
  var package_default = {
2009
2009
  name: "lecodes-cli",
2010
- version: "0.8.1",
2010
+ version: "0.8.2",
2011
2011
  description: "Clone, edit and push LeCodes projects from your machine.",
2012
2012
  type: "module",
2013
2013
  license: "MIT",
@@ -2203,9 +2203,42 @@ var writeManifest = (root, manifest) => {
2203
2203
  writeFileSync(manifestPath(root), JSON.stringify(manifest, null, 2));
2204
2204
  };
2205
2205
 
2206
- // src/ignore.ts
2207
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
2206
+ // src/config.ts
2207
+ import { homedir } from "node:os";
2208
2208
  import { join as join2 } from "node:path";
2209
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
2210
+ var DEFAULT_API = "https://le.codes";
2211
+ var CONFIG_DIR = join2(homedir(), ".lecodes");
2212
+ var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
2213
+ var loadConfig = () => {
2214
+ let stored = {};
2215
+ try {
2216
+ stored = JSON.parse(readFileSync2(CONFIG_FILE, "utf8"));
2217
+ } catch {}
2218
+ return {
2219
+ apiUrl: process.env.LECODES_API ?? stored.apiUrl,
2220
+ token: process.env.LECODES_TOKEN ?? stored.token
2221
+ };
2222
+ };
2223
+ var saveConfig = (config) => {
2224
+ mkdirSync2(CONFIG_DIR, { recursive: true });
2225
+ writeFileSync2(CONFIG_FILE, JSON.stringify(config, null, 2));
2226
+ };
2227
+ var normalizeApiUrl = (url) => url.trim().replace(/\/+$/, "");
2228
+ var requireToken = (config) => {
2229
+ if (!config.token)
2230
+ throw new CliError("Not logged in. Run `lecodes login` first.");
2231
+ return config.token;
2232
+ };
2233
+ var requireApiUrl = (config) => {
2234
+ if (!config.apiUrl)
2235
+ throw new CliError("No server URL configured. Run `lecodes login` first.");
2236
+ return normalizeApiUrl(config.apiUrl);
2237
+ };
2238
+
2239
+ // src/ignore.ts
2240
+ import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
2241
+ import { join as join3 } from "node:path";
2209
2242
  var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store"];
2210
2243
  var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
2211
2244
  # Only untracked files are affected — assets already in the project stay tracked
@@ -2286,19 +2319,19 @@ var buildGlobMatcher = (patterns) => {
2286
2319
  };
2287
2320
  var loadIgnore = (root) => {
2288
2321
  const lines = [...BUILTIN_PATTERNS];
2289
- const file = join2(root, ".lecodesignore");
2322
+ const file = join3(root, ".lecodesignore");
2290
2323
  if (existsSync2(file)) {
2291
2324
  try {
2292
- lines.push(...readFileSync2(file, "utf8").split(/\r?\n/));
2325
+ lines.push(...readFileSync3(file, "utf8").split(/\r?\n/));
2293
2326
  } catch {}
2294
2327
  }
2295
2328
  const rules = lines.map(compile).filter((r) => r !== null);
2296
2329
  return buildMatcher(rules);
2297
2330
  };
2298
2331
  var writeDefaultIgnore = (root) => {
2299
- const file = join2(root, ".lecodesignore");
2332
+ const file = join3(root, ".lecodesignore");
2300
2333
  if (!existsSync2(file))
2301
- writeFileSync2(file, DEFAULT_IGNORE_FILE);
2334
+ writeFileSync3(file, DEFAULT_IGNORE_FILE);
2302
2335
  };
2303
2336
 
2304
2337
  // src/commands/compile.ts
@@ -2370,39 +2403,6 @@ var downloadBlob = async (apiUrl, src) => {
2370
2403
  return new Uint8Array(await resp.arrayBuffer());
2371
2404
  };
2372
2405
 
2373
- // src/config.ts
2374
- import { homedir } from "node:os";
2375
- import { join as join3 } from "node:path";
2376
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
2377
- var DEFAULT_API = "https://le.codes";
2378
- var CONFIG_DIR = join3(homedir(), ".lecodes");
2379
- var CONFIG_FILE = join3(CONFIG_DIR, "config.json");
2380
- var loadConfig = () => {
2381
- let stored = {};
2382
- try {
2383
- stored = JSON.parse(readFileSync3(CONFIG_FILE, "utf8"));
2384
- } catch {}
2385
- return {
2386
- apiUrl: process.env.LECODES_API ?? stored.apiUrl,
2387
- token: process.env.LECODES_TOKEN ?? stored.token
2388
- };
2389
- };
2390
- var saveConfig = (config) => {
2391
- mkdirSync2(CONFIG_DIR, { recursive: true });
2392
- writeFileSync3(CONFIG_FILE, JSON.stringify(config, null, 2));
2393
- };
2394
- var normalizeApiUrl = (url) => url.trim().replace(/\/+$/, "");
2395
- var requireToken = (config) => {
2396
- if (!config.token)
2397
- throw new CliError("Not logged in. Run `lecodes login` first.");
2398
- return config.token;
2399
- };
2400
- var requireApiUrl = (config) => {
2401
- if (!config.apiUrl)
2402
- throw new CliError("No server URL configured. Run `lecodes login` first.");
2403
- return normalizeApiUrl(config.apiUrl);
2404
- };
2405
-
2406
2406
  // src/compile/collect.ts
2407
2407
  import { readFileSync as readFileSync5 } from "node:fs";
2408
2408
  import { basename } from "node:path";
@@ -3349,8 +3349,12 @@ class ViewController: LeCodesViewController {
3349
3349
 
3350
3350
  registerBundledPlugins(in: engine)
3351
3351
 
3352
- guard let url = Bundle.main.url(forResource: "app", withExtension: "js"),
3353
- let code = try? String(contentsOf: url, encoding: .utf8) else {
3352
+ // OTA (native, no JS involved): start the check FIRST — even a bundle that crashes
3353
+ // on boot can never block its own fix — then run the newest local bundle (a
3354
+ // downloaded update, or the embedded app.js). New code applies on the next launch.
3355
+ // Configured via app.json "update" (→ Info.plist LeCodesUpdateURL); offline: no-op.
3356
+ LeCodesUpdater.checkForUpdate()
3357
+ guard let code = LeCodesUpdater.activeBundle() else {
3354
3358
  print("LeCodes: bundled app.js is missing — run \`lecodes app sync\`")
3355
3359
  return
3356
3360
  }
@@ -3358,13 +3362,18 @@ class ViewController: LeCodesViewController {
3358
3362
  }
3359
3363
  }
3360
3364
  `;
3361
- var infoPlistTemplate = (pluginKeys) => {
3365
+ var infoPlistTemplate = (pluginKeys, updateUrl) => {
3366
+ const xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3362
3367
  const keys = Object.keys(pluginKeys).sort();
3363
3368
  const pluginBlock = keys.length === 0 ? "" : `
3364
3369
  <!-- lecodes:plugins — merged from installed plugin manifests by \`lecodes app sync\` -->
3365
3370
  ${keys.map((k) => ` <key>${k}</key>
3366
3371
  <string>${pluginKeys[k]}</string>`).join(`
3367
3372
  `)}`;
3373
+ const updateBlock = !updateUrl ? "" : `
3374
+ <!-- lecodes:update — native OTA source (app.json "update"); LeCodesUpdater checks it at launch -->
3375
+ <key>LeCodesUpdateURL</key>
3376
+ <string>${xml(updateUrl)}</string>`;
3368
3377
  return `<?xml version="1.0" encoding="UTF-8"?>
3369
3378
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3370
3379
  <plist version="1.0">
@@ -3385,7 +3394,7 @@ ${keys.map((k) => ` <key>${k}</key>
3385
3394
  </dict>
3386
3395
  </array>
3387
3396
  </dict>
3388
- </dict>${pluginBlock}
3397
+ </dict>${pluginBlock}${updateBlock}
3389
3398
  </dict>
3390
3399
  </plist>
3391
3400
  `;
@@ -3579,6 +3588,16 @@ Sync links the smallest SDK product that covers what the compiled bundle actuall
3579
3588
  project switches it automatically on the next sync; set \`ios.variant\` in \`../app.json\`
3580
3589
  to pin one instead.
3581
3590
 
3591
+ ## Over-the-air updates
3592
+
3593
+ With \`"update": true\` in \`../app.json\` (server-backed projects; or
3594
+ \`"update": {"url": "https://…/app.js"}\` for any static hosting), the app checks that URL
3595
+ at launch — natively, before any JS runs, so a broken bundle can never block its own fix.
3596
+ Unchanged code costs a ~0-byte 304; new code downloads in the background and runs on the
3597
+ NEXT launch. Offline it's a silent no-op — the app always launches from a local bundle.
3598
+ An App Store update always beats a cached OTA bundle. JS only: adding/removing native
3599
+ plugins still needs a store release.
3600
+
3582
3601
  ## Config
3583
3602
 
3584
3603
  Identity lives in \`../app.json\` (name, version, plugins, ios.bundleId / teamId / variant /
@@ -3587,7 +3606,7 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
3587
3606
  `;
3588
3607
 
3589
3608
  // src/commands/app.ts
3590
- var SDK_PACKAGE_VERSION = "1.1.0";
3609
+ var SDK_PACKAGE_VERSION = "1.2.0";
3591
3610
  var SDK_VARIANTS = ["core", "2d", "3d", "full"];
3592
3611
  var FIRST_PARTY_PLUGINS = {
3593
3612
  camera: "letary/lecodes-plugins",
@@ -3809,6 +3828,25 @@ var ensureSdkResourcesBundle = async () => {
3809
3828
  throw new CliError(`No LeCodesSDKResources.bundle inside the release asset ${url}`);
3810
3829
  return bundle;
3811
3830
  };
3831
+ var resolveUpdateUrl = (update, root) => {
3832
+ if (!update)
3833
+ return;
3834
+ let url = typeof update === "string" ? update : typeof update === "object" ? update.url : undefined;
3835
+ if (!url) {
3836
+ let manifest = null;
3837
+ try {
3838
+ manifest = readManifest(root);
3839
+ } catch {}
3840
+ if (!manifest) {
3841
+ warn(`app.json "update": true needs a server-backed project — use "update": {"url": "https://…/app.js"} for custom hosting. OTA left unconfigured.`);
3842
+ return;
3843
+ }
3844
+ url = `${requireApiUrl(loadConfig())}/code/${manifest.uuid}.js`;
3845
+ }
3846
+ if (!url.startsWith("https://"))
3847
+ warn(`update URL is not https — iOS blocks it on device (ATS): ${url}`);
3848
+ return url;
3849
+ };
3812
3850
  var appSync = async (_args) => {
3813
3851
  const root = projectRoot();
3814
3852
  const config = readJson(join11(root, "app.json"));
@@ -3916,7 +3954,10 @@ var appSync = async (_args) => {
3916
3954
  const mergedKeys = {};
3917
3955
  for (const [key, values] of Object.entries(infoValues))
3918
3956
  mergedKeys[key] = [...new Set(values)].join(" ");
3919
- writeFileSync7(join11(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys));
3957
+ const updateUrl = resolveUpdateUrl(config.update, root);
3958
+ writeFileSync7(join11(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
3959
+ if (updateUrl)
3960
+ log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch, new code applies next launch`);
3920
3961
  if (pluginSpecs.length > 0 || existsSync8(lockPath))
3921
3962
  writeJson(lockPath, { plugins: newLock });
3922
3963
  const pbxPath = join11(iosDir, "App.xcodeproj", "project.pbxproj");
@@ -4046,6 +4087,10 @@ var appDoctor = async (_args) => {
4046
4087
  const wanted = Object.keys(config.plugins ?? {});
4047
4088
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
4048
4089
  check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
4090
+ if (config.update) {
4091
+ const plist = readFileSync10(join11(root, "ios", "App", "Info.plist"), "utf8");
4092
+ check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
4093
+ }
4049
4094
  const asc = readJson(join11(homedir2(), ".lecodes", "asc.json"));
4050
4095
  check(!!asc?.keyPath, "App Store Connect API key configured", "pass --key-path/--key-id/--issuer-id to `lecodes app upload` once to store it", false);
4051
4096
  log("");
@@ -7313,7 +7358,8 @@ ${c.bold("Commands:")}
7313
7358
  keeps heavy assets streaming from the server), pick the SDK
7314
7359
  variant from the bundle's engine use, vendor app.json
7315
7360
  plugins (pinned in plugins.lock.json), regenerate the
7316
- CLI-owned shell files
7361
+ CLI-owned shell files. app.json "update": true (or
7362
+ {"url": …}) enables native OTA JS updates at launch
7317
7363
  app plugin <list|add|remove> Manage native plugins in app.json (then run 'lecodes app sync')
7318
7364
  add takes a known id (camera, qr-scanner) or github:owner/repo[@tag]
7319
7365
  app open [ios] Open the native shell in its IDE (Xcode; Android Studio later)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lecodes-cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Clone, edit and push LeCodes projects from your machine.",
5
5
  "type": "module",
6
6
  "license": "MIT",