facult 2.13.0 → 2.13.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 (2) hide show
  1. package/bin/fclt.cjs +103 -56
  2. package/package.json +1 -1
package/bin/fclt.cjs CHANGED
@@ -15,6 +15,9 @@ const REPO_NAME = "fclt";
15
15
  const PACKAGE_NAME = "facult";
16
16
  const DOWNLOAD_RETRIES = 12;
17
17
  const DOWNLOAD_RETRY_DELAY_MS = 5000;
18
+ const ACTIVE_RUNTIME_WAIT_MS = 10_000;
19
+ const ACTIVE_RUNTIME_WAIT_INTERVAL_MS = 100;
20
+ const STALE_RUNTIME_TEMP_MS = 10 * 60 * 1000;
18
21
 
19
22
  function isHelpLikeArgs(args) {
20
23
  return (
@@ -80,70 +83,75 @@ async function main() {
80
83
  let installedBinaryThisRun = false;
81
84
 
82
85
  if (!(await fileExists(binaryPath))) {
83
- const packageManager = detectPackageManager();
84
- const hasSourceFallback = await canUseSourceFallback(sourceEntry);
85
- const incompleteCache = await hasIncompleteRuntimeCache({
86
+ await removeStaleRuntimeTemps({
86
87
  installDir,
87
88
  binaryName,
89
+ maxAgeMs: STALE_RUNTIME_TEMP_MS,
88
90
  });
91
+ const packageManager = detectPackageManager();
92
+ const hasSourceFallback = await canUseSourceFallback(sourceEntry);
89
93
 
90
- if (incompleteCache) {
91
- await removeIncompleteRuntimeTemps({ installDir, binaryName });
92
- }
93
-
94
- if (hasSourceFallback && (incompleteCache || isHelpLikeArgs(args))) {
94
+ if (hasSourceFallback && isHelpLikeArgs(args)) {
95
95
  return runSourceFallback({
96
96
  sourceEntry,
97
97
  version,
98
98
  packageManager,
99
- reason: new Error(
100
- incompleteCache
101
- ? "incomplete cached runtime download"
102
- : "runtime binary missing for help-like command"
103
- ),
99
+ reason: new Error("runtime binary missing for help-like command"),
104
100
  });
105
101
  }
106
102
 
107
- const tag = `v${version}`;
108
- const assetName = `${PACKAGE_NAME}-${version}-${resolved.platform}-${resolved.arch}${resolved.ext}`;
109
- const url = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${tag}/${assetName}`;
110
- const tmpPath = `${binaryPath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
111
-
112
- try {
113
- await fsp.mkdir(installDir, { recursive: true });
114
- await downloadWithRetry(url, tmpPath, {
115
- attempts: DOWNLOAD_RETRIES,
116
- delayMs: DOWNLOAD_RETRY_DELAY_MS,
103
+ if (await hasIncompleteRuntimeCache({ installDir, binaryName })) {
104
+ await waitForFile(binaryPath, {
105
+ timeoutMs: ACTIVE_RUNTIME_WAIT_MS,
106
+ intervalMs: ACTIVE_RUNTIME_WAIT_INTERVAL_MS,
117
107
  });
118
- if (resolved.platform !== "windows") {
119
- await fsp.chmod(tmpPath, 0o755);
120
- }
121
- await fsp.rename(tmpPath, binaryPath);
122
- installedBinaryThisRun = true;
123
- } catch (error) {
124
- await safeUnlink(tmpPath);
125
- if (await canUseSourceFallback(sourceEntry)) {
126
- return runSourceFallback({
127
- sourceEntry,
128
- version,
129
- packageManager: detectPackageManager(),
130
- reason: error,
108
+ }
109
+
110
+ if (await fileExists(binaryPath)) {
111
+ // Another concurrent launcher finished the runtime install while this
112
+ // process was waiting.
113
+ } else {
114
+ const tag = `v${version}`;
115
+ const assetName = `${PACKAGE_NAME}-${version}-${resolved.platform}-${resolved.arch}${resolved.ext}`;
116
+ const url = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${tag}/${assetName}`;
117
+ const tmpPath = `${binaryPath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
118
+
119
+ try {
120
+ await fsp.mkdir(installDir, { recursive: true });
121
+ await downloadWithRetry(url, tmpPath, {
122
+ attempts: DOWNLOAD_RETRIES,
123
+ delayMs: DOWNLOAD_RETRY_DELAY_MS,
131
124
  });
125
+ if (resolved.platform !== "windows") {
126
+ await fsp.chmod(tmpPath, 0o755);
127
+ }
128
+ await installDownloadedRuntime(tmpPath, binaryPath);
129
+ installedBinaryThisRun = true;
130
+ } catch (error) {
131
+ await safeUnlink(tmpPath);
132
+ if (await canUseSourceFallback(sourceEntry)) {
133
+ return runSourceFallback({
134
+ sourceEntry,
135
+ version,
136
+ packageManager: detectPackageManager(),
137
+ reason: error,
138
+ });
139
+ }
140
+ const message =
141
+ error instanceof Error ? error.message : String(error ?? "");
142
+ console.error(
143
+ [
144
+ "Unable to download the fclt binary for this platform.",
145
+ `Expected asset: ${assetName}`,
146
+ `URL: ${url}`,
147
+ `Reason: ${message}`,
148
+ "",
149
+ "Try installing directly from releases:",
150
+ "https://github.com/hack-dance/fclt/releases",
151
+ ].join("\n")
152
+ );
153
+ process.exit(1);
132
154
  }
133
- const message =
134
- error instanceof Error ? error.message : String(error ?? "");
135
- console.error(
136
- [
137
- "Unable to download the fclt binary for this platform.",
138
- `Expected asset: ${assetName}`,
139
- `URL: ${url}`,
140
- `Reason: ${message}`,
141
- "",
142
- "Try installing directly from releases:",
143
- "https://github.com/hack-dance/fclt/releases",
144
- ].join("\n")
145
- );
146
- process.exit(1);
147
155
  }
148
156
  }
149
157
 
@@ -367,19 +375,58 @@ async function hasIncompleteRuntimeCache({ installDir, binaryName }) {
367
375
  }
368
376
  }
369
377
 
370
- async function removeIncompleteRuntimeTemps({ installDir, binaryName }) {
378
+ async function removeStaleRuntimeTemps({ installDir, binaryName, maxAgeMs }) {
371
379
  try {
372
380
  const entries = await fsp.readdir(installDir);
373
- await Promise.all(
374
- entries
375
- .filter((entry) => entry.startsWith(`${binaryName}.tmp-`))
376
- .map((entry) => safeUnlink(path.join(installDir, entry)))
377
- );
381
+ const now = Date.now();
382
+ const stalePaths = [];
383
+ for (const entry of entries) {
384
+ if (!entry.startsWith(`${binaryName}.tmp-`)) {
385
+ continue;
386
+ }
387
+ const candidate = path.join(installDir, entry);
388
+ try {
389
+ const stats = await fsp.stat(candidate);
390
+ if (now - stats.mtimeMs > maxAgeMs) {
391
+ stalePaths.push(candidate);
392
+ }
393
+ } catch {
394
+ // Ignore temp files that disappeared while scanning.
395
+ }
396
+ }
397
+ await Promise.all(stalePaths.map((candidate) => safeUnlink(candidate)));
378
398
  } catch {
379
399
  // Ignore missing runtime dirs while cleaning stale temp files.
380
400
  }
381
401
  }
382
402
 
403
+ async function waitForFile(filePath, { timeoutMs, intervalMs }) {
404
+ const deadline = Date.now() + timeoutMs;
405
+ while (Date.now() < deadline) {
406
+ if (await fileExists(filePath)) {
407
+ return true;
408
+ }
409
+ await sleep(intervalMs);
410
+ }
411
+ return await fileExists(filePath);
412
+ }
413
+
414
+ async function installDownloadedRuntime(tmpPath, binaryPath) {
415
+ if (await fileExists(binaryPath)) {
416
+ await safeUnlink(tmpPath);
417
+ return;
418
+ }
419
+ try {
420
+ await fsp.rename(tmpPath, binaryPath);
421
+ } catch (error) {
422
+ if (await fileExists(binaryPath)) {
423
+ await safeUnlink(tmpPath);
424
+ return;
425
+ }
426
+ throw error;
427
+ }
428
+ }
429
+
383
430
  async function safeUnlink(filePath) {
384
431
  try {
385
432
  await fsp.unlink(filePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facult",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "description": "Manage canonical AI capabilities, sync surfaces, and evolution state.",
5
5
  "type": "module",
6
6
  "license": "MIT",