@faable/faable 1.12.0 → 1.13.0

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.
@@ -0,0 +1,37 @@
1
+ import ora from 'ora';
2
+ import { version } from '../../config.js';
3
+ import { isDevBuild, getLatestVersion, isNewerVersion, CLI_PACKAGE } from '../../lib/UpdateChecker.js';
4
+ import { cmd } from '../../lib/cmd.js';
5
+ import { log } from '../../log.js';
6
+
7
+ const upgrade = {
8
+ command: "upgrade",
9
+ describe: "Upgrade the Faable CLI to the latest version",
10
+ handler: async () => {
11
+ if (isDevBuild(version)) {
12
+ log.warn("Development build, skipping self-upgrade");
13
+ return;
14
+ }
15
+ const latest = await getLatestVersion();
16
+ if (!latest) {
17
+ log.error("❌ Could not reach the npm registry to check for updates");
18
+ process.exit(1);
19
+ }
20
+ if (!isNewerVersion(latest, version)) {
21
+ log.info(`✅ Already on the latest version (${version})`);
22
+ return;
23
+ }
24
+ const spinner = ora(`Upgrading ${CLI_PACKAGE} ${version} → ${latest}`).start();
25
+ try {
26
+ await cmd(`npm install -g ${CLI_PACKAGE}@latest`, { timeout: 180_000 });
27
+ spinner.succeed(`Upgraded to ${latest}`);
28
+ }
29
+ catch {
30
+ spinner.fail("Upgrade failed");
31
+ log.error(`❌ Could not upgrade automatically. Try manually: npm install -g ${CLI_PACKAGE}@latest`);
32
+ process.exit(1);
33
+ }
34
+ },
35
+ };
36
+
37
+ export { upgrade };
package/dist/index.js CHANGED
@@ -4,15 +4,21 @@ import { deploy } from './commands/deploy/index.js';
4
4
  import { link_deprecated } from './commands/link/index.js';
5
5
  import { login } from './commands/login/index.js';
6
6
  import { logout } from './commands/logout/index.js';
7
+ import { upgrade } from './commands/upgrade/index.js';
7
8
  import { whoami } from './commands/whoami/index.js';
8
9
  import { version } from './config.js';
9
10
  import { Configuration } from './lib/Configuration.js';
11
+ import { notifyIfUpdateAvailable } from './lib/UpdateChecker.js';
10
12
  import { log } from './log.js';
11
13
 
12
14
  const yg = yargs();
13
15
  yg.scriptName('faable')
