Skip to content

Commit 2581044

Browse files
authored
Import package scripts when creating projects (#322)
- Extract shared project import resolution into `projectImport` - Auto-fill project actions from package scripts in sidebar and home - Show a warning when lockfiles make package manager selection ambiguous
1 parent fbe55c9 commit 2581044

7 files changed

Lines changed: 149 additions & 42 deletions

File tree

apps/web/src/components/CodeViewerPanel.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,7 @@ export const CodeViewerFileContent = memo(function CodeViewerFileContent(
498498
"border-primary/40 text-primary hover:bg-primary/10 dark:border-primary/30 dark:text-primary",
499499
saveButtonState === "saved" &&
500500
"border-emerald-500/30 text-emerald-600 hover:bg-emerald-500/10 dark:border-emerald-500/20 dark:text-emerald-400",
501-
saveButtonState === "clean" &&
502-
"text-muted-foreground",
501+
saveButtonState === "clean" && "text-muted-foreground",
503502
)}
504503
title={
505504
saveButtonState === "dirty"
@@ -603,8 +602,6 @@ export default function CodeViewerPanel() {
603602
[setPendingContext],
604603
);
605604

606-
607-
608605
return (
609606
<div className="flex h-full w-full flex-col bg-background">
610607
<div

apps/web/src/components/DiffPanel.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
319319
if (!selectedFilePath || !patchViewportRef.current) {
320320
return;
321321
}
322-
const selectedFile = renderableFiles.find(
323-
(f) => resolveFileDiffPath(f) === selectedFilePath,
324-
);
322+
const selectedFile = renderableFiles.find((f) => resolveFileDiffPath(f) === selectedFilePath);
325323
if (selectedFile) {
326324
const key = buildFileDiffRenderKey(selectedFile);
327325
setCollapsedFileKeys((current) => {
@@ -546,9 +544,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
546544
const filePath = resolveFileDiffPath(fileDiff);
547545
const fileKey = buildFileDiffRenderKey(fileDiff);
548546
const themedFileKey = `${fileKey}:${resolvedTheme}`;
549-
const isAccepted = acceptedFileKeys.has(
550-
buildAcceptedDiffFileKey(fileDiff),
551-
);
547+
const isAccepted = acceptedFileKeys.has(buildAcceptedDiffFileKey(fileDiff));
552548
const isCollapsed = collapsedFileKeys.has(fileKey);
553549
const changeType = categorizeFileDiff(fileDiff);
554550
return (
@@ -642,8 +638,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
642638
<FileDiff
643639
fileDiff={fileDiff}
644640
options={{
645-
diffStyle:
646-
diffRenderMode === "split" ? "split" : "unified",
641+
diffStyle: diffRenderMode === "split" ? "split" : "unified",
647642
lineDiffType: "none",
648643
overflow: diffWordWrap ? "wrap" : "scroll",
649644
theme: resolveDiffThemeName(resolvedTheme),

apps/web/src/components/Sidebar.tsx

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,7 @@ import { WorkspaceFileTree } from "~/components/WorkspaceFileTree";
123123
import { EditableThreadTitle } from "~/components/EditableThreadTitle";
124124
import { useProjectTitleEditor } from "~/hooks/useProjectTitleEditor";
125125
import { useThreadTitleEditor } from "~/hooks/useThreadTitleEditor";
126-
import {
127-
buildProjectScriptDraftsFromPackageScripts,
128-
materializeProjectScripts,
129-
readPackageScriptInventory,
130-
resolvePackageManagerResolution,
131-
} from "~/projectScriptDefaults";
126+
import { resolveImportedProjectScripts } from "~/lib/projectImport";
132127
import { useClientMode } from "~/hooks/useClientMode";
133128
import { CloneRepositoryDialog } from "~/components/CloneRepositoryDialog";
134129
import { getProjectColor } from "~/projectColors";
@@ -769,28 +764,8 @@ export default function Sidebar() {
769764
const createdAt = new Date().toISOString();
770765
const title = cwd.split(/[/\\]/).findLast(isNonEmptyString) ?? cwd;
771766
try {
772-
let projectScripts;
773-
let packageScriptWarning: string | null = null;
774-
try {
775-
const inventory = await readPackageScriptInventory(api, cwd);
776-
const packageManagerResolution = resolvePackageManagerResolution(inventory);
777-
packageScriptWarning =
778-
inventory.scriptNames.length > 0 ? packageManagerResolution.warning : null;
779-
if (
780-
inventory.scriptNames.length > 0 &&
781-
packageManagerResolution.preferredPackageManager &&
782-
!packageManagerResolution.requiresManualSelection
783-
) {
784-
projectScripts = materializeProjectScripts(
785-
buildProjectScriptDraftsFromPackageScripts({
786-
scriptNames: inventory.scriptNames,
787-
packageManager: packageManagerResolution.preferredPackageManager,
788-
}),
789-
);
790-
}
791-
} catch {
792-
projectScripts = undefined;
793-
}
767+
const { scripts: projectScripts, warning: packageScriptWarning } =
768+
await resolveImportedProjectScripts(api, cwd);
794769

795770
await api.orchestration.dispatchCommand({
796771
type: "project.create",

apps/web/src/components/file-view/FileViewShell.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,6 @@ export function FileViewShell(props: { initialCwd: string; initialPath: string |
8484
[setPendingContext],
8585
);
8686

87-
88-
8987
return (
9088
<div className="flex h-full w-full flex-col">
9189
{/* Tab bar */}

apps/web/src/components/home/ChatHomeEmptyState.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useAppSettings } from "../../appSettings";
77
import { APP_DISPLAY_NAME } from "../../branding";
88
import { isElectron } from "../../env";
99
import { useHandleNewThread } from "../../hooks/useHandleNewThread";
10+
import { resolveImportedProjectScripts } from "../../lib/projectImport";
1011
import { serverConfigQueryOptions } from "../../lib/serverReactQuery";
1112
import { newCommandId, newProjectId } from "../../lib/utils";
1213
import { readNativeApi } from "../../nativeApi";
@@ -105,15 +106,25 @@ export function ChatHomeEmptyState() {
105106
const title = pickedPath.split(/[/\\]/).findLast((segment) => segment.length > 0) ?? pickedPath;
106107
try {
107108
const projectId = newProjectId();
109+
const { scripts: projectScripts, warning: packageScriptWarning } =
110+
await resolveImportedProjectScripts(api, pickedPath);
108111
await api.orchestration.dispatchCommand({
109112
type: "project.create",
110113
commandId: newCommandId(),
111114
projectId,
112115
title,
113116
workspaceRoot: pickedPath,
114117
defaultModel: DEFAULT_MODEL_BY_PROVIDER.codex,
118+
...(projectScripts ? { scripts: projectScripts } : {}),
115119
createdAt: new Date().toISOString(),
116120
});
121+
if (packageScriptWarning) {
122+
toastManager.add({
123+
type: "warning",
124+
title: "Project actions need a package manager choice",
125+
description: packageScriptWarning,
126+
});
127+
}
117128
await handleNewThread(projectId, {
118129
envMode: appSettings.defaultThreadEnvMode,
119130
}).catch(() => undefined);
@@ -146,15 +157,25 @@ export function ChatHomeEmptyState() {
146157

147158
const projectId = newProjectId();
148159
try {
160+
const { scripts: projectScripts, warning: packageScriptWarning } =
161+
await resolveImportedProjectScripts(api, result.path);
149162
await api.orchestration.dispatchCommand({
150163
type: "project.create",
151164
commandId: newCommandId(),
152165
projectId,
153166
title: result.repoName,
154167
workspaceRoot: result.path,
155168
defaultModel: DEFAULT_MODEL_BY_PROVIDER.codex,
169+
...(projectScripts ? { scripts: projectScripts } : {}),
156170
createdAt: new Date().toISOString(),
157171
});
172+
if (packageScriptWarning) {
173+
toastManager.add({
174+
type: "warning",
175+
title: "Project actions need a package manager choice",
176+
description: packageScriptWarning,
177+
});
178+
}
158179
await handleNewThread(projectId, {
159180
envMode: appSettings.defaultThreadEnvMode,
160181
}).catch(() => undefined);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { resolveImportedProjectScripts } from "./projectImport";
4+
5+
describe("resolveImportedProjectScripts", () => {
6+
it("imports scripts when the package manager can be resolved automatically", async () => {
7+
const api = {
8+
projects: {
9+
readFile: vi.fn().mockResolvedValue({
10+
relativePath: "package.json",
11+
contents: JSON.stringify({
12+
scripts: {
13+
lint: "eslint .",
14+
build: "vite build",
15+
},
16+
}),
17+
sizeBytes: 64,
18+
truncated: false,
19+
}),
20+
listDirectory: vi.fn().mockResolvedValue({
21+
entries: [{ path: "bun.lock", kind: "file" }],
22+
truncated: false,
23+
}),
24+
},
25+
} as const;
26+
27+
await expect(resolveImportedProjectScripts(api as never, "/tmp/repo")).resolves.toEqual({
28+
scripts: [
29+
{
30+
id: "lint",
31+
name: "Lint",
32+
command: "bun run lint",
33+
icon: "lint",
34+
runOnWorktreeCreate: false,
35+
},
36+
{
37+
id: "build",
38+
name: "Build",
39+
command: "bun run build",
40+
icon: "build",
41+
runOnWorktreeCreate: false,
42+
},
43+
],
44+
warning: null,
45+
});
46+
});
47+
48+
it("returns a warning without importing scripts when package manager choice is ambiguous", async () => {
49+
const api = {
50+
projects: {
51+
readFile: vi.fn().mockResolvedValue({
52+
relativePath: "package.json",
53+
contents: JSON.stringify({
54+
scripts: {
55+
dev: "vite",
56+
},
57+
}),
58+
sizeBytes: 32,
59+
truncated: false,
60+
}),
61+
listDirectory: vi.fn().mockResolvedValue({
62+
entries: [
63+
{ path: "bun.lock", kind: "file" },
64+
{ path: "pnpm-lock.yaml", kind: "file" },
65+
],
66+
truncated: false,
67+
}),
68+
},
69+
} as const;
70+
71+
await expect(resolveImportedProjectScripts(api as never, "/tmp/repo")).resolves.toEqual({
72+
scripts: undefined,
73+
warning:
74+
"Multiple package manager lockfiles were detected (bun, pnpm). Select the package manager to use for imported actions.",
75+
});
76+
});
77+
});

apps/web/src/lib/projectImport.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { NativeApi, ProjectScript } from "@okcode/contracts";
2+
3+
import {
4+
buildProjectScriptDraftsFromPackageScripts,
5+
materializeProjectScripts,
6+
readPackageScriptInventory,
7+
resolvePackageManagerResolution,
8+
} from "../projectScriptDefaults";
9+
10+
export interface ImportedProjectScriptsResolution {
11+
scripts: ProjectScript[] | undefined;
12+
warning: string | null;
13+
}
14+
15+
export async function resolveImportedProjectScripts(
16+
api: NativeApi,
17+
cwd: string,
18+
): Promise<ImportedProjectScriptsResolution> {
19+
try {
20+
const inventory = await readPackageScriptInventory(api, cwd);
21+
const packageManagerResolution = resolvePackageManagerResolution(inventory);
22+
const warning = inventory.scriptNames.length > 0 ? packageManagerResolution.warning : null;
23+
24+
if (
25+
inventory.scriptNames.length === 0 ||
26+
!packageManagerResolution.preferredPackageManager ||
27+
packageManagerResolution.requiresManualSelection
28+
) {
29+
return { scripts: undefined, warning };
30+
}
31+
32+
return {
33+
scripts: materializeProjectScripts(
34+
buildProjectScriptDraftsFromPackageScripts({
35+
scriptNames: inventory.scriptNames,
36+
packageManager: packageManagerResolution.preferredPackageManager,
37+
}),
38+
),
39+
warning,
40+
};
41+
} catch {
42+
return { scripts: undefined, warning: null };
43+
}
44+
}

0 commit comments

Comments
 (0)