Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ deno init --npm veryfront
The default starter is `ai-agent`. Choose another template directly:

```bash
npx veryfront init <PROJECT_NAME> --template <TEMPLATE>
npx veryfront@latest init <PROJECT_NAME> --template <TEMPLATE>
```

Available starters: `ai-agent`, `minimal`, `agentic-workflow`.
Expand Down
1 change: 1 addition & 0 deletions cli/commands/init/init.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ describe("init command integration", () => {
assertEquals(result.stdout?.includes("Deploy:"), true);
assertEquals(result.stdout?.includes("Project structure"), false);
assertEquals(result.stdout?.includes("npm run deploy"), true);
assertEquals(result.stdout?.includes("npx veryfront@latest deploy"), false);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
assertEquals(result.stdout?.includes("npx veryfront deploy"), false);
assertEquals(result.stdout?.includes("Project files created"), false);
assertEquals(result.stdout?.includes("Dependencies installed"), false);
Expand Down
2 changes: 1 addition & 1 deletion cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* @example
* ```sh
* npx veryfront dev
* npx veryfront@latest dev
* ```
*/

Expand Down
124 changes: 123 additions & 1 deletion cli/shared/update-check.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { compareVersions, shouldSkip } from "./update-check.ts";
import {
checkForUpdates,
compareVersions,
getUpdateInstallCommand,
shouldSkip,
UPDATE_INSTALL_COMMAND,
UPDATE_REGISTRY_URL,
} from "./update-check.ts";
import { setJsonMode } from "./json-output.ts";
import { setQuietMode } from "../utils/index.ts";

Expand Down Expand Up @@ -115,4 +122,119 @@ describe("update-check", () => {
}
});
});

describe("registry lookup", () => {
it("selects update commands that preserve the active install method", () => {
assertEquals(
getUpdateInstallCommand({
standalone: false,
executablePath: "prefix/homebrew/bin/node",
}),
"npm install -g veryfront@latest",
);
assertEquals(
getUpdateInstallCommand({
standalone: true,
executablePath: "prefix/homebrew/lib/node_modules/veryfront/bin/veryfront",
}),
"npm install -g veryfront@latest",
);
assertEquals(
getUpdateInstallCommand({
standalone: true,
executablePath: "prefix/homebrew/bin/veryfront",
}),
"brew upgrade veryfront/tap/veryfront",
);
assertEquals(
getUpdateInstallCommand({
standalone: true,
executablePath: "prefix/.veryfront/bin/veryfront",
}),
"curl -fsSL https://veryfront.com/install.sh | sh",
);
});

it("reads and caches the npm latest response", async () => {
const writes: Array<{ path: string; data: string }> = [];
const notices: Array<{ current: string; latest: string }> = [];

await checkForUpdates("1.2.3", {
shouldSkip: () => false,
cacheLocation: {
directory: "cache/veryfront",
file: "cache/veryfront/update-check.json",
},
fileSystem: {
readTextFile: () => Promise.reject(new Error("missing")),
mkdir: () => Promise.resolve(),
writeTextFile: (path, data) => {
writes.push({ path, data });
return Promise.resolve();
},
},
fetcher: (input) => {
assertEquals(String(input), "https://registry.npmjs.org/veryfront/latest");
return Promise.resolve(Response.json({ version: "1.2.4" }));
},
now: () => 123,
printNotice: (current, latest) => notices.push({ current, latest }),
});

assertEquals(UPDATE_REGISTRY_URL, "https://registry.npmjs.org/veryfront/latest");
assertEquals(UPDATE_INSTALL_COMMAND, "npm install -g veryfront@latest");
assertEquals(writes, [{
path: "cache/veryfront/update-check.json",
data: JSON.stringify({ lastCheck: 123, latestVersion: "1.2.4" }),
}]);
assertEquals(notices, [{ current: "1.2.3", latest: "1.2.4" }]);
});

it("prints a valid update notice when cache persistence fails", async () => {
const diagnostics: string[] = [];
const notices: Array<{ current: string; latest: string }> = [];
Comment thread
coderabbitai[bot] marked this conversation as resolved.

await checkForUpdates("1.2.3", {
shouldSkip: () => false,
cacheLocation: {
directory: "cache/veryfront",
file: "cache/veryfront/update-check.json",
},
fileSystem: {
readTextFile: () => Promise.reject(new Error("missing")),
mkdir: () => Promise.resolve(),
writeTextFile: () => Promise.reject(new Error("read-only cache")),
},
fetcher: () => Promise.resolve(Response.json({ version: "1.2.4" })),
printNotice: (current, latest) => notices.push({ current, latest }),
debug: (message) => diagnostics.push(message),
});

assertEquals(notices, [{ current: "1.2.3", latest: "1.2.4" }]);
assertEquals(diagnostics, ["Veryfront could not cache the update check."]);
});

it("reports a broken registry endpoint in verbose diagnostics", async () => {
const diagnostics: string[] = [];

await checkForUpdates("1.2.3", {
shouldSkip: () => false,
cacheLocation: {
directory: "cache/veryfront",
file: "cache/veryfront/update-check.json",
},
fileSystem: {
readTextFile: () => Promise.reject(new Error("missing")),
mkdir: () => Promise.resolve(),
writeTextFile: () => Promise.resolve(),
},
fetcher: () => Promise.resolve(new Response(null, { status: 404 })),
debug: (message) => diagnostics.push(message),
});

assertEquals(diagnostics, [
"Veryfront could not check for updates: npm registry returned 404.",
]);
});
});
});
148 changes: 112 additions & 36 deletions cli/shared/update-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,54 @@
*/

