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
167 changes: 163 additions & 4 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@
{
"from": "resources/bin/VERSION",
"to": "bin/VERSION"
},
{
"from": "resources/cli",
"to": "cli"
}
],
"asar": true,
Expand Down
16 changes: 16 additions & 0 deletions resources/cli/1code
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
# 1code - Open 1Code in a directory
# Usage: 1code [directory]

DIR="${1:-.}"

# Resolve to absolute path
if [[ -d "$DIR" ]]; then
DIR=$(cd "$DIR" && pwd)
else
echo "Error: '$DIR' is not a valid directory"
exit 1
fi

# Open 1Code with the directory as argument
open -a "1Code" --args "$DIR"
154 changes: 153 additions & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { app, BrowserWindow, session, Menu } from "electron"
import { join } from "path"
import { createServer } from "http"
import { readFileSync, existsSync, unlinkSync, readlinkSync } from "fs"
import {
readFileSync,
existsSync,
unlinkSync,
readlinkSync,
symlinkSync,
lstatSync,
} from "fs"
import * as Sentry from "@sentry/electron/main"
import { initDatabase, closeDatabase } from "./lib/db"
import { createMainWindow, getWindow, showLoginPage } from "./windows/main"
Expand Down Expand Up @@ -76,6 +83,113 @@ export function getAuthManager(): AuthManager {
return authManager
}

// Launch directory from CLI (e.g., `1code /path/to/project`)
let launchDirectory: string | null = null

export function getLaunchDirectory(): string | null {
const dir = launchDirectory
launchDirectory = null // consume once
return dir
}

function parseLaunchDirectory(): string | null {
// Look for a directory argument in argv
// Skip electron executable and script path
const args = process.argv.slice(process.defaultApp ? 2 : 1)

for (const arg of args) {
// Skip flags and protocol URLs
if (arg.startsWith("-") || arg.includes("://")) continue

// Check if it's a valid directory
if (existsSync(arg)) {
try {
const stat = lstatSync(arg)
if (stat.isDirectory()) {
console.log("[CLI] Launch directory:", arg)
return arg
}
} catch {
// ignore
}
}
}
return null
}

// CLI command installation
const CLI_INSTALL_PATH = "/usr/local/bin/1code"

function getCliSourcePath(): string {
if (app.isPackaged) {
return join(process.resourcesPath, "cli", "1code")
}
return join(__dirname, "..", "..", "resources", "cli", "1code")
}

export function isCliInstalled(): boolean {
try {
if (!existsSync(CLI_INSTALL_PATH)) return false
const stat = lstatSync(CLI_INSTALL_PATH)
if (!stat.isSymbolicLink()) return false
const target = readlinkSync(CLI_INSTALL_PATH)
return target === getCliSourcePath()
} catch {
return false
}
}

export async function installCli(): Promise<{ success: boolean; error?: string }> {
const { exec } = await import("child_process")
const { promisify } = await import("util")
const execAsync = promisify(exec)

const sourcePath = getCliSourcePath()

if (!existsSync(sourcePath)) {
return { success: false, error: "CLI script not found in app bundle" }
}

try {
// Remove existing if present
if (existsSync(CLI_INSTALL_PATH)) {
await execAsync(
`osascript -e 'do shell script "rm -f ${CLI_INSTALL_PATH}" with administrator privileges'`,
)
}

// Create symlink with admin privileges
await execAsync(
`osascript -e 'do shell script "ln -s \\"${sourcePath}\\" ${CLI_INSTALL_PATH}" with administrator privileges'`,
)

console.log("[CLI] Installed 1code command to", CLI_INSTALL_PATH)
return { success: true }
} catch (error: any) {
console.error("[CLI] Failed to install:", error)
return { success: false, error: error.message || "Installation failed" }
}
}