14
- .middleware(function (_argv) {
16
+ .middleware(async function (argv) {
15
17
  log.info(`Faable CLI ${version}`);
18
+ // `upgrade` does its own (forced) check
19
+ if (argv._[0] !== 'upgrade') {
20
+ await notifyIfUpdateAvailable(version);
21
+ }
16
22
  }, true)
17
23
  .option('c', {
18
24
  alias: 'config',
@@ -33,6 +39,7 @@ yg.scriptName('faable')
33
39
  .command(login)
34
40
  .command(logout)
35
41
  .command(whoami)
42
+ .command(upgrade)
36
43
  .command(link_deprecated)
37
44
  .demandCommand(1)
38
45
  .help()
@@ -0,0 +1,111 @@
1
+ import { spawn } from 'child_process';
2
+ import fs from 'fs-extra';
3
+ import os from 'os';
4
+ import path__default from 'path';
5
+ import { log } from '../log.js';
6
+
7
+ const CLI_PACKAGE = "@faable/faable";
8
+ const REGISTRY_URL = `https://registry.npmjs.org/${CLI_PACKAGE}/latest`;
9
+ // Hitting the registry at most once a day keeps the background refresh rare.
10
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
11
+ const UPGRADE_FETCH_TIMEOUT_MS = 10_000;
12
+ const cache_path = () => path__default.join(os.homedir(), ".faable", "update-check.json");
13
+ // Local dev runs with the semantic-release placeholder version.
14
+ const isDevBuild = (version) => version.startsWith("0.0.0");
15
+ const isCacheStale = (cache, now) => {
16
+ if (!cache.last_check)
17
+ return true;
18
+ const last = new Date(cache.last_check).getTime();
19
+ return Number.isNaN(last) || now - last > CHECK_INTERVAL_MS;
20
+ };
21
+ const isNewerVersion = (latest, current) => {
22
+ const parse = (v) => {
23
+ const [core, prerelease] = v.replace(/^v/, "").split("-");
24
+ const [major = 0, minor = 0, patch = 0] = core.split(".").map(Number);
25
+ return { major, minor, patch, prerelease };
26
+ };
27
+ const a = parse(latest);
28
+ const b = parse(current);
29
+ if (a.major !== b.major)
30
+ return a.major > b.major;
31
+ if (a.minor !== b.minor)
32
+ return a.minor > b.minor;
33
+ if (a.patch !== b.patch)
34
+ return a.patch > b.patch;
35
+ // Same core version: the release wins over its own prereleases.
36
+ return !a.prerelease && !!b.prerelease;
37
+ };
38
+ const readCache = async () => {
39
+ try {
40
+ return await fs.readJSON(cache_path());
41
+ }
42
+ catch {
43
+ return {};
44
+ }
45
+ };
46
+ const writeCache = async (cache) => {
47
+ try {
48
+ await fs.ensureDir(path__default.dirname(cache_path()));
49
+ await fs.writeJSON(cache_path(), cache, { spaces: 2 });
50
+ }
51
+ catch {
52
+ // A read-only home dir shouldn't break the CLI.
53
+ }
54
+ };
55
+ // Refresh the cache from a detached child so the current run never waits on
56
+ // the network (update-notifier pattern): the notice shows on the next run.
57
+ const spawnBackgroundRefresh = () => {
58
+ const script = `fetch(${JSON.stringify(REGISTRY_URL)})
59
+ .then((r) => r.json())
60
+ .then((d) => require("fs").writeFileSync(${JSON.stringify(cache_path())},
61
+ JSON.stringify({ last_check: new Date().toISOString(), latest: d.version }, null, 2)))
62
+ .catch(() => {})`;
63
+ try {
64
+ spawn(process.execPath, ["-e", script], {
65
+ detached: true,
66
+ stdio: "ignore",
67
+ windowsHide: true,
68
+ }).unref();
69
+ }
70
+ catch {
71
+ // No update check is worth failing a command over.
72
+ }
73
+ };
74
+ /**
75
+ * Fetch the latest published version, updating the cache. Only used by
76
+ * `faable upgrade`, where the user explicitly asked and waiting is expected.
77
+ */
78
+ const getLatestVersion = async () => {
79
+ try {
80
+ const res = await fetch(REGISTRY_URL, {
81
+ signal: AbortSignal.timeout(UPGRADE_FETCH_TIMEOUT_MS),
82
+ });
83
+ if (!res.ok)
84
+ return;
85
+ const data = (await res.json());
86
+ if (data.version) {
87
+ await writeCache({ last_check: new Date().toISOString(), latest: data.version });
88
+ }
89
+ return data.version;
90
+ }
91
+ catch {
92
+ return;
93
+ }
94
+ };
95
+ const notifyIfUpdateAvailable = async (current) => {
96
+ if (isDevBuild(current) || process.env.CI || process.env.GITHUB_ACTIONS) {
97
+ return;
98
+ }
99
+ const cache = await readCache();
100
+ if (cache.latest && isNewerVersion(cache.latest, current)) {
101
+ log.warn(`⬆️ Update available: ${current} → ${cache.latest}. Run \`faable upgrade\` to get the latest version.`);
102
+ }
103
+ if (isCacheStale(cache, Date.now())) {
104
+ // Stamp the attempt first so a failing child can't cause a spawn storm:
105
+ // whatever happens, the next refresh is a day away.
106
+ await writeCache({ ...cache, last_check: new Date().toISOString() });
107
+ spawnBackgroundRefresh();
108
+ }
109
+ };
110
+
111
+ export { CLI_PACKAGE, getLatestVersion, isCacheStale, isDevBuild, isNewerVersion, notifyIfUpdateAvailable };
@@ -1,12 +1,23 @@
1
- import { cmd } from './cmd.js';
1
+ import { spawn } from 'promisify-child-process';
2
2
  import { log } from '../log.js';
3
3
 
4
4
  // Returns the "org/repo" slug of the git origin remote (the format the API
5
5
  // stores in `app.repository`), the raw URL for non-GitHub remotes, or
6
6
  // undefined when there is no usable remote.
7
+ //
8
+ // This is a best-effort auto-detection: running outside a git repository (or
9
+ // without an `origin` remote) is an expected, benign case, so failures are
10
+ // swallowed at debug level — never surfaced as errors/warnings. It does NOT
11
+ // go through `cmd()` on purpose: that helper loudly logs stderr and the exit
12
+ // code, which is right for user-invoked build steps but pure noise here.
7
13
  const getGitRemoteUrl = async (workdir) => {
8
14
  try {
9
- const { stdout } = await cmd('git remote get-url origin', { cwd: workdir });
15
+ const child = spawn('git', ['remote', 'get-url', 'origin'], {
16
+ encoding: 'utf8',
17
+ stdio: 'pipe',
18
+ cwd: workdir
19
+ });
20
+ const { stdout } = await child;
10
21
  const url = stdout?.toString().trim();
11
22
  if (!url)
12
23
  return undefined;
@@ -18,7 +29,7 @@ const getGitRemoteUrl = async (workdir) => {
18
29
  return url;
19
30
  }
20
31
  catch {
21
- log.warn('Could not detect git remote origin URL.');
32
+ log.debug('No git origin remote detected; skipping repo-based app lookup.');
22
33
  return undefined;
23
34
  }
24
35
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",