Skip to content

Commit 951c2bb

Browse files
committed
WIP
1 parent 72f2bfa commit 951c2bb

File tree

3 files changed

+254
-13
lines changed

3 files changed

+254
-13
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"editor.insertSpaces": false,
3-
"editor.defaultFormatter": "esbenp.prettier-vscode"
3+
"editor.defaultFormatter": "esbenp.prettier-vscode",
4+
"kiroAgent.configureMCP": "Disabled"
45
}

src/environments.ts

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,200 @@
1-
import { commands, Disposable } from "vscode";
1+
import os from "os";
2+
import {
3+
commands,
4+
Disposable,
5+
StatusBarAlignment,
6+
ThemeColor,
7+
window,
8+
workspace,
9+
} from "vscode";
210
import type { Core } from "./core";
311
import { AppAction, createOpenOPHandler } from "./url-utils";
412
import { COMMANDS } from "./constants";
13+
import { existsSync } from "fs";
14+
import path from "path";
15+
import { execFile } from "child_process";
516

617
export class Environments {
18+
private disposables: Disposable[] = [];
19+
private item = window.createStatusBarItem(
20+
"op.mounted",
21+
StatusBarAlignment.Left,
22+
1000,
23+
);
24+
private refreshPending = false;
25+
private intervalHandle?: NodeJS.Timeout;
26+
727
public constructor(private core: Core) {
828
commands.registerCommand(COMMANDS.IMPORT_PROJECT, async () =>
929
createOpenOPHandler(this.core)({
1030
action: AppAction.ImportProject,
1131
}),
1232
);
33+
34+
const isWindows = os.platform() === "win32";
35+
if (isWindows) {
36+
return;
37+
}
38+
39+
this.item.name = "1Password: Mount Status";
40+
core.context.subscriptions.push(this);
41+
42+
// Start polling every 500ms
43+
this.intervalHandle = setInterval(() => {
44+
void this.refresh();
45+
}, 500);
46+
this.disposables.push({
47+
dispose: () => this.intervalHandle && clearInterval(this.intervalHandle),
48+
});
49+
50+
void this.refresh();
51+
}
52+
53+
public dispose(): void {
54+
for (const d of this.disposables) {
55+
d.dispose();
56+
}
57+
this.item.dispose();
58+
}
59+
60+
private async refresh(): Promise<void> {
61+
if (this.refreshPending) {
62+
return;
63+
}
64+
this.refreshPending = true;
65+
66+
try {
67+
const firstFolder = workspace.workspaceFolders?.[0];
68+
const firstFsPath = firstFolder?.uri.fsPath;
69+
70+
if (!firstFsPath) {
71+
this.setNoEnvironment();
72+
return;
73+
}
74+
75+
const dbPath = path.join(
76+
os.homedir(),
77+
"Library",
78+
"Group Containers",
79+
"2BUA8C4S2C.com.1password",
80+
"Library",
81+
"Application Support",
82+
"1Password",
83+
"Data",
84+
"debug",
85+
"1password.sqlite",
86+
);
87+
88+
if (!existsSync(dbPath)) {
89+
this.setNoEnvironment();
90+
return;
91+
}
92+
93+
const mounts = await this.getEnvironmentMounts(dbPath);
94+
const matching = mounts.find((m) => {
95+
const mountPath = m.mountPath.split("/").slice(0, -1).join("/");
96+
return mountPath === firstFsPath;
97+
});
98+
99+
if (!matching) {
100+
this.setNoEnvironment();
101+
return;
102+
}
103+
104+
if (!matching.isEnabled) {
105+
this.item.text = "$(close) Environment Disabled";
106+
this.item.tooltip = `Mount to ${matching.environmentName} is disabled`;
107+
this.item.backgroundColor = new ThemeColor(
108+
"statusBarItem.warningBackground",
109+
);
110+
this.item.show();
111+
return;
112+
}
113+
114+
this.item.text = `$(check) Environment: ${matching.environmentName}`;
115+
this.item.tooltip = `Mounted to ${matching.environmentName}`;
116+
this.item.backgroundColor = undefined;
117+
this.item.show();
118+
} catch {
119+
this.setNoEnvironment();
120+
} finally {
121+
this.refreshPending = false;
122+
}
123+
}
124+
125+
private setNoEnvironment(): void {
126+
this.item.text = "$(close) No Environment";
127+
this.item.tooltip =
128+
"No 1Password environments were found for this workspace. Click to import.";
129+
this.item.backgroundColor = new ThemeColor(
130+
"statusBarItem.warningBackground",
131+
);
132+
this.item.command = COMMANDS.IMPORT_PROJECT;
133+
this.item.show();
134+
}
135+
136+
private async getEnvironmentMounts(dbPath: string): Promise<
137+
{
138+
mountPath: string;
139+
environmentName: string;
140+
isEnabled: boolean;
141+
}[]
142+
> {
143+
const sqliteBin = "/usr/bin/sqlite3";
144+
const sql =
145+
"SELECT key_name, hex(data) FROM objects WHERE key_name LIKE 'dev-environment-mount/%';";
146+
147+
const stdout = await new Promise<string>((resolve, reject) => {
148+
const args = ["-batch", "-noheader", "-cmd", ".mode tabs", dbPath, sql];
149+
execFile(
150+
sqliteBin,
151+
args,
152+
{ encoding: "utf8", maxBuffer: 10 * 1024 * 1024 },
153+
(err, out, stderr) => {
154+
void stderr;
155+
if (err) {
156+
reject(err);
157+
return;
158+
}
159+
resolve(out);
160+
},
161+
);
162+
});
163+
164+
const mounts: {
165+
mountPath: string;
166+
environmentName: string;
167+
isEnabled: boolean;
168+
}[] = [];
169+
const lines = stdout.split(/\r?\n/).filter((l) => l.trim().length > 0);
170+
for (const line of lines) {
171+
const [keyName, dataHex] = line.split("\t");
172+
if (!keyName || !dataHex) {
173+
continue;
174+
}
175+
176+
try {
177+
const jsonStr = Buffer.from(dataHex.trim(), "hex").toString("utf8");
178+
const parsed = JSON.parse(jsonStr) as {
179+
mountPath?: string;
180+
environmentName?: string;
181+
isEnabled?: boolean;
182+
};
183+
if (
184+
typeof parsed.mountPath === "string" &&
185+
typeof parsed.environmentName === "string" &&
186+
typeof parsed.isEnabled === "boolean"
187+
) {
188+
mounts.push({
189+
mountPath: parsed.mountPath,
190+
environmentName: parsed.environmentName,
191+
isEnabled: parsed.isEnabled,
192+
});
193+
}
194+
} catch {
195+
// ignore malformed rows
196+
}
197+
}
198+
return mounts;
13199
}
14200
}

