diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml index eb515eda..16953380 100644 --- a/.github/workflows/pr-policy.yml +++ b/.github/workflows/pr-policy.yml @@ -32,6 +32,7 @@ jobs: node-version: 20 - name: Block manual lockfile edits + if: github.head_ref != 'chore/refresh-lockfile' run: | changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}")" if printf '%s\n' "$changed" | grep -qx 'pnpm-lock.yaml'; then diff --git a/.github/workflows/refresh-lockfile.yml b/.github/workflows/refresh-lockfile.yml index 079fdd4e..a879e5bc 100644 --- a/.github/workflows/refresh-lockfile.yml +++ b/.github/workflows/refresh-lockfile.yml @@ -11,11 +11,12 @@ concurrency: cancel-in-progress: false jobs: - refresh_and_verify: + refresh: runs-on: ubuntu-latest - timeout-minutes: 25 + timeout-minutes: 10 permissions: contents: write + pull-requests: write steps: - name: Checkout repository @@ -40,6 +41,7 @@ jobs: run: | changed="$(git status --porcelain)" if [ -z "$changed" ]; then + echo "Lockfile is already up to date." exit 0 fi if printf '%s\n' "$changed" | grep -Fvq ' pnpm-lock.yaml'; then @@ -48,29 +50,32 @@ jobs: exit 1 fi - - name: Commit refreshed lockfile + - name: Create or update pull request + env: + GH_TOKEN: ${{ github.token }} run: | if git diff --quiet -- pnpm-lock.yaml; then + echo "Lockfile unchanged, nothing to do." exit 0 fi + + BRANCH="chore/refresh-lockfile" git config user.name "lockfile-bot" git config user.email "lockfile-bot@users.noreply.github.com" + + git checkout -B "$BRANCH" git add pnpm-lock.yaml git commit -m "chore(lockfile): refresh pnpm-lock.yaml" - git push || { - echo "Push failed because master moved during lockfile refresh." - echo "A later refresh run should recompute the lockfile from the newer master state." - exit 1 - } + git push --force origin "$BRANCH" - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Typecheck - run: pnpm -r typecheck - - - name: Run tests - run: pnpm test:run - - - name: Build - run: pnpm build + # Create PR if one doesn't already exist + existing=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number') + if [ -z "$existing" ]; then + gh pr create \ + --head "$BRANCH" \ + --title "chore(lockfile): refresh pnpm-lock.yaml" \ + --body "Auto-generated lockfile refresh after dependencies changed on master. This PR only updates pnpm-lock.yaml." + echo "Created new PR." + else + echo "PR #$existing already exists, branch updated via force push." + fi diff --git a/cli/src/__tests__/allowed-hostname.test.ts b/cli/src/__tests__/allowed-hostname.test.ts index 92dfbf42..572689c4 100644 --- a/cli/src/__tests__/allowed-hostname.test.ts +++ b/cli/src/__tests__/allowed-hostname.test.ts @@ -42,6 +42,7 @@ function writeBaseConfig(configPath: string) { }, auth: { baseUrlMode: "auto", + disableSignUp: false, }, storage: { provider: "local_disk", diff --git a/cli/src/commands/configure.ts b/cli/src/commands/configure.ts index d072fee9..969ead97 100644 --- a/cli/src/commands/configure.ts +++ b/cli/src/commands/configure.ts @@ -61,6 +61,7 @@ function defaultConfig(): PaperclipConfig { }, auth: { baseUrlMode: "auto", + disableSignUp: false, }, storage: defaultStorageConfig(), secrets: defaultSecretsConfig(), diff --git a/cli/src/commands/onboard.ts b/cli/src/commands/onboard.ts index 0e70d9cf..e3f17001 100644 --- a/cli/src/commands/onboard.ts +++ b/cli/src/commands/onboard.ts @@ -185,6 +185,7 @@ function quickstartDefaultsFromEnv(): { }, auth: { baseUrlMode: authBaseUrlMode, + disableSignUp: false, ...(authPublicBaseUrl ? { publicBaseUrl: authPublicBaseUrl } : {}), }, storage: { diff --git a/cli/src/prompts/server.ts b/cli/src/prompts/server.ts index c2ab4218..00611560 100644 --- a/cli/src/prompts/server.ts +++ b/cli/src/prompts/server.ts @@ -113,7 +113,7 @@ export async function promptServer(opts?: { } const port = Number(portStr) || 3100; - let auth: AuthConfig = { baseUrlMode: "auto" }; + let auth: AuthConfig = { baseUrlMode: "auto", disableSignUp: false }; if (deploymentMode === "authenticated" && exposure === "public") { const urlInput = await p.text({ message: "Public base URL", @@ -139,11 +139,13 @@ export async function promptServer(opts?: { } auth = { baseUrlMode: "explicit", + disableSignUp: false, publicBaseUrl: urlInput.trim().replace(/\/+$/, ""), }; } else if (currentAuth?.baseUrlMode === "explicit" && currentAuth.publicBaseUrl) { auth = { baseUrlMode: "explicit", + disableSignUp: false, publicBaseUrl: currentAuth.publicBaseUrl, }; } diff --git a/scripts/prepare-server-ui-dist.sh b/scripts/prepare-server-ui-dist.sh new file mode 100755 index 00000000..d43807b3 --- /dev/null +++ b/scripts/prepare-server-ui-dist.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +# prepare-server-ui-dist.sh — Build the UI and copy it into server/ui-dist. +# This keeps @paperclipai/server publish artifacts self-contained for static UI serving. + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +UI_DIST="$REPO_ROOT/ui/dist" +SERVER_UI_DIST="$REPO_ROOT/server/ui-dist" + +echo " -> Building @paperclipai/ui..." +pnpm --dir "$REPO_ROOT" --filter @paperclipai/ui build + +if [ ! -f "$UI_DIST/index.html" ]; then + echo "Error: UI build output missing at $UI_DIST/index.html" + exit 1 +fi + +rm -rf "$SERVER_UI_DIST" +cp -r "$UI_DIST" "$SERVER_UI_DIST" +echo " -> Copied ui/dist to server/ui-dist" diff --git a/scripts/release.sh b/scripts/release.sh index 6827e0fa..3668d87c 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -283,9 +283,7 @@ pnpm --filter @paperclipai/adapter-openclaw-gateway build pnpm --filter @paperclipai/server build # Build UI and bundle into server package for static serving -pnpm --filter @paperclipai/ui build -rm -rf "$REPO_ROOT/server/ui-dist" -cp -r "$REPO_ROOT/ui/dist" "$REPO_ROOT/server/ui-dist" +bash "$REPO_ROOT/scripts/prepare-server-ui-dist.sh" # Bundle skills into packages that need them (adapters + server) for pkg_dir in server packages/adapters/claude-local packages/adapters/codex-local; do diff --git a/server/package.json b/server/package.json index 3e74286b..5c37c211 100644 --- a/server/package.json +++ b/server/package.json @@ -24,7 +24,10 @@ "scripts": { "dev": "tsx src/index.ts", "dev:watch": "PAPERCLIP_MIGRATION_PROMPT=never tsx watch --ignore ../ui/node_modules --ignore ../ui/.vite --ignore ../ui/dist src/index.ts", + "prepare:ui-dist": "bash ../scripts/prepare-server-ui-dist.sh", "build": "tsc", + "prepack": "pnpm run prepare:ui-dist", + "postpack": "rm -rf ui-dist", "clean": "rm -rf dist", "start": "node dist/index.js", "typecheck": "tsc --noEmit" diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 6335f02c..10d0709b 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -7,6 +7,7 @@ import { issuesApi } from "../api/issues"; import { queryKeys } from "../lib/queryKeys"; import { groupBy } from "../lib/groupBy"; import { formatDate, cn } from "../lib/utils"; +import { timeAgo } from "../lib/timeAgo"; import { StatusIcon } from "./StatusIcon"; import { PriorityIcon } from "./PriorityIcon"; import { EmptyState } from "./EmptyState"; @@ -17,7 +18,7 @@ import { Input } from "@/components/ui/input"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; -import { CircleDot, Plus, Filter, ArrowUpDown, Layers, Check, X, ChevronRight, List, Columns3, User, Search, ArrowDown } from "lucide-react"; +import { CircleDot, Plus, Filter, ArrowUpDown, Layers, Check, X, ChevronRight, List, Columns3, User, Search } from "lucide-react"; import { KanbanBoard } from "./KanbanBoard"; import type { Issue } from "@paperclipai/shared"; @@ -233,24 +234,6 @@ export function IssuesList({ const activeFilterCount = countActiveFilters(viewState); - const [showScrollBottom, setShowScrollBottom] = useState(false); - useEffect(() => { - const el = document.getElementById("main-content"); - if (!el) return; - const check = () => { - const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; - setShowScrollBottom(distanceFromBottom > 300); - }; - check(); - el.addEventListener("scroll", check, { passive: true }); - return () => el.removeEventListener("scroll", check); - }, [filtered.length]); - - const scrollToBottom = useCallback(() => { - const el = document.getElementById("main-content"); - if (el) el.scrollTo({ top: el.scrollHeight, behavior: "smooth" }); - }, []); - const groupedContent = useMemo(() => { if (viewState.groupBy === "none") { return [{ key: "__all", label: null as string | null, items: filtered }]; @@ -608,149 +591,163 @@ export function IssuesList({ - {/* Spacer matching caret width so status icon aligns with group title (hidden on mobile) */} -
-- {issue.title} +