ezi 2.0.0 → 2.0.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.
Files changed (3) hide show
  1. package/README.md +14 -6
  2. package/dist/index.js +146 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,16 +36,24 @@ npm publish # package `ezi`, bin `ezi` -> dist/index.js
36
36
  Users then run:
37
37
 
38
38
  ```bash
39
- npx -y ezi . # first run downloads ~30-60MB binary to cache, opens editor
39
+ npx -y ezi@latest . # first run downloads ~30-60MB binary to cache, opens editor
40
40
  ezi . # after npm i -g ezi
41
41
  ```
42
42
 
43
- Keep `VERSION` in `src/index.ts`, `version` in `package.json`, and `version`
44
- in `app/Cargo.toml` in sync — the launcher builds its download URL as
45
- `.../releases/download/v<VERSION>/ezicode-<target>.*`.
43
+ Version resolution (no republish needed for editor releases):
44
+
45
+ 1. `EZI_VERSION=0.1.2` pins that release, no network.
46
+ 2. Unset (or `EZI_VERSION=latest`, just `npx ezi`) checks
47
+ `api.github.com/repos/<owner>/<repo>/releases/latest` on every run.
48
+ Same tag reuses the cached binary (no download); new tag downloads once
49
+ to `<cache>/vX.Y.Z/`. Offline / API failure reuses the last-seen tag,
50
+ else the compiled-in default near `app/Cargo.toml`.
51
+
52
+ Only republish `ezi` for launcher bugfixes. Skip the lookup with
53
+ `EZI_NO_UPDATE_CHECK=1`. Optional `EZI_GITHUB_TOKEN` raises API rate limits.
46
54
 
47
55
  ## Cache
48
56
 
49
- - Linux/macOS: `~/.cache/ezi/v<version>/` (or `$XDG_CACHE_HOME`)
57
+ - Linux/macOS: `~/.cache/ezi/v<version>/` (or `$XDG_CACHE_HOME`) + `latest.json` tag cache
50
58
  - Windows: `%LOCALAPPDATA%/ezi/cache/v<version>/`
51
- - Override: `EZI_CACHE`, `EZI_VERSION`, `EZI_REPO`
59
+ - Override: `EZI_CACHE`, `EZI_VERSION`, `EZI_REPO`, `EZI_NO_UPDATE_CHECK=1`
package/dist/index.js CHANGED
@@ -9,9 +9,11 @@
9
9
  * npx ezi --version launcher + editor version
10
10
  *
11
11
  * Env overrides:
12
- * EZI_VERSION GitHub tag version without `v` (default: package version)
12
+ * EZI_VERSION pinned version without `v`, `latest`, or unset (default: auto-detect)
13
13
  * EZI_REPO `owner/repo` (default: olovalabs/ezicode)
14
14
  * EZI_CACHE cache dir override
15
+ * EZI_NO_UPDATE_CHECK set to 1 to skip GitHub lookup, use fallback immediately
16
+ * EZI_GITHUB_TOKEN optional token to raise api.github.com rate limits
15
17
  *
16
18
  * Design: zero npm dependencies (node builtins only) so the published
17
19
  * package stays ~5KB. Platform binaries come from GitHub Releases:
@@ -58,10 +60,9 @@ const https = __importStar(require("node:https"));
58
60
  const os = __importStar(require("node:os"));
59
61
  const path = __importStar(require("node:path"));
60
62
  const node_child_process_1 = require("node:child_process");
61
- const LAUNCHER_VERSION = "2.0.0"; // npm package version
62
- const EDITOR_VERSION = "0.1.2"; // keep in sync with app/Cargo.toml (default download)
63
+ const LAUNCHER_VERSION = "2.0.1"; // npm package version
64
+ const FALLBACK_EDITOR_VERSION = "0.1.3"; // used when offline / API fails; keep near app/Cargo.toml
63
65
  const REPO = process.env.EZI_REPO || "olovalabs/ezicode";
64
- const TAG = `v${process.env.EZI_VERSION || EDITOR_VERSION}`;
65
66
  function resolveTarget() {
66
67
  const platform = process.platform;
67
68
  const arch = process.arch;
@@ -81,15 +82,141 @@ function resolveTarget() {
81
82
  `Prebuilt binaries exist for linux-x64, darwin-arm64, win32-x64 only. ` +
82
83
  `Build from source: https://github.com/${REPO}`);
83
84
  }