src/url-utils.ts

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import { Item, item } from "@1password/op-js";
22
import { default as open } from "open";
3-
import { commands, env, Uri, UriHandler, workspace, window } from "vscode";
3+
import {
4+
commands,
5+
env,
6+
Uri,
7+
UriHandler,
8+
workspace,
9+
window,
10+
WorkspaceFolder,
11+
} from "vscode";
412
import { COMMANDS, QUALIFIED_EXTENSION_ID } from "./constants";
513
import { Core } from "./core";
614
import { logger } from "./logger";
15+
import { promises as fs } from "fs";
16+
import * as path from "path";
717

818
export enum UriAction {
919
OpenItem = "open-item",
@@ -45,7 +55,7 @@ export const createOpenOPHandler =
4555
url.searchParams.append("i", vaultItem.id);
4656
break;
4757
case AppAction.ImportProject:
48-
const path = await getWorkspaceFolder();
58+
const path = await getWorkspaceEnvironmentFile();
4959
url.searchParams.append("path", path);
5060
break;
5161
}
@@ -71,27 +81,71 @@ export class OpvsUriHandler implements UriHandler {
7181
}
7282
}
7383

74-
const getWorkspaceFolder = async (): Promise<string | undefined> => {
84+
const getWorkspaceEnvironmentFile = async (): Promise<string | undefined> => {
7585
const workspaceFolders = workspace.workspaceFolders;
7686
if (!workspaceFolders || workspaceFolders.length === 0) {
7787
return undefined; // No workspace open
7888
}
7989

90+
let folder: WorkspaceFolder;
91+
92+
// get the workspace folder
8093
if (workspaceFolders.length === 1) {
81-
return workspaceFolders[0].uri.fsPath; // Single workspace
94+
folder = workspaceFolders[0];
95+
} else {
96+
const selected = await window.showQuickPick(
97+
workspaceFolders.map((f) => ({
98+
label: f.name,
99+
description: f.uri.fsPath,
100+
folder: f,
101+
})),
102+
{
103+
placeHolder: "Select a workspace folder",
104+
matchOnDescription: true,
105+
},
106+
);
107+
if (!selected) {
108+
return undefined;
109+
}
110+
folder = selected.folder;
111+
}
112+
113+
const folderPath = folder.uri.fsPath;
114+
115+
// get the environment file
116+
let envFiles: string[] = [];
117+
try {
118+
const files = await fs.readdir(folderPath);
119+
envFiles = files.filter(
120+
(file) => file === ".env" || file.startsWith(".env."),
121+
);
122+
if (files.includes(".env")) {
123+
envFiles.unshift(".env"); // Ensure .env is first if present
124+
envFiles = Array.from(new Set(envFiles)); // Remove duplicates
125+
}
126+
} catch {
127+
// ignore errors, treat as no env files
128+
}
129+
130+
if (envFiles.length === 0) {
131+
return undefined;
132+
}
133+
134+
if (envFiles.length === 1) {
135+
return path.join(folderPath, envFiles[0]);
82136
}
83137

84-
const selected = await window.showQuickPick(
85-
workspaceFolders.map((folder) => ({
86-
label: folder.name,
87-
description: folder.uri.fsPath,
88-
folder,
138+
const selectedEnv = await window.showQuickPick(
139+
envFiles.map((file) => ({
140+
label: file,
141+
description: path.join(folderPath, file),
142+
file,
89143
})),
90144
{
91-
placeHolder: "Select a workspace folder",
145+
placeHolder: "Select an environment file",
92146
matchOnDescription: true,
93147
},
94148
);
95149

96-
return selected?.folder.uri.fsPath;
150+
return selectedEnv ? path.join(folderPath, selectedEnv.file) : undefined;
97151
};

0 commit comments

Comments
 (0)