@calo-design/cli 0.4.2 → 0.4.4
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/bin/cli.js +195 -4
- package/bin/login.js +18 -2
- package/bin/share.js +11 -2
- package/bin/templates/gallery-assets/component-gallery/meal-photo.png +0 -0
- package/bin/templates/gallery-assets/component-gallery/smoothie-photo.png +0 -0
- package/bin/templates/gallery-assets/live-flows/active-subscriber-home/ingredient-penne.png +0 -0
- package/bin/templates/gallery-index.tsx +1678 -0
- package/bin/templates/gallery-suppress.ts +50 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ const os = require("node:os");
|
|
|
17
17
|
const path = require("node:path");
|
|
18
18
|
const { cmdPush } = require("./mirror-push");
|
|
19
19
|
const { cmdShare } = require("./share");
|
|
20
|
-
const { cmdLogin, cmdLogout, ensureLoggedIn, githubToken } = require("./login");
|
|
20
|
+
const { cmdLogin, cmdLogout, ensureLoggedIn, githubToken, reportEvent, ensureSession, loadSession, BROKER } = require("./login");
|
|
21
21
|
|
|
22
22
|
const ORG = "Calo-Design";
|
|
23
23
|
const SKILL_REPO = `${ORG}/calo-design`;
|
|
@@ -141,11 +141,77 @@ function ensureBabelConfig(dir) {
|
|
|
141
141
|
fs.writeFileSync(p, 'module.exports = (api) => {\n api.cache(true);\n return { presets: ["babel-preset-expo"] };\n};\n');
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// The skill is re-installed fresh on every init, but the runtime used to freeze at
|
|
145
|
+
// first install — fresh guidance driving a stale library (the skill can reference
|
|
146
|
+
// primitives the installed @calo/design-system doesn't have yet). Detect when the
|
|
147
|
+
// installed @calo git deps are behind their repo HEAD so init can re-pin in place.
|
|
148
|
+
function installedCaloShas(rt) {
|
|
149
|
+
// npm records a git dep as "resolved": "git+https://…/<repo>.git#<40-hex-sha>"
|
|
150
|
+
const locks = [path.join(rt, "package-lock.json"), path.join(rt, "node_modules", ".package-lock.json")];
|
|
151
|
+
for (const lp of locks) {
|
|
152
|
+
try {
|
|
153
|
+
const lock = JSON.parse(fs.readFileSync(lp, "utf8"));
|
|
154
|
+
const out = {};
|
|
155
|
+
for (const spec of PKG_SPECS) {
|
|
156
|
+
const repo = spec.match(/\/([^/]+?)(?:\.git)?$/)[1];
|
|
157
|
+
for (const v of Object.values(lock.packages || {})) {
|
|
158
|
+
const m = v && v.resolved && String(v.resolved).match(new RegExp(`${repo}(?:\\.git)?#([0-9a-f]{40})`));
|
|
159
|
+
if (m) { out[repo] = m[1]; break; }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (Object.keys(out).length) return out;
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function remoteHeadSha(repoUrl, env) {
|
|
169
|
+
const r = spawnSync("git", ["ls-remote", repoUrl, "HEAD"], { env, encoding: "utf8" });
|
|
170
|
+
if (r.error || r.status !== 0 || !r.stdout) return null;
|
|
171
|
+
const m = r.stdout.match(/^([0-9a-f]{40})\s/);
|
|
172
|
+
return m ? m[1] : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function staleCaloDeps(rt, env) {
|
|
176
|
+
const installed = installedCaloShas(rt);
|
|
177
|
+
if (!installed) return []; // can't tell — don't churn a working runtime
|
|
178
|
+
const stale = [];
|
|
179
|
+
for (const spec of PKG_SPECS) {
|
|
180
|
+
const repo = spec.match(/\/([^/]+?)(?:\.git)?$/)[1];
|
|
181
|
+
const head = remoteHeadSha(spec.replace(/^git\+/, ""), env);
|
|
182
|
+
// Unknown local sha with a reachable remote also counts, so old runtimes self-heal.
|
|
183
|
+
if (head && head !== installed[repo]) stale.push(repo);
|
|
184
|
+
}
|
|
185
|
+
return stale;
|
|
186
|
+
}
|
|
187
|
+
|
|
144
188
|
async function ensureRuntime({ force } = {}) {
|
|
145
189
|
const rt = runtimeDir();
|
|
146
190
|
if (!force && runtimeExists()) {
|
|
147
191
|
ensureBabelConfig(rt); // backfill: runtimes built before this shipped no babel.config.js
|
|
148
|
-
|
|
192
|
+
let stale = [];
|
|
193
|
+
let env;
|
|
194
|
+
try {
|
|
195
|
+
env = gitTokenEnv(await githubToken());
|
|
196
|
+
stale = staleCaloDeps(rt, env);
|
|
197
|
+
} catch {} // offline / broker hiccup — keep the existing runtime, don't block init
|
|
198
|
+
if (!stale.length) {
|
|
199
|
+
ok(`shared runtime ready (${tilde(rt)})`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
log(c.b(`\n[runtime] ${stale.join(" + ")} behind latest — refreshing the shared runtime`));
|
|
203
|
+
try {
|
|
204
|
+
// Non-destructive: `npm install` of the git specs re-resolves HEAD and updates
|
|
205
|
+
// node_modules in place; a failure aborts cleanly and leaves the runtime working.
|
|
206
|
+
runWithRetry("npm", ["install", ...PKG_SPECS, "--legacy-peer-deps"], 3, { cwd: rt, env });
|
|
207
|
+
let manifest = { pkgSpecs: PKG_SPECS, peers: PEERS };
|
|
208
|
+
try { manifest = { ...JSON.parse(fs.readFileSync(runtimeManifestPath(), "utf8")), ...manifest }; } catch {}
|
|
209
|
+
manifest.refreshedAt = new Date().toISOString();
|
|
210
|
+
fs.writeFileSync(runtimeManifestPath(), JSON.stringify(manifest, null, 2) + "\n");
|
|
211
|
+
ok("shared runtime refreshed — every linked prototype now uses the latest Calo stack");
|
|
212
|
+
} catch (e) {
|
|
213
|
+
warn(`couldn't refresh the runtime (${e.message}) — continuing with the existing install; \`calo-design update\` retries this.`);
|
|
214
|
+
}
|
|
149
215
|
return;
|
|
150
216
|
}
|
|
151
217
|
const scaffolded = fs.existsSync(path.join(rt, "package.json"));
|
|
@@ -342,6 +408,44 @@ export default function Index() {
|
|
|
342
408
|
`);
|
|
343
409
|
}
|
|
344
410
|
|
|
411
|
+
// The component gallery's routes: a root layout (fonts + providers, no local imports so it's
|
|
412
|
+
// portable) and the @calo/design-system showroom (bundled with the CLI) as the index route.
|
|
413
|
+
// Always (re)written so a `calo-design update` + relaunch also ships the latest showroom.
|
|
414
|
+
function writeGalleryApp(projectRoot) {
|
|
415
|
+
const srcDir = path.join(projectRoot, "src");
|
|
416
|
+
const appDir = path.join(srcDir, "app");
|
|
417
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
418
|
+
// Silences known non-actionable RNW-on-React-19 dev warnings (nested Pressables, deprecated
|
|
419
|
+
// style props) so the embedded gallery has a clean overlay. Imported first in _layout.
|
|
420
|
+
fs.cpSync(path.join(__dirname, "templates", "gallery-suppress.ts"), path.join(srcDir, "suppress-dev-warnings.ts"));
|
|
421
|
+
fs.writeFileSync(path.join(appDir, "_layout.tsx"), `import "../suppress-dev-warnings";
|
|
422
|
+
import { Stack } from "expo-router";
|
|
423
|
+
import { useFonts } from "expo-font";
|
|
424
|
+
import { CaloBottomSheetProvider, CaloScreen, caloFonts, caloTokens } from "@calo/design-system";
|
|
425
|
+
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
426
|
+
|
|
427
|
+
export default function Layout() {
|
|
428
|
+
const [loaded] = useFonts(caloFonts);
|
|
429
|
+
return (
|
|
430
|
+
<SafeAreaProvider>
|
|
431
|
+
{loaded ? (
|
|
432
|
+
<CaloBottomSheetProvider>
|
|
433
|
+
<Stack screenOptions={{ headerShown: false, contentStyle: { backgroundColor: caloTokens.color.canvas } }} />
|
|
434
|
+
</CaloBottomSheetProvider>
|
|
435
|
+
) : (
|
|
436
|
+
<CaloScreen />
|
|
437
|
+
)}
|
|
438
|
+
</SafeAreaProvider>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
`);
|
|
442
|
+
const showroom = fs.readFileSync(path.join(__dirname, "templates", "gallery-index.tsx"), "utf8");
|
|
443
|
+
fs.writeFileSync(path.join(appDir, "index.tsx"), showroom);
|
|
444
|
+
// The showroom require()s a few local images (../assets/... from src/app) — ship them too.
|
|
445
|
+
const assetsSrc = path.join(__dirname, "templates", "gallery-assets");
|
|
446
|
+
if (fs.existsSync(assetsSrc)) fs.cpSync(assetsSrc, path.join(projectRoot, "src", "assets"), { recursive: true });
|
|
447
|
+
}
|
|
448
|
+
|
|
345
449
|
function writeGitignore(projectRoot) {
|
|
346
450
|
const gi = path.join(projectRoot, ".gitignore");
|
|
347
451
|
const needed = ["node_modules/", ".expo/", "dist/", "*.log"];
|
|
@@ -424,6 +528,8 @@ async function cmdInit() {
|
|
|
424
528
|
log(c.dim(`\n Prototype folder: ${tilde(dir)}`));
|
|
425
529
|
}
|
|
426
530
|
const cd = targetName;
|
|
531
|
+
// Slug used for the design-ops feed (The Pass) — the prototype's folder name.
|
|
532
|
+
const slug = targetName || path.basename(process.cwd());
|
|
427
533
|
|
|
428
534
|
// Machine setup runs every time, independent of this folder: the skill is installed
|
|
429
535
|
// above; ensure the shared runtime so @calo/design-system + @calo/flows live on this
|
|
@@ -441,12 +547,14 @@ async function cmdInit() {
|
|
|
441
547
|
// Re-running in an already-linked prototype: refresh the link if the runtime is ok.
|
|
442
548
|
if (runtimeReady) linkProject(process.cwd());
|
|
443
549
|
else warn("shared runtime unavailable — left the existing link untouched.");
|
|
550
|
+
await reportEvent("init", slug);
|
|
444
551
|
return nextSteps({ linked: true, cd });
|
|
445
552
|
}
|
|
446
553
|
if (isExistingProject()) {
|
|
447
554
|
warn("existing project detected — installing the Calo stack here (not linking), to avoid clobbering your node_modules.");
|
|
448
555
|
await installCaloDeps(process.cwd());
|
|
449
556
|
ok("packages + peers installed in this project");
|
|
557
|
+
await reportEvent("init", slug);
|
|
450
558
|
return nextSteps({ cd });
|
|
451
559
|
}
|
|
452
560
|
if (!dirIsScaffoldable()) {
|
|
@@ -462,6 +570,7 @@ async function cmdInit() {
|
|
|
462
570
|
}
|
|
463
571
|
|
|
464
572
|
// Fresh prototype folder → link to the shared runtime, with a per-project fallback.
|
|
573
|
+
await reportEvent("init", slug);
|
|
465
574
|
if (!runtimeReady) { await installHere(); return nextSteps({ cd }); }
|
|
466
575
|
try {
|
|
467
576
|
linkProject(process.cwd());
|
|
@@ -486,6 +595,80 @@ async function cmdUpdate() {
|
|
|
486
595
|
} else {
|
|
487
596
|
log(c.dim(" No shared runtime yet — run `calo-design init` in a prototype folder to create one."));
|
|
488
597
|
}
|
|
598
|
+
// Report the refresh to the design-ops feed, tagged with the DS version we floated to.
|
|
599
|
+
let dsVersion = "";
|
|
600
|
+
try {
|
|
601
|
+
dsVersion = require(path.join(runtimeDir(), "node_modules", "@calo", "design-system", "package.json")).version || "";
|
|
602
|
+
} catch { /* runtime not present — report without a version */ }
|
|
603
|
+
await reportEvent("update", dsVersion);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Print the logged-in identity. `--json` for machine callers (The Pass). The JSON is written
|
|
607
|
+
// on its own line (leading newline) so a chatty shell rc can't fuse output onto the JSON line;
|
|
608
|
+
// The Pass parses the last non-empty stdout line.
|
|
609
|
+
async function cmdWhoami() {
|
|
610
|
+
const s = loadSession();
|
|
611
|
+
if (!s || !s.email) {
|
|
612
|
+
if (has("--json")) { process.stdout.write("\n" + JSON.stringify({ email: null }) + "\n"); return; }
|
|
613
|
+
log("Not logged in — run `calo-design login`.");
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
if (has("--json")) { process.stdout.write("\n" + JSON.stringify({ email: s.email, expiresAt: s.expiresAt }) + "\n"); return; }
|
|
617
|
+
log(s.email);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// The design-ops feed (The Pass reads this via `--json`). Reuses the stored session —
|
|
621
|
+
// no second login. ensureSession refreshes a stale JWT silently using the refresh token.
|
|
622
|
+
async function cmdFeed() {
|
|
623
|
+
const session = await ensureSession();
|
|
624
|
+
const li = args.indexOf("--limit");
|
|
625
|
+
const limit = li >= 0 && /^\d+$/.test(args[li + 1] || "") ? Math.max(1, Math.min(500, parseInt(args[li + 1], 10))) : 100;
|
|
626
|
+
let res;
|
|
627
|
+
try {
|
|
628
|
+
// Bounded so a black-holed broker can't hang the CLI (and, in turn, The Pass shelling out here).
|
|
629
|
+
res = await fetch(`${BROKER}/v1/events?limit=${limit}`, {
|
|
630
|
+
headers: { authorization: `Bearer ${session}` },
|
|
631
|
+
signal: AbortSignal.timeout(15000),
|
|
632
|
+
});
|
|
633
|
+
} catch (e) {
|
|
634
|
+
throw new Error(`can't reach the Calo broker at ${BROKER} (${e.message})`);
|
|
635
|
+
}
|
|
636
|
+
const text = await res.text();
|
|
637
|
+
if (!res.ok) throw new Error(`feed failed (${res.status}): ${text.slice(0, 200)}`);
|
|
638
|
+
if (has("--json")) { process.stdout.write("\n" + text + "\n"); return; }
|
|
639
|
+
const data = JSON.parse(text);
|
|
640
|
+
for (const e of data.events) log(`${c.dim(e.ts)} ${e.email} ${c.b(e.action)} ${e.target || ""}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Serve the component gallery (the @calo/design-system showroom) locally, rendered LIVE from
|
|
644
|
+
// the shared runtime — so `calo-design update` keeps it current (no rebuild/redeploy). Design
|
|
645
|
+
// Kitchen auto-starts this; it also works standalone in a terminal. Runs Metro in the
|
|
646
|
+
// foreground so whoever launched it owns the process lifecycle.
|
|
647
|
+
async function cmdGallery() {
|
|
648
|
+
if (!runtimeExists()) {
|
|
649
|
+
throw new Error("The Calo runtime isn't set up on this machine yet — run `calo-design init` first, then retry.");
|
|
650
|
+
}
|
|
651
|
+
const dir = path.join(designchefHome(), "gallery");
|
|
652
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
653
|
+
// Materialize / refresh a thin project linked to the shared runtime (same machinery as
|
|
654
|
+
// a linked prototype), then swap in the showroom routes.
|
|
655
|
+
writeThinConfigs(dir);
|
|
656
|
+
ensureBabelConfig(dir);
|
|
657
|
+
writeMetroConfig(dir);
|
|
658
|
+
writeGitignore(dir);
|
|
659
|
+
linkNodeModules(dir);
|
|
660
|
+
writeGalleryApp(dir);
|
|
661
|
+
|
|
662
|
+
let port = "8091";
|
|
663
|
+
const pj = args.find((a) => a.startsWith("--port="));
|
|
664
|
+
if (pj) port = pj.slice(7);
|
|
665
|
+
else { const pi = args.indexOf("--port"); if (pi >= 0) port = args[pi + 1] || port; }
|
|
666
|
+
if (!/^\d+$/.test(port)) port = "8091";
|
|
667
|
+
|
|
668
|
+
log(c.b(`\n[gallery] Serving the Calo component gallery on http://localhost:${port}`));
|
|
669
|
+
log(c.dim(` Live from the shared runtime (${tilde(runtimeDir())}) — always the installed @calo/design-system.`));
|
|
670
|
+
log(c.dim(" First launch compiles the design system (~30–60s). Ctrl-C to stop."));
|
|
671
|
+
run("npx", ["expo", "start", "--web", "--port", port], { cwd: dir });
|
|
489
672
|
}
|
|
490
673
|
|
|
491
674
|
function help() {
|
|
@@ -503,6 +686,8 @@ function help() {
|
|
|
503
686
|
${c.dim(" push --slug x --title \"…\" --owner \"…\" --screenshot path --dry-run --direct")}
|
|
504
687
|
${c.dim("share")} publish THIS web prototype (React/Vite/CRA/Next) to Cloudflare Pages → share link
|
|
505
688
|
${c.dim(" share --slug x --dir dist --build --project calo-prototypes --dry-run")}
|
|
689
|
+
${c.dim("gallery")} serve the @calo/design-system component gallery locally (live from the runtime)
|
|
690
|
+
${c.dim(" gallery --port 8091")}
|
|
506
691
|
|
|
507
692
|
Login is required once before init; the session refreshes automatically.
|
|
508
693
|
init always installs the skill and ensures the shared ds/flows runtime on your machine;
|
|
@@ -513,16 +698,22 @@ function help() {
|
|
|
513
698
|
|
|
514
699
|
(async () => {
|
|
515
700
|
try {
|
|
701
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "version") { console.log(require("../package.json").version); return; }
|
|
516
702
|
if (cmd === "login") await cmdLogin(args.slice(1));
|
|
517
703
|
else if (cmd === "logout") cmdLogout();
|
|
518
704
|
else if (cmd === "init") await cmdInit();
|
|
519
705
|
else if (cmd === "update") await cmdUpdate();
|
|
520
706
|
else if (cmd === "push") await cmdPush(args.slice(1));
|
|
521
707
|
else if (cmd === "share") await cmdShare(args.slice(1));
|
|
708
|
+
else if (cmd === "feed") await cmdFeed();
|
|
709
|
+
else if (cmd === "whoami") await cmdWhoami();
|
|
710
|
+
else if (cmd === "gallery") await cmdGallery();
|
|
522
711
|
else help();
|
|
523
712
|
} catch (err) {
|
|
524
|
-
|
|
525
|
-
|
|
713
|
+
// Diagnostics go to stderr so stdout stays pure for machine callers (The Pass parses
|
|
714
|
+
// `feed --json` / `whoami --json` from stdout); a non-zero exit is the failure signal.
|
|
715
|
+
console.error(`\n\x1b[31m✗\x1b[0m ${err.message}`);
|
|
716
|
+
console.error(c.dim(" Stuck? Re-run `calo-design login`, then retry. Or check that the Calo broker is reachable."));
|
|
526
717
|
process.exit(1);
|
|
527
718
|
}
|
|
528
719
|
})();
|
package/bin/login.js
CHANGED
|
@@ -42,13 +42,16 @@ function clearSession() {
|
|
|
42
42
|
} catch {}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
async function api(pathname, body, token) {
|
|
45
|
+
async function api(pathname, body, token, { timeoutMs } = {}) {
|
|
46
46
|
let res;
|
|
47
47
|
try {
|
|
48
48
|
res = await fetch(BROKER + pathname, {
|
|
49
49
|
method: "POST",
|
|
50
50
|
headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
51
51
|
body: JSON.stringify(body || {}),
|
|
52
|
+
// Opt-in timeout. Existing callers keep their (unbounded) behavior; telemetry passes a
|
|
53
|
+
// tight bound so a black-holed broker (connect-but-silent) can never hang the command.
|
|
54
|
+
...(timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
|
|
52
55
|
});
|
|
53
56
|
} catch (e) {
|
|
54
57
|
throw new Error(`can't reach the Calo broker at ${BROKER} (${e.message})`);
|
|
@@ -190,4 +193,17 @@ async function publishToMirror(payload) {
|
|
|
190
193
|
return api("/v1/publish", payload, session);
|
|
191
194
|
}
|
|
192
195
|
|
|
193
|
-
|
|
196
|
+
// Best-effort design-ops telemetry: report an init/update to the broker feed (The Pass).
|
|
197
|
+
// Never throws and never blocks the command on failure — telemetry is not the job. Uses the
|
|
198
|
+
// existing session if it's still valid; won't trigger a refresh just to report an event.
|
|
199
|
+
async function reportEvent(action, target) {
|
|
200
|
+
try {
|
|
201
|
+
const s = loadSession();
|
|
202
|
+
if (!s || !s.session || expSec(s) - 60 <= nowSec()) return; // not logged in / expired — skip
|
|
203
|
+
await api("/v1/events", { action, target: target || "" }, s.session, { timeoutMs: 3000 });
|
|
204
|
+
} catch {
|
|
205
|
+
/* swallow — telemetry must never break a command */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { cmdLogin, cmdLogout, ensureSession, ensureLoggedIn, githubToken, easToken, publishWeb, publishToMirror, reportEvent, BROKER, loadSession };
|
package/bin/share.js
CHANGED
|
@@ -144,10 +144,19 @@ async function cmdShare(args = []) {
|
|
|
144
144
|
const sizeMb = (fs.statSync(tgz).size / 1e6).toFixed(1);
|
|
145
145
|
log(c.b(`\n[upload] ${sizeMb} MB → broker → wrangler pages deploy`));
|
|
146
146
|
const r = await publishWeb({ slug, project, tarball: fs.readFileSync(tgz) });
|
|
147
|
+
const url = r.url || aliasUrl(slug, project);
|
|
147
148
|
log("");
|
|
148
|
-
|
|
149
|
+
// The broker verifies the URL actually serves (verified:false => deployed but the link
|
|
150
|
+
// didn't answer). Don't print a confident "Live" for a link the broker couldn't reach.
|
|
151
|
+
if (r.verified === false) {
|
|
152
|
+
warn(`Deployed, but ${url} did not respond yet.`);
|
|
153
|
+
log(c.dim(" If it's still blank in ~30s, the broker likely deployed to a different"));
|
|
154
|
+
log(c.dim(" Cloudflare account/project than this URL implies — check broker CLOUDFLARE_* config."));
|
|
155
|
+
} else {
|
|
156
|
+
ok(`Live: ${c.g(url)}`);
|
|
157
|
+
log(c.dim(` Re-run \`calo-design share\` to update “${slug}” in place. Share the link above.`));
|
|
158
|
+
}
|
|
149
159
|
if (r.version) log(c.dim(` this version: ${r.version}`));
|
|
150
|
-
log(c.dim(` Re-run \`calo-design share\` to update “${slug}” in place. Share the link above.`));
|
|
151
160
|
} finally {
|
|
152
161
|
try { fs.rmSync(tgz); } catch {}
|
|
153
162
|
}
|
|
Binary file
|