84
- function cacheDir() {
85
+ function baseCacheDir() {
85
86
  if (process.env.EZI_CACHE)
86
87
  return process.env.EZI_CACHE;
87
88
  if (process.platform === "win32") {
88
89
  const base = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
89
- return path.join(base, "ezi", "cache", TAG);
90
+ return path.join(base, "ezi", "cache");
90
91
  }
91
92
  const xdg = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
92
- return path.join(xdg, "ezi", TAG);
93
+ return path.join(xdg, "ezi");
94
+ }
95
+ function cacheDirFor(tag) {
96
+ return path.join(baseCacheDir(), tag);
97
+ }
98
+ function latestCacheFile() {
99
+ return path.join(baseCacheDir(), "latest.json");
100
+ }
101
+ function normalizeTag(raw) {
102
+ const t = raw.trim();
103
+ return t.startsWith("v") ? t : `v${t}`;
104
+ }
105
+ function fetchLatestTag(verbose) {
106
+ return new Promise((resolve) => {
107
+ const headers = {
108
+ "User-Agent": "ezi-launcher",
109
+ Accept: "application/vnd.github+json",
110
+ };
111
+ if (process.env.EZI_GITHUB_TOKEN) {
112
+ headers.Authorization = `Bearer ${process.env.EZI_GITHUB_TOKEN}`;
113
+ }
114
+ const req = https.get(`https://api.github.com/repos/${REPO}/releases/latest`, { headers, timeout: 5000 }, (res) => {
115
+ const status = res.statusCode || 0;
116
+ if (status !== 200) {
117
+ if (verbose)
118
+ console.log(`ezi: release lookup failed (http ${status}), using fallback`);
119
+ res.resume();
120
+ resolve(null);
121
+ return;
122
+ }
123
+ let body = "";
124
+ res.setEncoding("utf8");
125
+ res.on("data", (chunk) => {
126
+ body += chunk;
127
+ // Guard against unexpectedly large payloads.
128
+ if (body.length > 256 * 1024) {
129
+ res.destroy();
130
+ resolve(null);
131
+ }
132
+ });
133
+ res.on("end", () => {
134
+ try {
135
+ const json = JSON.parse(body);
136
+ if (typeof json.tag_name === "string" && json.tag_name.trim()) {
137
+ resolve(normalizeTag(json.tag_name));
138
+ }
139
+ else {
140
+ resolve(null);
141
+ }
142
+ }
143
+ catch {
144
+ resolve(null);
145
+ }
146
+ });
147
+ });
148
+ req.on("timeout", () => {
149
+ if (verbose)
150
+ console.log("ezi: release lookup timed out, using fallback");
151
+ req.destroy();
152
+ resolve(null);
153
+ });
154
+ req.on("error", (err) => {
155
+ if (verbose)
156
+ console.log(`ezi: release lookup error (${err.message}), using fallback`);
157
+ resolve(null);
158
+ });
159
+ });
160
+ }
161
+ function readLastSeenTag(verbose) {
162
+ try {
163
+ const raw = fs.readFileSync(latestCacheFile(), "utf8");
164
+ const cached = JSON.parse(raw);
165
+ if (typeof cached.tag !== "string" || !cached.tag)
166
+ return null;
167
+ if (verbose)
168
+ console.log(`ezi: last seen ${cached.tag}`);
169
+ return cached.tag;
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ function writeCachedTag(tag) {
176
+ try {
177
+ fs.mkdirSync(baseCacheDir(), { recursive: true });
178
+ fs.writeFileSync(latestCacheFile(), JSON.stringify({ tag, checkedAt: Date.now() }), "utf8");
179
+ }
180
+ catch {
181
+ // Cache is best-effort; a read-only homedir must not break launches.
182
+ }
183
+ }
184
+ /**
185
+ * Resolve which editor release to run:
186
+ * EZI_VERSION=<x.y.z|vX.Y.Z> -> that pin, no network.
187
+ * EZI_VERSION=latest (or unset) -> check GitHub every run; same tag reuses
188
+ * cached binary (no download), new tag downloads once, offline reuses last seen.
189
+ * EZI_NO_UPDATE_CHECK=1 -> skip network, use last seen or fallback.
190
+ */
191
+ async function resolveTag(verbose) {
192
+ const pinned = (process.env.EZI_VERSION || "").trim();
193
+ if (pinned && pinned.toLowerCase() !== "latest") {
194
+ return normalizeTag(pinned);
195
+ }
196
+ if (process.env.EZI_NO_UPDATE_CHECK === "1") {
197
+ const last = readLastSeenTag(verbose);
198
+ if (last)
199
+ return last;
200
+ if (verbose)
201
+ console.log("ezi: update check disabled, using fallback");
202
+ return `v${FALLBACK_EDITOR_VERSION}`;
203
+ }
204
+ // Always check: cheap API call (~200ms). Same version -> cached binary, no download.
205
+ const latest = await fetchLatestTag(verbose);
206
+ if (latest) {
207
+ const prev = readLastSeenTag(false);
208
+ if (prev !== latest && verbose)
209
+ console.log(`ezi: new release ${latest} (was ${prev || "none"})`);
210
+ else if (verbose)
211
+ console.log(`ezi: latest is ${latest}, reusing cache`);
212
+ writeCachedTag(latest);
213
+ return latest;
214
+ }
215
+ // Offline/API failure: reuse last seen tag so repeat launches keep working.
216
+ const stale = readLastSeenTag(verbose);
217
+ if (stale)
218
+ return stale;
219
+ return `v${FALLBACK_EDITOR_VERSION}`;
93
220
  }
94
221
  function download(url, dest, redirects = 5) {
95
222
  return new Promise((resolve, reject) => {
@@ -132,15 +259,15 @@ function extractTargz(archive, destDir) {
132
259
  });
133
260
  });
134
261
  }
135
- async function ensureBinary(verbose) {
262
+ async function ensureBinary(tag, verbose) {
136
263
  const target = resolveTarget();
137
- const dir = cacheDir();
264
+ const dir = cacheDirFor(tag);
138
265
  fs.mkdirSync(dir, { recursive: true });
139
266
  if (target.kind === "exe") {
140
267
  const bin = path.join(dir, "ezicode.exe");
141
268
  if (fs.existsSync(bin))
142
269
  return bin;
143
- const url = `https://github.com/${REPO}/releases/download/${TAG}/${target.asset}`;
270
+ const url = `https://github.com/${REPO}/releases/download/${tag}/${target.asset}`;
144
271
  const tmp = bin + ".download";
145
272
  if (verbose)
146
273
  console.log(`ezi: downloading ${url}`);
@@ -153,7 +280,7 @@ async function ensureBinary(verbose) {
153
280
  fs.chmodSync(bin, 0o755);
154
281
  return bin;
155
282
  }
156
- const url = `https://github.com/${REPO}/releases/download/${TAG}/${target.asset}`;
283
+ const url = `https://github.com/${REPO}/releases/download/${tag}/${target.asset}`;
157
284
  const archive = path.join(dir, target.asset);
158
285
  if (!fs.existsSync(archive)) {
159
286
  if (verbose)
@@ -168,7 +295,7 @@ async function ensureBinary(verbose) {
168
295
  fs.chmodSync(bin, 0o755);
169
296
  return bin;
170
297
  }
171
- function printHelp() {
298
+ function printHelp(fallbackTag) {
172
299
  console.log(`ezi ${LAUNCHER_VERSION} — launcher for ezicode (https://github.com/${REPO})
173
300
 
174
301
  Usage:
@@ -179,9 +306,10 @@ Usage:
179
306
  ezi --clean remove cached binaries
180
307
 
181
308
  Env:
182
- EZI_VERSION release version (default ${EDITOR_VERSION})
309
+ EZI_VERSION pin release (e.g. 0.1.3), "latest", or unset for auto-detect (default ${fallbackTag})
183
310
  EZI_REPO owner/repo (default ${REPO})
184
311
  EZI_CACHE cache dir override
312
+ EZI_NO_UPDATE_CHECK=1 skip GitHub lookup, use cache/fallback
185
313
  `);
186
314
  }
187
315
  async function main() {
@@ -189,19 +317,20 @@ async function main() {
189
317
  const verbose = args.includes("--verbose") || !!process.env.EZI_VERBOSE;
190
318
  const passthrough = args.filter((a) => a !== "--verbose");
191
319
  if (passthrough.includes("--help") || passthrough.includes("-h")) {
192
- printHelp();
320
+ printHelp(`v${FALLBACK_EDITOR_VERSION}`);
193
321
  return;
194
322
  }
323
+ const tag = await resolveTag(verbose);
195
324
  if (passthrough.includes("--version") || passthrough.includes("-V")) {
196
- console.log(`ezi ${LAUNCHER_VERSION} (editor ${TAG} @ ${REPO})`);
325
+ console.log(`ezi ${LAUNCHER_VERSION} (editor ${tag} @ ${REPO})`);
197
326
  return;
198
327
  }
199
328
  if (passthrough.includes("--clean")) {
200
- fs.rmSync(cacheDir(), { recursive: true, force: true });
329
+ fs.rmSync(baseCacheDir(), { recursive: true, force: true });
201
330
  console.log("ezi: cache cleared");
202
331
  return;
203
332
  }
204
- const bin = await ensureBinary(verbose);
333
+ const bin = await ensureBinary(tag, verbose);
205
334
  if (passthrough.includes("--where")) {
206
335
  console.log(bin);
207
336
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezi",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Launch ezicode editor with npx — zero-install native binary launcher",
5
5
  "license": "MIT",
6
6
  "repository": {