Skip to content
Open
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
64 changes: 56 additions & 8 deletions daemon/fs/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@ import {
import { grep, type GrepResult } from "./grep.ts";

const inferMetadata = async (filepath: string): Promise<Metadata | null> => {
return { kind: "file" };
Copy link

@cubic-dev-ai cubic-dev-ai bot Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The early return makes the rest of inferMetadata unreachable, so block metadata is never inferred. Remove the unconditional return so the try/catch executes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At daemon/fs/api.ts, line 20:

<comment>The early return makes the rest of inferMetadata unreachable, so block metadata is never inferred. Remove the unconditional return so the try/catch executes.</comment>

<file context>
@@ -17,11 +17,18 @@ import {
 import { grep, type GrepResult } from "./grep.ts";
 
 const inferMetadata = async (filepath: string): Promise<Metadata | null> => {
+  return { kind: "file" };
+
   try {
</file context>
Fix with Cubic


Comment on lines 19 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

inferMetadata is currently short-circuited and no longer infers blocks.

Line 20 unconditionally returns { kind: "file" }, making the parsing/inference logic unreachable and breaking block-aware metadata propagation.

Proposed fix
 const inferMetadata = async (filepath: string): Promise<Metadata | null> => {
-  return { kind: "file" };
-
   try {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const inferMetadata = async (filepath: string): Promise<Metadata | null> => {
return { kind: "file" };
const inferMetadata = async (filepath: string): Promise<Metadata | null> => {
try {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@daemon/fs/api.ts` around lines 19 - 21, The function inferMetadata currently
short-circuits by immediately returning { kind: "file" }, skipping the real
inference logic; update inferMetadata to remove that unconditional return and
re-enable the file-parsing/block-detection path: read the file at filepath, run
the existing parsing/inference routines that detect blocks and produce Metadata
(preserving block-aware metadata propagation), handle I/O or parse errors
(return null on unrecoverable errors), and ensure the returned value conforms to
the Metadata type; use the inferMetadata function name to locate the stub and
wire it back into the calling flow.

try {
console.log("filepath", filepath)
const { __resolveType, name, path } = JSON.parse(
await Deno.readTextFile(filepath),
);
console.log("__resolveType", __resolveType)
console.log("name", name)
console.log("path", path)
const blockType = await inferBlockType(__resolveType);
console.log("blockType", blockType)

if (!blockType) {
return { kind: "file" };
Expand All @@ -43,6 +50,7 @@ const inferMetadata = async (filepath: string): Promise<Metadata | null> => {
__resolveType,
};
} catch (error) {
console.log("error", error)
if (error instanceof Deno.errors.NotFound) {
return null;
}
Expand Down Expand Up @@ -102,10 +110,25 @@ export interface GrepAPI {
response: GrepResult;
}

const shouldIgnore = (path: string) =>
basename(path) !== ".gitignore" &&
path.includes(`${SEPARATOR}.git`) ||
path.includes(`${SEPARATOR}node_modules${SEPARATOR}`);
const shouldIgnore = (path: string) => {
if (basename(path) === ".gitignore") {
return false;
}

// Check if path contains these directories anywhere in the path
const ignoredDirs = [
'.git',
'node_modules',
'.next',
'.faststore',
'dist',
'build',
'.turbo'
];

const pathSegments = path.split(SEPARATOR);
return ignoredDirs.some(dir => pathSegments.includes(dir));
};

const systemPathFromBrowser = (pathAndQuery: string) => {
const [url] = pathAndQuery.split("?");
Expand All @@ -120,37 +143,62 @@ const browserPathFromSystem = (filepath: string) =>

export async function* start(since: number): AsyncIterableIterator<FSEvent> {
try {
// Handle invalid since values (NaN, undefined, etc.)
const sinceTimestamp = Number.isFinite(since) ? since : 0;
console.log("[watchFS.start] Starting file walk, since:", since, "-> normalized:", sinceTimestamp);
const walker = walk(Deno.cwd(), { includeDirs: false, includeFiles: true });
let fileCount = 0;
let skippedCount = 0;
let processedCount = 0;

for await (const entry of walker) {
processedCount++;

if (shouldIgnore(entry.path)) {
skippedCount++;
continue;
}
console.log("entry", entry)

const [metadata, mtime] = await Promise.all([
inferMetadata(entry.path),
mtimeFor(entry.path),
]);
console.log("metadata", metadata);
console.log("mtime", mtime)
Comment on lines +161 to +168
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove or gate hot-loop debug logs in filesystem scan.

Lines 161/167/168 log per file and will heavily impact large repos; this should be behind VERBOSE (or debug level) only.

Proposed fix (verbose-gated logs)
-      console.log("entry", entry)
+      if (VERBOSE) console.log("[watchFS.start] entry", entry.path);

@@
-      console.log("metadata", metadata);
-      console.log("mtime", mtime)
+      if (VERBOSE) {
+        console.log("[watchFS.start] metadata/mtime", { path: entry.path, metadata, mtime });
+      }

@@
-    console.log(`[watchFS.start] File walk complete! Processed: ${processedCount}, Sent: ${fileCount}, Skipped: ${skippedCount}`);
-    console.log("[watchFS.start] Sending fs-snapshot event...");
+    if (VERBOSE) {
+      console.log(`[watchFS.start] File walk complete! Processed: ${processedCount}, Sent: ${fileCount}, Skipped: ${skippedCount}`);
+      console.log("[watchFS.start] Sending fs-snapshot event...");
+    }

@@
-    console.log("[watchFS.start] ✅ fs-snapshot event sent successfully");
+    if (VERBOSE) console.log("[watchFS.start] ✅ fs-snapshot event sent successfully");

Also applies to: 191-193, 199-201

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@daemon/fs/api.ts` around lines 161 - 168, The per-file console.log calls
(e.g., console.log("entry", entry) and the metadata/mtime logs produced around
the inferMetadata(entry.path) and mtimeFor(entry.path) calls) must be gated
behind a verbose/debug flag instead of being unconditionally emitted; update
these spots to either use a logger.debug(...) or wrap them in a conditional like
if (process.env.VERBOSE) { ... } (or the project's
logger.isDebug()/logger.debug) so the logs only run when verbose mode is
enabled; apply the same change to the other occurrences that log per-file
metadata/mtime as mentioned in the review.


if (
!metadata || mtime < since
) {
if (!metadata) {
continue;
}

if (mtime < sinceTimestamp) {
continue;
}

const filepath = browserPathFromSystem(entry.path);
fileCount++;

if (fileCount % 100 === 0) {
console.log(`[watchFS.start] Progress: ${fileCount} files sent, ${processedCount} processed, ${skippedCount} skipped`);
}

yield {
type: "fs-sync",
detail: { metadata, filepath, timestamp: mtime },
};
}

console.log(`[watchFS.start] File walk complete! Processed: ${processedCount}, Sent: ${fileCount}, Skipped: ${skippedCount}`);
console.log("[watchFS.start] Sending fs-snapshot event...");

yield {
type: "fs-snapshot",
detail: { timestamp: Date.now(), status: await git.status() },
};

console.log("[watchFS.start] ✅ fs-snapshot event sent successfully");
} catch (error) {
console.error(error);
console.error("[watchFS.start] ❌ Error during file walk:", error);
}
}

Expand Down
130 changes: 110 additions & 20 deletions daemon/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { activityMonitor, createIdleHandler } from "./monitor.ts";
import { register } from "./tunnel.ts";
import { createWorker, type WorkerOptions } from "./worker.ts";
import { portPool } from "./workers/portpool.ts";
import { exists } from "@std/fs";

const parsedArgs = parseArgs(Deno.args, {
string: ["build-cmd"],
Expand Down Expand Up @@ -118,6 +119,25 @@ globalThis.addEventListener("unhandledrejection", (e: {
console.log("unhandled rejection at:", e.promise, "reason:", e.reason);
});

type ProjectType = "faststore" | "fresh" | "hono";

const detectProjectType = async (): Promise<ProjectType> => {
try {
const packageJsonPath = join(Deno.cwd(), "package.json");
if (await exists(packageJsonPath)) {
const packageJson = JSON.parse(await Deno.readTextFile(packageJsonPath));
if (packageJson.dependencies?.["@faststore/cli"]) {
return "faststore";
}
// Could add more Node.js framework detection here (e.g., Hono)
}
return "fresh"; // Default to Fresh for Deno projects
Comment on lines +124 to +134
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Project-type detection is too narrow for FastStore installs.

Line 129 only checks dependencies["@faststore/cli"]. If FastStore CLI is in devDependencies (common), the project is misdetected as Fresh and the FastStore worker path is skipped.

Proposed fix
 const detectProjectType = async (): Promise<ProjectType> => {
   try {
     const packageJsonPath = join(Deno.cwd(), "package.json");
     if (await exists(packageJsonPath)) {
       const packageJson = JSON.parse(await Deno.readTextFile(packageJsonPath));
-      if (packageJson.dependencies?.["@faststore/cli"]) {
+      const deps = {
+        ...packageJson.dependencies,
+        ...packageJson.devDependencies,
+      };
+      if (deps?.["@faststore/cli"]) {
         return "faststore";
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const detectProjectType = async (): Promise<ProjectType> => {
try {
const packageJsonPath = join(Deno.cwd(), "package.json");
if (await exists(packageJsonPath)) {
const packageJson = JSON.parse(await Deno.readTextFile(packageJsonPath));
if (packageJson.dependencies?.["@faststore/cli"]) {
return "faststore";
}
// Could add more Node.js framework detection here (e.g., Hono)
}
return "fresh"; // Default to Fresh for Deno projects
const detectProjectType = async (): Promise<ProjectType> => {
try {
const packageJsonPath = join(Deno.cwd(), "package.json");
if (await exists(packageJsonPath)) {
const packageJson = JSON.parse(await Deno.readTextFile(packageJsonPath));
const deps = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
if (deps?.["@faststore/cli"]) {
return "faststore";
}
// Could add more Node.js framework detection here (e.g., Hono)
}
return "fresh"; // Default to Fresh for Deno projects
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@daemon/main.ts` around lines 124 - 134, detectProjectType currently only
inspects packageJson.dependencies for "@faststore/cli" so projects that list the
CLI in devDependencies or peerDependencies are misdetected; update the detection
in detectProjectType to read packageJson (using packageJsonPath) and check
packageJson.dependencies, packageJson.devDependencies and
packageJson.peerDependencies for "@faststore/cli" (and any equivalent FastStore
package names you consider necessary) and return "faststore" if found, otherwise
fall back to the existing "fresh" default.

} catch (error) {
console.log("Error detecting project type, defaulting to fresh:", error);
return "fresh";
}
};

const createBundler = (appName?: string) => {
const bundler = bundleApp(Deno.cwd());

Expand Down Expand Up @@ -221,7 +241,7 @@ const watch = async () => {
}
};

const createDeps = (): MiddlewareHandler => {
const createDeps = (projectType: ProjectType): MiddlewareHandler => {
let ok: Promise<unknown> | null | false = null;

const start = async () => {
Expand All @@ -234,33 +254,48 @@ const createDeps = (): MiddlewareHandler => {
}ms`,
});

start = performance.now();
await genManifestTS();
logs.push({
level: "info",
message: `${colors.bold("[step 2/4]")}: Manifest generation took ${
(performance.now() - start).toFixed(0)
}ms`,
});
// Only run Fresh-specific operations for Fresh projects
if (projectType === "fresh") {
start = performance.now();
await genManifestTS();
logs.push({
level: "info",
message: `${colors.bold("[step 2/4]")}: Manifest generation took ${
(performance.now() - start).toFixed(0)
}ms`,
});

start = performance.now();
await genBlocksJSON();
logs.push({
level: "info",
message: `${colors.bold("[step 3/4]")}: Blocks metadata generation took ${
(performance.now() - start).toFixed(0)
}ms`,
});

watch().catch(console.error);
watchMeta().catch(console.error);
} else {
await genManifestTS();
logs.push({
level: "info",
message: `${colors.bold("[step 2/4]")}: Skipped Fresh-specific operations (FastStore project)`,
});
}

start = performance.now();
await genBlocksJSON();
// watchFS is needed for all project types (sends events to admin)
watchFS().catch(console.error);
logs.push({
level: "info",
message: `${colors.bold("[step 3/4]")}: Blocks metadata generation took ${
(performance.now() - start).toFixed(0)
}ms`,
message: colors.green("File system watcher started"),
});

watch().catch(console.error);
watchMeta().catch(console.error);
watchFS().catch(console.error);

logs.push({
level: "info",
message: `${
colors.bold("[step 4/4]")
}: Started file watcher in background`,
}: Initialization complete`,
});
};

Expand All @@ -277,6 +312,9 @@ const createDeps = (): MiddlewareHandler => {
};
};

// Detect project type early for configuration
const PROJECT_TYPE = await detectProjectType();

const app = new Hono();

if (VERBOSE) {
Expand Down Expand Up @@ -306,7 +344,7 @@ app.get("/deco/_is_idle", createIdleHandler(DECO_SITE_NAME!, DECO_ENV_NAME!));
app.get("/deco/_liveness", () => new Response("OK", { status: 200 }));

// Globals are started after healthcheck to ensure k8s does not kill the pod before it is ready
app.use(createDeps());
app.use(createDeps(PROJECT_TYPE));
app.use(activityMonitor);
// These are the APIs that communicate with admin UI
app.use(createDaemonAPIs({ build: buildCmd, site: DECO_SITE_NAME }));
Expand Down Expand Up @@ -337,6 +375,58 @@ if (createRunCmd) {
};

app.route("", createWorker(createWorkerOptions));
} else {
// No explicit run command, create appropriate worker based on detected type
if (PROJECT_TYPE === "faststore") {
logs.push({
level: "info",
message: colors.green("Detected FastStore project, starting Node.js worker..."),
});

// FastStore/Next.js runs on port 3000 by default
const FASTSTORE_PORT = 3000;

const createFastStoreWorkerOptions = async (): Promise<WorkerOptions> => {
const packageJsonPath = join(Deno.cwd(), "package.json");
const packageJson = JSON.parse(await Deno.readTextFile(packageJsonPath));

// Detect package manager
const hasYarnLock = await exists(join(Deno.cwd(), "yarn.lock"));
const hasPnpmLock = await exists(join(Deno.cwd(), "pnpm-lock.yaml"));
const packageManager = hasPnpmLock ? "pnpm" : hasYarnLock ? "yarn" : "npm";

// Use volta-specified node version if available
const nodeVersion = packageJson.volta?.node;
if (nodeVersion) {
logs.push({
level: "info",
message: `Using Node.js ${nodeVersion} from Volta configuration`,
});
}

logs.push({
level: "info",
message: colors.blue(`FastStore worker will run on port ${FASTSTORE_PORT}`),
});

return {
command: new Deno.Command(packageManager, {
args: ["run", "dev"],
cwd: Deno.cwd(),
stdout: "piped",
stderr: "piped",
env: {
PORT: `${FASTSTORE_PORT}`,
NODE_ENV: "development",
},
}),
port: FASTSTORE_PORT,
persist,
};
};

app.route("", createWorker(createFastStoreWorkerOptions));
}
}

const LOCAL_STORAGE_ENV_NAME = "deco_host_env_name";
Expand Down
10 changes: 7 additions & 3 deletions daemon/sse/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ export const createSSE = () => {
app.get("/watch", (c) => {
const signal = c.req.raw.signal;
const done = Promise.withResolvers<void>();
const since = Number(c.req.query("since"));
const sinceParam = c.req.query("since");
const since = Number(sinceParam);
const normalizedSince = Number.isFinite(since) ? since : 0;

console.log("[SSE /watch] Query params:", { sinceParam, since, normalizedSince });

const enqueue = (
controller: ReadableStreamDefaultController<ServerSentEventMessage>,
Expand All @@ -41,7 +45,7 @@ export const createSSE = () => {
channel.removeEventListener("broadcast", handler);
});

for await (const event of startFS(since)) {
for await (const event of startFS(normalizedSince)) {
if (signal.aborted) {
return;
}
Expand All @@ -50,7 +54,7 @@ export const createSSE = () => {

enqueue(controller, startWorker());

startMeta(since)
startMeta(normalizedSince)
.then((meta) => meta && enqueue(controller, meta))
.catch(console.error);

Expand Down
Loading