import { getEnv } from "veryfront/platform";
import { createFileSystem } from "veryfront/platform";
import { createFileSystem, type FileSystem } from "veryfront/platform";
import { join } from "veryfront/platform/path";
import { getEnvironmentConfig } from "veryfront/config";
import { isJsonMode } from "./json-output.ts";
import { brand, dim, warning as warningColor } from "../ui/colors.ts";
import { isQuiet } from "../utils/index.ts";
import { cliLogger, isQuiet } from "../utils/index.ts";
import { detectCI } from "./interactive.ts";

const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
const REGISTRY_URL = "https://jsr.io/@veryfront/veryfront/meta.json";
const INSTALL_CMD = "deno install -gArf jsr:@veryfront/veryfront";
export const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/veryfront/latest";
export const UPDATE_INSTALL_COMMAND = "npm install -g veryfront@latest";
const HOMEBREW_UPDATE_COMMAND = "brew upgrade veryfront/tap/veryfront";
const INSTALL_SCRIPT_COMMAND = "curl -fsSL https://veryfront.com/install.sh | sh";
const STABLE_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;

interface UpdateCache {
lastCheck: number;
latestVersion: string | null;
}

function getCacheFile(): string | null {
interface UpdateCacheLocation {
directory: string;
file: string;
}

type UpdateCheckFileSystem = Pick<FileSystem, "readTextFile" | "mkdir" | "writeTextFile">;
type UpdateCheckFetcher = (
input: string | URL | Request,
init?: RequestInit,
) => Promise<Response>;

export interface UpdateCheckOptions {
shouldSkip?: () => boolean;
cacheLocation?: UpdateCacheLocation | null;
fileSystem?: UpdateCheckFileSystem;
fetcher?: UpdateCheckFetcher;
now?: () => number;
printNotice?: (current: string, latest: string) => void;
debug?: (message: string) => void;
}

class UpdateCheckFailure extends Error {}

function getCacheLocation(): UpdateCacheLocation | null {
const env = getEnvironmentConfig();
if (!env.homeDir) return null;
const cacheDir = join(env.homeDir, ".cache", "veryfront");
return join(cacheDir, "update-check.json");
const directory = join(env.homeDir, ".cache", "veryfront");
return { directory, file: join(directory, "update-check.json") };
}

export function compareVersions(current: string, latest: string): boolean {
Expand All @@ -50,62 +76,112 @@ export function shouldSkip(): boolean {
return false;
}

export function getUpdateInstallCommand(
context: { standalone: boolean; executablePath: string } = {
standalone: Deno.build.standalone ?? false,
executablePath: Deno.execPath(),
},
): string {
if (!context.standalone) return UPDATE_INSTALL_COMMAND;

const executablePath = context.executablePath.replaceAll("\\", "/").toLowerCase();
if (executablePath.includes("/node_modules/veryfront/bin/")) {
return UPDATE_INSTALL_COMMAND;
}

if (
executablePath.includes("/cellar/") ||
executablePath.includes("/homebrew/") ||
executablePath.includes("/linuxbrew/")
) {
return HOMEBREW_UPDATE_COMMAND;
}

if (!executablePath.endsWith(".exe")) return INSTALL_SCRIPT_COMMAND;
Comment thread
kojiwakayama marked this conversation as resolved.
return UPDATE_INSTALL_COMMAND;
}

function printUpdateNotice(current: string, latest: string): void {
console.error();
console.error(` ${warningColor("!")} Update available: ${current} → ${latest}`);
console.error(` ${dim("Run:")} ${brand(INSTALL_CMD)}`);
console.error(` ${dim("Run:")} ${brand(getUpdateInstallCommand())}`);
console.error();
}

async function fetchLatestVersion(fetcher: UpdateCheckFetcher): Promise<string> {
const response = await fetcher(UPDATE_REGISTRY_URL);
if (!response.ok) {
await response.body?.cancel();
throw new UpdateCheckFailure(
`Veryfront could not check for updates: npm registry returned ${response.status}.`,
);
}

const data: unknown = await response.json();
const latestVersion = data && typeof data === "object" && "version" in data
? (data as { version?: unknown }).version
: undefined;
if (typeof latestVersion !== "string" || !STABLE_SEMVER_PATTERN.test(latestVersion)) {
throw new UpdateCheckFailure(
"Veryfront could not check for updates: npm registry returned an invalid version.",
);
}

return latestVersion;
}

export async function checkForUpdates(
currentVersion: string,
options: UpdateCheckOptions = {},
): Promise<void> {
if (shouldSkip()) return;
if ((options.shouldSkip ?? shouldSkip)()) return;

const cacheFile = getCacheFile();
if (!cacheFile) return;
const cacheLocation = options.cacheLocation === undefined
? getCacheLocation()
: options.cacheLocation;
if (!cacheLocation) return;

const fs = createFileSystem();
const fs = options.fileSystem ?? createFileSystem();
const now = options.now ?? Date.now;
const notice = options.printNotice ?? printUpdateNotice;
const debug = options.debug ?? ((message: string) => cliLogger.debug(message));

try {
const raw = await fs.readTextFile(cacheFile);
const raw = await fs.readTextFile(cacheLocation.file);
const cache: UpdateCache = JSON.parse(raw);
if (Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
if (now() - cache.lastCheck < CHECK_INTERVAL_MS) {
if (
cache.latestVersion &&
compareVersions(currentVersion, cache.latestVersion)
) {
printUpdateNotice(currentVersion, cache.latestVersion);
notice(currentVersion, cache.latestVersion);
}
return;
}
} catch {
// No cache — proceed
// No cache. Continue with the registry check.
}

try {
const resp = await fetch(REGISTRY_URL);
if (!resp.ok) {
await resp.body?.cancel();
return;
const latestVersion = await fetchLatestVersion(options.fetcher ?? fetch);
if (compareVersions(currentVersion, latestVersion)) {
notice(currentVersion, latestVersion);
}
const data = await resp.json();
const latestVersion = data.latest as string | undefined;
if (!latestVersion) return;

const env = getEnvironmentConfig();
if (!env.homeDir) return;
const cacheDir = join(env.homeDir, ".cache", "veryfront");
await fs.mkdir(cacheDir, { recursive: true });
await fs.writeTextFile(
cacheFile,
JSON.stringify({ lastCheck: Date.now(), latestVersion }),
);

if (compareVersions(currentVersion, latestVersion)) {
printUpdateNotice(currentVersion, latestVersion);
try {
await fs.mkdir(cacheLocation.directory, { recursive: true });
await fs.writeTextFile(
cacheLocation.file,
JSON.stringify({ lastCheck: now(), latestVersion }),
);
} catch {
debug("Veryfront could not cache the update check.");
}
} catch {
// Network error — silently ignore
} catch (error) {
debug(
error instanceof UpdateCheckFailure
? error.message
: "Veryfront could not complete the update check.",
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
2 changes: 1 addition & 1 deletion cli/templates/files/docs-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A chatbot that answers questions from your own documents using Retrieval-Augment
2. Start the dev server:

```bash
npx veryfront dev
npx veryfront@latest dev
```

3. Index the sample docs in `content/`:
Expand Down
2 changes: 1 addition & 1 deletion cli/templates/integrations/figma/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ FIGMA_CLIENT_SECRET=your_figma_client_secret
### 3. Install the Integration

```bash
npx veryfront add figma
npx veryfront@latest add figma
```

## File Structure
Expand Down
Loading