From eb6272f1d8012c770a15681f05a7d814e7d2df6a Mon Sep 17 00:00:00 2001 From: "Irsyad A. Panjaitan" <44585532+irsyadadl@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:33:13 +0700 Subject: [PATCH 1/7] feat: implement diff command --- src/commands/diff.command.ts | 92 ++++++++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/src/commands/diff.command.ts b/src/commands/diff.command.ts index d9a5bc9..f699338 100644 --- a/src/commands/diff.command.ts +++ b/src/commands/diff.command.ts @@ -1,9 +1,95 @@ import { Command } from "@effect/cli" import { Console, Effect } from "effect" -import { componentNames, componentType } from "./add.command" +import { + HttpClient, + HttpClientRequest, + HttpClientResponse, +} from "@effect/platform" +import * as FS from "node:fs/promises" +import * as Path from "node:path" -export const diffCommand = Command.make("diff", { componentNames, componentType }, (config) => +const REMOTE_BASE = + "https://raw.githubusercontent.com/irsyadadl/intentui/2.x/components/ui" + +function simpleDiff(local: string, remote: string): string { + const l = local.split(/\r?\n/) + const r = remote.split(/\r?\n/) + const max = Math.max(l.length, r.length) + let out = "" + for (let i = 0; i < max; i++) { + const a = l[i] + const b = r[i] + if (a !== b) { + if (a !== undefined) out += `- ${a}\n` + if (b !== undefined) out += `+ ${b}\n` + } + } + return out +} + +async function listFiles(dir: string): Promise { + const entries = await FS.readdir(dir, { withFileTypes: true }) + const files: string[] = [] + for (const entry of entries) { + const res = Path.join(dir, entry.name) + if (entry.isDirectory()) { + files.push(...(await listFiles(res))) + } else { + files.push(res) + } + } + return files +} + +export const diffCommand = Command.make("diff", {}, () => Effect.gen(function* () { - yield* Console.log("Coming soon...") + const client = yield* HttpClient.HttpClient + const cwd = process.cwd() + let config + try { + const jsonStr = yield* Effect.tryPromise(() => FS.readFile("components.json", "utf8")) + config = JSON.parse(jsonStr) + } catch { + yield* Console.error("components.json not found or invalid") + return + } + + const alias: string = config.aliases?.ui ?? "components/ui" + const uiPath = Path.isAbsolute(alias.replace(/^@\//, "")) + ? alias.replace(/^@\//, "") + : Path.join(cwd, alias.replace(/^@\//, "")) + + let files: string[] = [] + try { + files = await listFiles(uiPath) + } catch { + yield* Console.error(`Unable to read local components at ${uiPath}`) + return + } + + const diffs: Array<{ file: string; diff: string }> = [] + for (const file of files) { + const relative = Path.relative(uiPath, file).replace(/\\/g, "/") + const remoteUrl = `${REMOTE_BASE}/${relative}` + const localContent = yield* Effect.tryPromise(() => FS.readFile(file, "utf8")) + const remoteContent = yield* HttpClientRequest.get(remoteUrl).pipe( + client.execute, + Effect.flatMap(HttpClientResponse.text), + Effect.catchAll(() => Effect.succeed("")), + ) + if (localContent !== remoteContent) { + diffs.push({ file: relative, diff: simpleDiff(localContent, remoteContent) }) + } + } + + if (diffs.length === 0) { + yield* Console.log("All components are up to date.") + } else { + for (const d of diffs) { + yield* Console.log(`--- ${d.file} ---`) + yield* Console.log(d.diff) + } + yield* Console.log("Some components differ from the registry. Run 'intentui add ' to sync.") + } }), ).pipe(Command.withDescription("Compares your local components with the registry versions.")) From da8b162654da3adb0330a7613ba36c3fb5ce3696 Mon Sep 17 00:00:00 2001 From: "Irsyad A. Panjaitan" <44585532+irsyadadl@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:03:24 +0700 Subject: [PATCH 2/7] fix: resolve diff command awaiting --- src/commands/diff.command.ts | 93 ++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/src/commands/diff.command.ts b/src/commands/diff.command.ts index d9a5bc9..a7743ef 100644 --- a/src/commands/diff.command.ts +++ b/src/commands/diff.command.ts @@ -1,9 +1,96 @@ import { Command } from "@effect/cli" import { Console, Effect } from "effect" -import { componentNames, componentType } from "./add.command" +import { + HttpClient, + HttpClientRequest, + HttpClientResponse, +} from "@effect/platform" +import * as FS from "node:fs/promises" +import * as Path from "node:path" -export const diffCommand = Command.make("diff", { componentNames, componentType }, (config) => +const REMOTE_BASE = + "https://raw.githubusercontent.com/irsyadadl/intentui/2.x/components/ui" + +function simpleDiff(local: string, remote: string): string { + const l = local.split(/\r?\n/) + const r = remote.split(/\r?\n/) + const max = Math.max(l.length, r.length) + let out = "" + for (let i = 0; i < max; i++) { + const a = l[i] + const b = r[i] + if (a !== b) { + if (a !== undefined) out += `- ${a}\n` + if (b !== undefined) out += `+ ${b}\n` + } + } + return out +} + +async function listFiles(dir: string): Promise { + const entries = await FS.readdir(dir, { withFileTypes: true }) + const files: string[] = [] + for (const entry of entries) { + const res = Path.join(dir, entry.name) + if (entry.isDirectory()) { + files.push(...(await listFiles(res))) + } else { + files.push(res) + } + } + return files +} + +export const diffCommand = Command.make("diff", {}, () => Effect.gen(function* () { - yield* Console.log("Coming soon...") + const client = yield* HttpClient.HttpClient + const cwd = process.cwd() + let config + try { + const jsonStr = yield* Effect.tryPromise(() => FS.readFile("components.json", "utf8")) + config = JSON.parse(jsonStr) + } catch { + yield* Console.error("components.json not found or invalid") + return + } + + const alias: string = config.aliases?.ui ?? "components/ui" + const uiPath = Path.isAbsolute(alias.replace(/^@\//, "")) + ? alias.replace(/^@\//, "") + : Path.join(cwd, alias.replace(/^@\//, "")) + + const filesResult = yield* Effect.tryPromise(() => listFiles(uiPath)).pipe( + Effect.catchAll(() => Effect.succeed([] as string[])), + ) + if (filesResult.length === 0) { + yield* Console.error(`Unable to read local components at ${uiPath}`) + return + } + const files = filesResult + + const diffs: Array<{ file: string; diff: string }> = [] + for (const file of files) { + const relative = Path.relative(uiPath, file).replace(/\\/g, "/") + const remoteUrl = `${REMOTE_BASE}/${relative}` + const localContent = yield* Effect.tryPromise(() => FS.readFile(file, "utf8")) + const remoteContent = yield* HttpClientRequest.get(remoteUrl).pipe( + client.execute, + Effect.flatMap(HttpClientResponse.text), + Effect.catchAll(() => Effect.succeed("")), + ) + if (localContent !== remoteContent) { + diffs.push({ file: relative, diff: simpleDiff(localContent, remoteContent) }) + } + } + + if (diffs.length === 0) { + yield* Console.log("All components are up to date.") + } else { + for (const d of diffs) { + yield* Console.log(`--- ${d.file} ---`) + yield* Console.log(d.diff) + } + yield* Console.log("Some components differ from the registry. Run 'intentui add ' to sync.") + } }), ).pipe(Command.withDescription("Compares your local components with the registry versions.")) From 8e46edc699a4ff8f7b6721a79afc058af8310a1a Mon Sep 17 00:00:00 2001 From: "Irsyad A. Panjaitan" Date: Thu, 5 Jun 2025 12:05:30 +0700 Subject: [PATCH 3/7] ff --- src/commands/diff.command.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/diff.command.ts b/src/commands/diff.command.ts index a7743ef..00b52d8 100644 --- a/src/commands/diff.command.ts +++ b/src/commands/diff.command.ts @@ -45,6 +45,7 @@ export const diffCommand = Command.make("diff", {}, () => Effect.gen(function* () { const client = yield* HttpClient.HttpClient const cwd = process.cwd() + // biome-ignore lint/suspicious/noImplicitAnyLet: let config try { const jsonStr = yield* Effect.tryPromise(() => FS.readFile("components.json", "utf8")) From eb2b7b41a2e35f88cc0ce49f3caa7fa2b4a64dc4 Mon Sep 17 00:00:00 2001 From: "Irsyad A. Panjaitan" Date: Thu, 5 Jun 2025 12:07:49 +0700 Subject: [PATCH 4/7] add config --- src/commands/diff.command.ts | 18 ++++++++---------- src/commands/init.command.ts | 5 ++--- src/commands/login.command.ts | 6 +----- src/lib/app.ts | 9 +++++++++ 4 files changed, 20 insertions(+), 18 deletions(-) create mode 100644 src/lib/app.ts diff --git a/src/commands/diff.command.ts b/src/commands/diff.command.ts index 00b52d8..368e0c7 100644 --- a/src/commands/diff.command.ts +++ b/src/commands/diff.command.ts @@ -1,15 +1,11 @@ -import { Command } from "@effect/cli" -import { Console, Effect } from "effect" -import { - HttpClient, - HttpClientRequest, - HttpClientResponse, -} from "@effect/platform" import * as FS from "node:fs/promises" import * as Path from "node:path" +import { Command } from "@effect/cli" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" +import { Console, Effect } from "effect" +import { app } from "~/lib/app" -const REMOTE_BASE = - "https://raw.githubusercontent.com/irsyadadl/intentui/2.x/components/ui" +const REMOTE_BASE = app.repo.ui function simpleDiff(local: string, remote: string): string { const l = local.split(/\r?\n/) @@ -91,7 +87,9 @@ export const diffCommand = Command.make("diff", {}, () => yield* Console.log(`--- ${d.file} ---`) yield* Console.log(d.diff) } - yield* Console.log("Some components differ from the registry. Run 'intentui add ' to sync.") + yield* Console.log( + "Some components differ from the registry. Run 'intentui add ' to sync.", + ) } }), ).pipe(Command.withDescription("Compares your local components with the registry versions.")) diff --git a/src/commands/init.command.ts b/src/commands/init.command.ts index b967fbd..c292001 100644 --- a/src/commands/init.command.ts +++ b/src/commands/init.command.ts @@ -1,7 +1,7 @@ import { Command } from "@effect/cli" -import { Console, Effect } from "effect" -import { Command as RawCommand, FileSystem } from "@effect/platform" +import { FileSystem, Command as RawCommand } from "@effect/platform" import { NodeFileSystem } from "@effect/platform-node" +import { Console, Effect } from "effect" import { REGISTRY_URL } from "~/consts" @@ -61,7 +61,6 @@ export const initCommand = Command.make("init", {}, () => const patched = lines.join("\n") yield* fileSytem.writeFileString(cssPath, patched) - }).pipe( Effect.scoped, Effect.provide(NodeCommandExecutor.layer), diff --git a/src/commands/login.command.ts b/src/commands/login.command.ts index d297692..95d3519 100644 --- a/src/commands/login.command.ts +++ b/src/commands/login.command.ts @@ -44,11 +44,7 @@ const generateNanoid = Effect.sync(() => nanoid()) const openUrl = (urlToOpen: string) => { const openCommand = - process.platform === "darwin" - ? "open" - : process.platform === "win32" - ? "start" - : "xdg-open" + process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open" return Effect.try({ try: () => diff --git a/src/lib/app.ts b/src/lib/app.ts new file mode 100644 index 0000000..3560ea0 --- /dev/null +++ b/src/lib/app.ts @@ -0,0 +1,9 @@ +export const app = { + repo: { + base: "irsyadadl/intentui", + branch: "2.x", + get ui() { + return `https://raw.githubusercontent.com/${this.base}/${this.branch}/components/ui` + }, + }, +} From f3823493a2055459d3071de6f0c622bdb8f221d5 Mon Sep 17 00:00:00 2001 From: "Irsyad A. Panjaitan" Date: Thu, 5 Jun 2025 12:09:28 +0700 Subject: [PATCH 5/7] add checking project --- src/commands/diff.command.ts | 2 ++ src/lib/check-current-user-project.ts | 41 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/lib/check-current-user-project.ts diff --git a/src/commands/diff.command.ts b/src/commands/diff.command.ts index 368e0c7..7c402bc 100644 --- a/src/commands/diff.command.ts +++ b/src/commands/diff.command.ts @@ -72,10 +72,12 @@ export const diffCommand = Command.make("diff", {}, () => const localContent = yield* Effect.tryPromise(() => FS.readFile(file, "utf8")) const remoteContent = yield* HttpClientRequest.get(remoteUrl).pipe( client.execute, + // @ts-ignore Effect.flatMap(HttpClientResponse.text), Effect.catchAll(() => Effect.succeed("")), ) if (localContent !== remoteContent) { + // @ts-ignore diffs.push({ file: relative, diff: simpleDiff(localContent, remoteContent) }) } } diff --git a/src/lib/check-current-user-project.ts b/src/lib/check-current-user-project.ts new file mode 100644 index 0000000..b2b3fbc --- /dev/null +++ b/src/lib/check-current-user-project.ts @@ -0,0 +1,41 @@ +import * as FS from "node:fs/promises" +import * as Path from "node:path" + +async function exists(p: string): Promise { + try { + await FS.stat(p) + return true + } catch { + return false + } +} + +export async function isNextWithSrc(dir: string): Promise { + const srcDir = Path.join(dir, "src") + if (!(await exists(srcDir))) return false + const hasPages = await exists(Path.join(srcDir, "pages")) + const hasApp = await exists(Path.join(srcDir, "app")) + return hasPages || hasApp +} + +export async function isNextWithoutSrc(dir: string): Promise { + if (await exists(Path.join(dir, "src"))) return false + const hasPages = await exists(Path.join(dir, "pages")) + const hasApp = await exists(Path.join(dir, "app")) + return hasPages || hasApp +} + +export async function isRemix(dir: string): Promise { + const pkgPath = Path.join(dir, "package.json") + if (!(await exists(pkgPath))) return false + const pkg = JSON.parse(await FS.readFile(pkgPath, "utf-8")) + const deps = { ...pkg.dependencies, ...pkg.devDependencies } + if (!deps || !("remix" in deps)) return false + return await exists(Path.join(dir, "app")) +} + +export async function isLaravel(dir: string): Promise { + const hasArtisan = await exists(Path.join(dir, "artisan")) + const hasComposer = await exists(Path.join(dir, "composer.json")) + return hasArtisan && hasComposer +} From 899e8d97c8a1f49eab85692b47fc9f61bc9e3bc6 Mon Sep 17 00:00:00 2001 From: "Irsyad A. Panjaitan" <44585532+irsyadadl@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:15:32 +0700 Subject: [PATCH 6/7] feat: auto-detect ui path --- src/commands/diff.command.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/commands/diff.command.ts b/src/commands/diff.command.ts index 7c402bc..9913161 100644 --- a/src/commands/diff.command.ts +++ b/src/commands/diff.command.ts @@ -4,6 +4,12 @@ import { Command } from "@effect/cli" import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" import { Console, Effect } from "effect" import { app } from "~/lib/app" +import { + isLaravel, + isNextWithSrc, + isNextWithoutSrc, + isRemix, +} from "~/lib/check-current-user-project" const REMOTE_BASE = app.repo.ui @@ -51,7 +57,21 @@ export const diffCommand = Command.make("diff", {}, () => return } - const alias: string = config.aliases?.ui ?? "components/ui" + let alias: string | undefined = config.aliases?.ui + + if (!alias) { + const nextWithSrc = yield* Effect.tryPromise(() => isNextWithSrc(cwd)) + const nextWithoutSrc = yield* Effect.tryPromise(() => isNextWithoutSrc(cwd)) + const remix = yield* Effect.tryPromise(() => isRemix(cwd)) + const laravel = yield* Effect.tryPromise(() => isLaravel(cwd)) + + if (nextWithSrc) alias = "src/components/ui" + else if (nextWithoutSrc) alias = "components/ui" + else if (remix) alias = "app/components/ui" + else if (laravel) alias = "resources/components/ui" + else alias = "components/ui" + } + const uiPath = Path.isAbsolute(alias.replace(/^@\//, "")) ? alias.replace(/^@\//, "") : Path.join(cwd, alias.replace(/^@\//, "")) @@ -72,12 +92,10 @@ export const diffCommand = Command.make("diff", {}, () => const localContent = yield* Effect.tryPromise(() => FS.readFile(file, "utf8")) const remoteContent = yield* HttpClientRequest.get(remoteUrl).pipe( client.execute, - // @ts-ignore Effect.flatMap(HttpClientResponse.text), Effect.catchAll(() => Effect.succeed("")), ) if (localContent !== remoteContent) { - // @ts-ignore diffs.push({ file: relative, diff: simpleDiff(localContent, remoteContent) }) } } From 12c5d371df22dd86c23575c94dd89c90b8cfd596 Mon Sep 17 00:00:00 2001 From: "Irsyad A. Panjaitan" Date: Thu, 5 Jun 2025 12:18:10 +0700 Subject: [PATCH 7/7] ff --- src/commands/diff.command.ts | 112 +---------------------------------- 1 file changed, 3 insertions(+), 109 deletions(-) diff --git a/src/commands/diff.command.ts b/src/commands/diff.command.ts index 9913161..d9a5bc9 100644 --- a/src/commands/diff.command.ts +++ b/src/commands/diff.command.ts @@ -1,115 +1,9 @@ -import * as FS from "node:fs/promises" -import * as Path from "node:path" import { Command } from "@effect/cli" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" import { Console, Effect } from "effect" -import { app } from "~/lib/app" -import { - isLaravel, - isNextWithSrc, - isNextWithoutSrc, - isRemix, -} from "~/lib/check-current-user-project" +import { componentNames, componentType } from "./add.command" -const REMOTE_BASE = app.repo.ui - -function simpleDiff(local: string, remote: string): string { - const l = local.split(/\r?\n/) - const r = remote.split(/\r?\n/) - const max = Math.max(l.length, r.length) - let out = "" - for (let i = 0; i < max; i++) { - const a = l[i] - const b = r[i] - if (a !== b) { - if (a !== undefined) out += `- ${a}\n` - if (b !== undefined) out += `+ ${b}\n` - } - } - return out -} - -async function listFiles(dir: string): Promise { - const entries = await FS.readdir(dir, { withFileTypes: true }) - const files: string[] = [] - for (const entry of entries) { - const res = Path.join(dir, entry.name) - if (entry.isDirectory()) { - files.push(...(await listFiles(res))) - } else { - files.push(res) - } - } - return files -} - -export const diffCommand = Command.make("diff", {}, () => +export const diffCommand = Command.make("diff", { componentNames, componentType }, (config) => Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - const cwd = process.cwd() - // biome-ignore lint/suspicious/noImplicitAnyLet: - let config - try { - const jsonStr = yield* Effect.tryPromise(() => FS.readFile("components.json", "utf8")) - config = JSON.parse(jsonStr) - } catch { - yield* Console.error("components.json not found or invalid") - return - } - - let alias: string | undefined = config.aliases?.ui - - if (!alias) { - const nextWithSrc = yield* Effect.tryPromise(() => isNextWithSrc(cwd)) - const nextWithoutSrc = yield* Effect.tryPromise(() => isNextWithoutSrc(cwd)) - const remix = yield* Effect.tryPromise(() => isRemix(cwd)) - const laravel = yield* Effect.tryPromise(() => isLaravel(cwd)) - - if (nextWithSrc) alias = "src/components/ui" - else if (nextWithoutSrc) alias = "components/ui" - else if (remix) alias = "app/components/ui" - else if (laravel) alias = "resources/components/ui" - else alias = "components/ui" - } - - const uiPath = Path.isAbsolute(alias.replace(/^@\//, "")) - ? alias.replace(/^@\//, "") - : Path.join(cwd, alias.replace(/^@\//, "")) - - const filesResult = yield* Effect.tryPromise(() => listFiles(uiPath)).pipe( - Effect.catchAll(() => Effect.succeed([] as string[])), - ) - if (filesResult.length === 0) { - yield* Console.error(`Unable to read local components at ${uiPath}`) - return - } - const files = filesResult - - const diffs: Array<{ file: string; diff: string }> = [] - for (const file of files) { - const relative = Path.relative(uiPath, file).replace(/\\/g, "/") - const remoteUrl = `${REMOTE_BASE}/${relative}` - const localContent = yield* Effect.tryPromise(() => FS.readFile(file, "utf8")) - const remoteContent = yield* HttpClientRequest.get(remoteUrl).pipe( - client.execute, - Effect.flatMap(HttpClientResponse.text), - Effect.catchAll(() => Effect.succeed("")), - ) - if (localContent !== remoteContent) { - diffs.push({ file: relative, diff: simpleDiff(localContent, remoteContent) }) - } - } - - if (diffs.length === 0) { - yield* Console.log("All components are up to date.") - } else { - for (const d of diffs) { - yield* Console.log(`--- ${d.file} ---`) - yield* Console.log(d.diff) - } - yield* Console.log( - "Some components differ from the registry. Run 'intentui add ' to sync.", - ) - } + yield* Console.log("Coming soon...") }), ).pipe(Command.withDescription("Compares your local components with the registry versions."))