@contextual-io/cli 0.6.0 → 0.7.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.
- package/README.md +1 -1
- package/dist/hooks/init/index.d.ts +3 -0
- package/dist/hooks/init/index.js +38 -0
- package/dist/utils/message.d.ts +14 -0
- package/dist/utils/message.js +31 -0
- package/dist/utils/update-check.d.ts +23 -0
- package/dist/utils/update-check.js +101 -0
- package/oclif.manifest.json +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { boxMessage } from "../../utils/message.js";
|
|
3
|
+
import { checkForUpdate, hasNotifiedVersion, markChecked } from "../../utils/update-check.js";
|
|
4
|
+
const defaultCheckFrequencyMs = 6 * 60 * 60 * 1000;
|
|
5
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
6
|
+
const getUpdateMessage = ({ binName, currentVersion, latestVersion, packageName }) => boxMessage([
|
|
7
|
+
`A newer version of ${chalk.blueBright(chalk.bold(binName))} is available: ${chalk.bold(currentVersion)} -> ${chalk.bold(latestVersion)}`,
|
|
8
|
+
`Install ${chalk.greenBright(chalk.bold(packageName + "@latest"))} with your preferred package manager.`,
|
|
9
|
+
].join("\n")) + "\n\n";
|
|
10
|
+
const hook = async function ({ config }) {
|
|
11
|
+
if (config.scopedEnvVarTrue("DISABLE_UPDATE_CHECK"))
|
|
12
|
+
return;
|
|
13
|
+
const updateCheckDays = Number.parseInt(config.scopedEnvVar("UPDATE_CHECK_DAYS") ?? "0", 10);
|
|
14
|
+
if (!Number.isSafeInteger(updateCheckDays) || updateCheckDays < 0)
|
|
15
|
+
return;
|
|
16
|
+
const checkFrequencyMs = updateCheckDays > 0 ? updateCheckDays * dayMs : defaultCheckFrequencyMs;
|
|
17
|
+
const result = await checkForUpdate({
|
|
18
|
+
currentVersion: config.version,
|
|
19
|
+
packageName: config.pjson.name,
|
|
20
|
+
timeoutMs: checkFrequencyMs,
|
|
21
|
+
});
|
|
22
|
+
if (!result) {
|
|
23
|
+
markChecked({ cacheDir: config.cacheDir });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (result.updateAvailable && !hasNotifiedVersion({ cacheDir: config.cacheDir, latestVersion: result.latestVersion })) {
|
|
27
|
+
process.stderr.write(getUpdateMessage({
|
|
28
|
+
binName: config.bin,
|
|
29
|
+
currentVersion: result.currentVersion,
|
|
30
|
+
latestVersion: result.latestVersion,
|
|
31
|
+
packageName: config.pjson.name,
|
|
32
|
+
}));
|
|
33
|
+
markChecked({ cacheDir: config.cacheDir, notifiedVersion: result.latestVersion });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
markChecked({ cacheDir: config.cacheDir });
|
|
37
|
+
};
|
|
38
|
+
export default hook;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type BoxOptions = {
|
|
2
|
+
borderColorFn?: (text: string) => string;
|
|
3
|
+
bottomLeftBorderChar?: string;
|
|
4
|
+
bottomRightBorderChar?: string;
|
|
5
|
+
textColorFn?: (text: string) => string;
|
|
6
|
+
topLeftBorderChar?: string;
|
|
7
|
+
topRightBorderChar?: string;
|
|
8
|
+
xBorderChar?: string;
|
|
9
|
+
xPadding?: number;
|
|
10
|
+
yBorderChar?: string;
|
|
11
|
+
yPadding?: number;
|
|
12
|
+
};
|
|
13
|
+
export declare const boxMessage: (message: string, { borderColorFn, textColorFn, xPadding, yPadding, }?: BoxOptions) => string;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
|
+
const BoxTL = "╭";
|
|
4
|
+
const BoxTR = "╮";
|
|
5
|
+
const BoxBL = "╰";
|
|
6
|
+
const BoxBR = "╯";
|
|
7
|
+
const BoxX = "─";
|
|
8
|
+
const BoxY = "│";
|
|
9
|
+
const getVisibleLength = (value) => stripVTControlCharacters(value).length;
|
|
10
|
+
export const boxMessage = (message, { borderColorFn = (text) => chalk.yellow(text), textColorFn = (text) => chalk.white(text), xPadding = 2, yPadding = 0, } = {}) => {
|
|
11
|
+
xPadding = Math.max(0, xPadding);
|
|
12
|
+
yPadding = Math.max(0, yPadding);
|
|
13
|
+
const lines = message.split("\n");
|
|
14
|
+
const maxLength = Math.max(...lines.map(line => getVisibleLength(line)));
|
|
15
|
+
const totalInnerWidth = maxLength + xPadding * 2;
|
|
16
|
+
const leftPadding = " ".repeat(xPadding);
|
|
17
|
+
const topBorder = borderColorFn(`${BoxTL}${BoxX.repeat(totalInnerWidth)}${BoxTR}`);
|
|
18
|
+
const bottomBorder = borderColorFn(`${BoxBL}${BoxX.repeat(totalInnerWidth)}${BoxBR}`);
|
|
19
|
+
const yPaddingLines = Array.from({ length: yPadding }, () => `${textColorFn(BoxY)}${" ".repeat(totalInnerWidth)}${textColorFn(BoxY)}`);
|
|
20
|
+
const contentLines = lines.map(line => {
|
|
21
|
+
const rightPadding = " ".repeat(maxLength - getVisibleLength(line) + xPadding);
|
|
22
|
+
return `${borderColorFn(BoxY)}${leftPadding}${textColorFn(line)}${rightPadding}${borderColorFn(BoxY)}`;
|
|
23
|
+
});
|
|
24
|
+
return [
|
|
25
|
+
topBorder,
|
|
26
|
+
...yPaddingLines,
|
|
27
|
+
...contentLines,
|
|
28
|
+
...yPaddingLines,
|
|
29
|
+
bottomBorder,
|
|
30
|
+
].join("\n");
|
|
31
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type UpdateCheckResult = {
|
|
2
|
+
currentVersion: string;
|
|
3
|
+
latestVersion: string;
|
|
4
|
+
updateAvailable: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare const compareVersions: (left: string, right: string) => -1 | 0 | 1;
|
|
7
|
+
export declare const checkForUpdate: ({ currentVersion, packageName, timeoutMs, }: {
|
|
8
|
+
currentVersion: string;
|
|
9
|
+
packageName: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
}) => Promise<undefined | UpdateCheckResult>;
|
|
12
|
+
export declare const shouldRunPeriodicCheck: ({ cacheDir, frequencyMs, }: {
|
|
13
|
+
cacheDir: string;
|
|
14
|
+
frequencyMs: number;
|
|
15
|
+
}) => boolean;
|
|
16
|
+
export declare const markChecked: ({ cacheDir, notifiedVersion, }: {
|
|
17
|
+
cacheDir: string;
|
|
18
|
+
notifiedVersion?: string;
|
|
19
|
+
}) => void;
|
|
20
|
+
export declare const hasNotifiedVersion: ({ cacheDir, latestVersion, }: {
|
|
21
|
+
cacheDir: string;
|
|
22
|
+
latestVersion: string;
|
|
23
|
+
}) => boolean;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fetch from "cross-fetch";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const NpmLatestVersionResponse = z.object({
|
|
6
|
+
version: z.string(),
|
|
7
|
+
});
|
|
8
|
+
const updateCheckFileName = "update-check.json";
|
|
9
|
+
const parseVersionPart = (value) => {
|
|
10
|
+
const parsed = Number.parseInt(value, 10);
|
|
11
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
12
|
+
};
|
|
13
|
+
const splitVersion = (version) => {
|
|
14
|
+
const trimmed = version.startsWith("v") ? version.slice(1) : version;
|
|
15
|
+
const [corePart, prerelease] = trimmed.split("-", 2);
|
|
16
|
+
const core = corePart.split(".").map((part) => parseVersionPart(part));
|
|
17
|
+
return { core, prerelease };
|
|
18
|
+
};
|
|
19
|
+
export const compareVersions = (left, right) => {
|
|
20
|
+
const leftVersion = splitVersion(left);
|
|
21
|
+
const rightVersion = splitVersion(right);
|
|
22
|
+
const maxLength = Math.max(leftVersion.core.length, rightVersion.core.length);
|
|
23
|
+
for (let i = 0; i < maxLength; i += 1) {
|
|
24
|
+
const leftPart = leftVersion.core[i] ?? 0;
|
|
25
|
+
const rightPart = rightVersion.core[i] ?? 0;
|
|
26
|
+
if (leftPart > rightPart)
|
|
27
|
+
return 1;
|
|
28
|
+
if (leftPart < rightPart)
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
if (!leftVersion.prerelease && rightVersion.prerelease)
|
|
32
|
+
return 1;
|
|
33
|
+
if (leftVersion.prerelease && !rightVersion.prerelease)
|
|
34
|
+
return -1;
|
|
35
|
+
return 0;
|
|
36
|
+
};
|
|
37
|
+
const readState = (cacheDir) => {
|
|
38
|
+
const statePath = path.join(cacheDir, updateCheckFileName);
|
|
39
|
+
if (!fs.existsSync(statePath))
|
|
40
|
+
return {};
|
|
41
|
+
try {
|
|
42
|
+
const rawState = fs.readFileSync(statePath, "utf8");
|
|
43
|
+
return JSON.parse(rawState);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const writeState = (cacheDir, state) => {
|
|
50
|
+
if (!fs.existsSync(cacheDir)) {
|
|
51
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
const statePath = path.join(cacheDir, updateCheckFileName);
|
|
54
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
55
|
+
};
|
|
56
|
+
const fetchLatestVersion = async (packageName, timeoutMs) => {
|
|
57
|
+
try {
|
|
58
|
+
const encodedPackageName = encodeURIComponent(packageName);
|
|
59
|
+
const response = await fetch(`https://registry.npmjs.org/${encodedPackageName}/latest`, {
|
|
60
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok)
|
|
63
|
+
return undefined;
|
|
64
|
+
const { version } = NpmLatestVersionResponse.parse(await response.json());
|
|
65
|
+
return version;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
export const checkForUpdate = async ({ currentVersion, packageName, timeoutMs = 1500, }) => {
|
|
72
|
+
const latestVersion = await fetchLatestVersion(packageName, timeoutMs);
|
|
73
|
+
if (!latestVersion)
|
|
74
|
+
return undefined;
|
|
75
|
+
return {
|
|
76
|
+
currentVersion,
|
|
77
|
+
latestVersion,
|
|
78
|
+
updateAvailable: compareVersions(latestVersion, currentVersion) > 0,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export const shouldRunPeriodicCheck = ({ cacheDir, frequencyMs, }) => {
|
|
82
|
+
const state = readState(cacheDir);
|
|
83
|
+
if (!state.lastCheckedAt)
|
|
84
|
+
return true;
|
|
85
|
+
const lastCheckedAt = new Date(state.lastCheckedAt).getTime();
|
|
86
|
+
if (Number.isNaN(lastCheckedAt))
|
|
87
|
+
return true;
|
|
88
|
+
return Date.now() - lastCheckedAt >= frequencyMs;
|
|
89
|
+
};
|
|
90
|
+
export const markChecked = ({ cacheDir, notifiedVersion, }) => {
|
|
91
|
+
const state = readState(cacheDir);
|
|
92
|
+
writeState(cacheDir, {
|
|
93
|
+
...state,
|
|
94
|
+
lastCheckedAt: new Date().toISOString(),
|
|
95
|
+
lastNotifiedVersion: notifiedVersion ?? state.lastNotifiedVersion,
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
export const hasNotifiedVersion = ({ cacheDir, latestVersion, }) => {
|
|
99
|
+
const state = readState(cacheDir);
|
|
100
|
+
return state.lastNotifiedVersion === latestVersion;
|
|
101
|
+
};
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contextual-io/cli",
|
|
3
3
|
"description": "Contextual CLI",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.1",
|
|
5
5
|
"author": "Nasser Oloumi",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ctxl": "./bin/run.js"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"@oclif/plugin-autocomplete": "^3.2.39",
|
|
12
12
|
"@oclif/plugin-help": "^6",
|
|
13
13
|
"@oclif/plugin-not-found": "^3.2.73",
|
|
14
|
-
"@oclif/table": "^0.5.
|
|
14
|
+
"@oclif/table": "^0.5.2",
|
|
15
15
|
"chalk": "^5.6.2",
|
|
16
16
|
"cross-fetch": "^4.1.0",
|
|
17
17
|
"open": "^11.0.0",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"eslint-config-prettier": "^10",
|
|
34
34
|
"eslint-plugin-unused-imports": "^4.3.0",
|
|
35
35
|
"mocha": "^10",
|
|
36
|
-
"oclif": "^4",
|
|
36
|
+
"oclif": "^4.22.81",
|
|
37
37
|
"shx": "^0.3.3",
|
|
38
38
|
"ts-node": "^10",
|
|
39
39
|
"typescript": "^5"
|
|
@@ -57,6 +57,9 @@
|
|
|
57
57
|
"bin": "ctxl",
|
|
58
58
|
"dirname": "ctxl",
|
|
59
59
|
"commands": "./dist/commands",
|
|
60
|
+
"hooks": {
|
|
61
|
+
"init": "./dist/hooks/init/index.js"
|
|
62
|
+
},
|
|
60
63
|
"plugins": [
|
|
61
64
|
"@oclif/plugin-help",
|
|
62
65
|
"@oclif/plugin-not-found",
|