mirror of
https://github.com/paperclipai/paperclip
synced 2026-04-25 17:25:15 +02:00
chore: improve worktree tooling and security docs
This commit is contained in:
126
scripts/ensure-workspace-package-links.ts
Normal file
126
scripts/ensure-workspace-package-links.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
import fs from "node:fs/promises";
|
||||
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { repoRoot } from "./dev-service-profile.ts";
|
||||
|
||||
type WorkspaceLinkMismatch = {
|
||||
workspaceDir: string;
|
||||
packageName: string;
|
||||
expectedPath: string;
|
||||
actualPath: string | null;
|
||||
};
|
||||
|
||||
function readJsonFile(filePath: string): Record<string, unknown> {
|
||||
return JSON.parse(readFileSync(filePath, "utf8")) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function discoverWorkspacePackagePaths(rootDir: string): Map<string, string> {
|
||||
const packagePaths = new Map<string, string>();
|
||||
const ignoredDirNames = new Set([".git", ".paperclip", "dist", "node_modules"]);
|
||||
|
||||
function visit(dirPath: string) {
|
||||
const packageJsonPath = path.join(dirPath, "package.json");
|
||||
if (existsSync(packageJsonPath)) {
|
||||
const packageJson = readJsonFile(packageJsonPath);
|
||||
if (typeof packageJson.name === "string" && packageJson.name.length > 0) {
|
||||
packagePaths.set(packageJson.name, dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (ignoredDirNames.has(entry.name)) continue;
|
||||
visit(path.join(dirPath, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
visit(path.join(rootDir, "packages"));
|
||||
visit(path.join(rootDir, "server"));
|
||||
visit(path.join(rootDir, "ui"));
|
||||
visit(path.join(rootDir, "cli"));
|
||||
|
||||
return packagePaths;
|
||||
}
|
||||
|
||||
function isLinkedGitWorktreeCheckout(rootDir: string) {
|
||||
const gitMetadataPath = path.join(rootDir, ".git");
|
||||
if (!existsSync(gitMetadataPath)) return false;
|
||||
|
||||
const stat = lstatSync(gitMetadataPath);
|
||||
if (!stat.isFile()) return false;
|
||||
|
||||
return readFileSync(gitMetadataPath, "utf8").trimStart().startsWith("gitdir:");
|
||||
}
|
||||
|
||||
if (!isLinkedGitWorktreeCheckout(repoRoot)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const workspacePackagePaths = discoverWorkspacePackagePaths(repoRoot);
|
||||
const workspaceDirs = Array.from(
|
||||
new Set(
|
||||
Array.from(workspacePackagePaths.values())
|
||||
.map((packagePath) => path.relative(repoRoot, packagePath))
|
||||
.filter((workspaceDir) => workspaceDir.length > 0),
|
||||
),
|
||||
).sort();
|
||||
|
||||
function findWorkspaceLinkMismatches(workspaceDir: string): WorkspaceLinkMismatch[] {
|
||||
const packageJson = readJsonFile(path.join(repoRoot, workspaceDir, "package.json"));
|
||||
const dependencies = {
|
||||
...(packageJson.dependencies as Record<string, unknown> | undefined),
|
||||
...(packageJson.devDependencies as Record<string, unknown> | undefined),
|
||||
};
|
||||
const mismatches: WorkspaceLinkMismatch[] = [];
|
||||
|
||||
for (const [packageName, version] of Object.entries(dependencies)) {
|
||||
if (typeof version !== "string" || !version.startsWith("workspace:")) continue;
|
||||
|
||||
const expectedPath = workspacePackagePaths.get(packageName);
|
||||
if (!expectedPath) continue;
|
||||
|
||||
const linkPath = path.join(repoRoot, workspaceDir, "node_modules", ...packageName.split("/"));
|
||||
const actualPath = existsSync(linkPath) ? path.resolve(realpathSync(linkPath)) : null;
|
||||
if (actualPath === path.resolve(expectedPath)) continue;
|
||||
|
||||
mismatches.push({
|
||||
workspaceDir,
|
||||
packageName,
|
||||
expectedPath: path.resolve(expectedPath),
|
||||
actualPath,
|
||||
});
|
||||
}
|
||||
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
async function ensureWorkspaceLinksCurrent(workspaceDir: string) {
|
||||
const mismatches = findWorkspaceLinkMismatches(workspaceDir);
|
||||
if (mismatches.length === 0) return;
|
||||
|
||||
console.log(`[paperclip] detected stale workspace package links for ${workspaceDir}; relinking dependencies...`);
|
||||
for (const mismatch of mismatches) {
|
||||
console.log(
|
||||
`[paperclip] ${mismatch.packageName}: ${mismatch.actualPath ?? "missing"} -> ${mismatch.expectedPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const mismatch of mismatches) {
|
||||
const linkPath = path.join(repoRoot, mismatch.workspaceDir, "node_modules", ...mismatch.packageName.split("/"));
|
||||
await fs.mkdir(path.dirname(linkPath), { recursive: true });
|
||||
await fs.rm(linkPath, { recursive: true, force: true });
|
||||
await fs.symlink(mismatch.expectedPath, linkPath);
|
||||
}
|
||||
|
||||
const remainingMismatches = findWorkspaceLinkMismatches(workspaceDir);
|
||||
if (remainingMismatches.length === 0) return;
|
||||
|
||||
throw new Error(
|
||||
`Workspace relink did not repair all ${workspaceDir} package links: ${remainingMismatches.map((item) => item.packageName).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const workspaceDir of workspaceDirs) {
|
||||
await ensureWorkspaceLinksCurrent(workspaceDir);
|
||||
}
|
||||
110
scripts/paperclip-issue-update.sh
Executable file
110
scripts/paperclip-issue-update.sh
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/paperclip-issue-update.sh [--issue-id ID] [--status STATUS] [--comment TEXT] [--dry-run]
|
||||
|
||||
Reads a multiline markdown comment from stdin when stdin is piped. This preserves
|
||||
newlines when building the JSON payload for PATCH /api/issues/{issueId}.
|
||||
|
||||
Examples:
|
||||
scripts/paperclip-issue-update.sh --issue-id "$PAPERCLIP_TASK_ID" --status in_progress <<'MD'
|
||||
Investigating formatting
|
||||
|
||||
- Pulled the raw comment body
|
||||
- Comparing it with the run transcript
|
||||
MD
|
||||
|
||||
scripts/paperclip-issue-update.sh --issue-id "$PAPERCLIP_TASK_ID" --status done --dry-run <<'MD'
|
||||
Done
|
||||
|
||||
- Fixed the issue update helper
|
||||
MD
|
||||
EOF
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
printf 'Missing required command: %s\n' "$1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
issue_id="${PAPERCLIP_TASK_ID:-}"
|
||||
status=""
|
||||
comment_arg=""
|
||||
dry_run=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--issue-id)
|
||||
issue_id="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--status)
|
||||
status="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--comment)
|
||||
comment_arg="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf 'Unknown argument: %s\n' "$1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$issue_id" ]]; then
|
||||
printf 'Missing issue id. Pass --issue-id or set PAPERCLIP_TASK_ID.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
comment=""
|
||||
if [[ -n "$comment_arg" ]]; then
|
||||
comment="$comment_arg"
|
||||
elif [[ ! -t 0 ]]; then
|
||||
comment="$(cat)"
|
||||
fi
|
||||
|
||||
require_command jq
|
||||
|
||||
payload="$(
|
||||
jq -nc \
|
||||
--arg status "$status" \
|
||||
--arg comment "$comment" \
|
||||
'
|
||||
(if $status == "" then {} else {status: $status} end) +
|
||||
(if $comment == "" then {} else {comment: $comment} end)
|
||||
'
|
||||
)"
|
||||
|
||||
if [[ "$dry_run" == "1" ]]; then
|
||||
printf '%s\n' "$payload"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "${PAPERCLIP_API_URL:-}" || -z "${PAPERCLIP_API_KEY:-}" || -z "${PAPERCLIP_RUN_ID:-}" ]]; then
|
||||
printf 'Missing PAPERCLIP_API_URL, PAPERCLIP_API_KEY, or PAPERCLIP_RUN_ID.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -sS -X PATCH \
|
||||
"$PAPERCLIP_API_URL/api/issues/$issue_id" \
|
||||
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
||||
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary "$payload"
|
||||
@@ -321,20 +321,6 @@ if ! run_isolated_worktree_init; then
|
||||
write_fallback_worktree_config
|
||||
fi
|
||||
|
||||
disable_seeded_routines() {
|
||||
local company_id="${PAPERCLIP_COMPANY_ID:-}"
|
||||
if [[ -z "$company_id" ]]; then
|
||||
echo "PAPERCLIP_COMPANY_ID not set; skipping routine disable post-step." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! run_paperclipai_command routines disable-all --config "$worktree_config_path" --company-id "$company_id"; then
|
||||
echo "paperclipai CLI not available in this workspace; skipping routine disable post-step." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
disable_seeded_routines
|
||||
|
||||
list_base_node_modules_paths() {
|
||||
cd "$base_cwd" &&
|
||||
find . \
|
||||
|
||||
Reference in New Issue
Block a user