export async function uninstallCli(): Promise<{ success: boolean; error?: string }> {
const { exec } = await import("child_process")
const { promisify } = await import("util")
const execAsync = promisify(exec)

try {
if (existsSync(CLI_INSTALL_PATH)) {
await execAsync(
`osascript -e 'do shell script "rm -f ${CLI_INSTALL_PATH}" with administrator privileges'`,
)
}
console.log("[CLI] Uninstalled 1code command")
return { success: true }
} catch (error: any) {
console.error("[CLI] Failed to uninstall:", error)
return { success: false, error: error.message || "Uninstallation failed" }
}
}

// Handle auth code from deep link (exported for IPC handlers)
export async function handleAuthCode(code: string): Promise<void> {
console.log("[Auth] Handling auth code:", code.slice(0, 8) + "...")
Expand Down Expand Up @@ -477,6 +591,41 @@ if (gotTheLock) {
},
},
{ type: "separator" },
{
label: isCliInstalled()
? "Uninstall '1code' Command..."
: "Install '1code' Command in PATH...",
click: async () => {
const { dialog } = await import("electron")
if (isCliInstalled()) {
const result = await uninstallCli()
if (result.success) {
dialog.showMessageBox({
type: "info",
message: "CLI command uninstalled",
detail: "The '1code' command has been removed from your PATH.",
})
buildMenu()
} else {
dialog.showErrorBox("Uninstallation Failed", result.error || "Unknown error")
}
} else {
const result = await installCli()
if (result.success) {
dialog.showMessageBox({
type: "info",
message: "CLI command installed",
detail:
"You can now use '1code .' in any terminal to open 1Code in that directory.",
})
buildMenu()
} else {
dialog.showErrorBox("Installation Failed", result.error || "Unknown error")
}
}
},
},
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
Expand Down Expand Up @@ -632,6 +781,9 @@ if (gotTheLock) {
}, 5000)
}

// Handle directory argument from CLI (e.g., `1code /path/to/project`)
launchDirectory = parseLaunchDirectory()

// Handle deep link from app launch (Windows/Linux)
const deepLinkUrl = process.argv.find((arg) =>
arg.startsWith(`${PROTOCOL}://`),
Expand Down
8 changes: 8 additions & 0 deletions src/main/lib/trpc/routers/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ import { dialog, BrowserWindow } from "electron"
import { basename } from "path"
import { getGitRemoteInfo } from "../../git"
import { trackProjectOpened } from "../../analytics"
import { getLaunchDirectory } from "../../../index"

export const projectsRouter = router({
/**
* Get launch directory from CLI args (consumed once)
*/
getLaunchDirectory: publicProcedure.query(() => {
return getLaunchDirectory()
}),

/**
* List all projects
*/
Expand Down
45 changes: 45 additions & 0 deletions src/renderer/features/agents/main/new-chat-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,51 @@ export function NewChatForm({
const { data: projectsList, isLoading: isLoadingProjects } =
trpc.projects.list.useQuery()

// Check for launch directory from CLI (e.g., `1code /path/to/project`)
const { data: launchDirectory } = trpc.projects.getLaunchDirectory.useQuery()

// Mutation to create project from path
const createProjectMutation = trpc.projects.create.useMutation({
onSuccess: (project) => {
if (project) {
// Update projects list cache
utils.projects.list.setData(undefined, (oldData) => {
if (!oldData) return [project]
const exists = oldData.some((p) => p.id === project.id)
if (exists) {
return oldData.map((p) =>
p.id === project.id ? { ...p, updatedAt: project.updatedAt } : p,
)
}
return [project, ...oldData]
})

// Select the project
setSelectedProject({
id: project.id,
name: project.name,
path: project.path,
gitRemoteUrl: project.gitRemoteUrl,
gitProvider: project.gitProvider as
| "github"
| "gitlab"
| "bitbucket"
| null,
gitOwner: project.gitOwner,
gitRepo: project.gitRepo,
})
}
},
})

// Handle launch directory from CLI
useEffect(() => {
if (launchDirectory) {
console.log("[CLI] Processing launch directory:", launchDirectory)
createProjectMutation.mutate({ path: launchDirectory })
}
}, [launchDirectory])

// Validate selected project exists in DB
// While loading, trust the stored value to prevent flicker
const validatedProject = useMemo(() => {
Expand Down