fix(packages): repack npm fallback without scripts

Create sanitized npm tarballs directly in Go so workspace dependency fallback does not run package lifecycle scripts or create global symlinks to temporary folders.
This commit is contained in:
Goon committed 2026-05-18 16:04:12 +07:00
1 parent d8fdf8fcd3
commit 99460a7f8a
2 files changed
+168 -3

No files matched your search

+124 -3
View File
@@ -1,11 +1,15 @@
package skills
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"os"
"os/exec"
@@ -79,13 +83,13 @@ func installNpmPackageWithWorkspaceRewrite(ctx context.Context, target string) (
if err := os.MkdirAll(repackDir, 0o750); err != nil {
return packOut, fmt.Errorf("npm workspace fallback repack dir: %w", err)
}
sanitizedTarball, repackOut, err := npmPackTarball(ctx, packageDir, repackDir)
sanitizedTarball, err := packNpmPackageDir(packageDir, repackDir)
if err != nil {
return appendNpmFallbackOutput(packOut, repackOut), err
return packOut, err
}
installOut, err := runNpmInstall(ctx, sanitizedTarball)
return appendNpmFallbackOutput(packOut, repackOut, installOut), err
return appendNpmFallbackOutput(packOut, installOut), err
}
func npmPackTarball(ctx context.Context, target, destination string) (string, []byte, error) {
@@ -200,6 +204,123 @@ func npmViewPackageVersion(ctx context.Context, name string) (string, error) {
return version, nil
}
func packNpmPackageDir(packageDir, destination string) (string, error) {
raw, err := os.ReadFile(filepath.Join(packageDir, "package.json"))
if err != nil {
return "", fmt.Errorf("read package.json for repack: %w", err)
}
var pkg struct {
Name string `json:"name"`
Version string `json:"version"`
}
if err := json.Unmarshal(raw, &pkg); err != nil {
return "", fmt.Errorf("parse package.json for repack: %w", err)
}
filename := npmTarballFilename(pkg.Name, pkg.Version)
if filename == "" {
return "", errors.New("npm workspace fallback cannot derive tarball filename")
}
tarball := filepath.Join(destination, filename)
out, err := os.OpenFile(tarball, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
return "", fmt.Errorf("create sanitized npm tarball: %w", err)
}
defer out.Close()
gz := gzip.NewWriter(out)
tw := tar.NewWriter(gz)
walkErr := filepath.WalkDir(packageDir, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("npm workspace fallback unsupported package entry: %s", path)
}
rel, err := filepath.Rel(packageDir, path)
if err != nil {
return err
}
hdr, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
hdr.Name = filepath.ToSlash(filepath.Join("package", rel))
hdr.Mode = int64(info.Mode().Perm())
if err := tw.WriteHeader(hdr); err != nil {
return err
}
in, err := os.Open(path)
if err != nil {
return err
}
_, copyErr := io.Copy(tw, in)
closeErr := in.Close()
if copyErr != nil {
return copyErr
}
return closeErr
})
closeTarErr := tw.Close()
closeGzipErr := gz.Close()
if walkErr != nil {
return "", fmt.Errorf("pack sanitized npm tarball: %w", walkErr)
}
if closeTarErr != nil {
return "", fmt.Errorf("close sanitized npm tarball: %w", closeTarErr)
}
if closeGzipErr != nil {
return "", fmt.Errorf("close sanitized npm gzip: %w", closeGzipErr)
}
return tarball, nil
}
func npmTarballFilename(name, version string) string {
name = strings.TrimSpace(name)
version = strings.TrimSpace(version)
if name == "" || version == "" {
return ""
}
name = strings.TrimPrefix(name, "@")
name = strings.ReplaceAll(name, "/", "-")
name = strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z':
return r
case r >= 'A' && r <= 'Z':
return r
case r >= '0' && r <= '9':
return r
case r == '.' || r == '_' || r == '-':
return r
default:
return '-'
}
}, name)
version = strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z':
return r
case r >= 'A' && r <= 'Z':
return r
case r >= '0' && r <= '9':
return r
case r == '.' || r == '_' || r == '-':
return r
default:
return '-'
}
}, version)
return name + "-" + version + ".tgz"
}
func extractNpmTarballToDir(tarball, destination string) error {
files, err := ExtractArchiveAs(tarball, filepath.Base(tarball), 50*1024*1024)
if err != nil {
@@ -106,3 +106,47 @@ func TestRewriteWorkspacePackageJSON(t *testing.T) {
t.Fatalf("ws = %q, want unchanged", got)
}
}
func TestPackNpmPackageDirDoesNotNeedNpmScripts(t *testing.T) {
dir := t.TempDir()
packageDir := filepath.Join(dir, "package")
if err := os.MkdirAll(packageDir, 0o750); err != nil {
t.Fatal(err)
}
raw := []byte(`{
"name": "@agenttasks/cli",
"version": "0.1.0",
"scripts": {
"prepack": "exit 127"
}
}`)
if err := os.WriteFile(filepath.Join(packageDir, "package.json"), raw, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(packageDir, "index.js"), []byte("console.log('ok')\n"), 0o600); err != nil {
t.Fatal(err)
}
tarball, err := packNpmPackageDir(packageDir, dir)
if err != nil {
t.Fatalf("packNpmPackageDir error: %v", err)
}
if filepath.Base(tarball) != "agenttasks-cli-0.1.0.tgz" {
t.Fatalf("tarball name = %q", filepath.Base(tarball))
}
files, err := ExtractArchiveAs(tarball, filepath.Base(tarball), 1024*1024)
if err != nil {
t.Fatalf("extract sanitized tarball: %v", err)
}
seenPackageJSON := false
for _, file := range files {
if file.Name == "package/package.json" {
seenPackageJSON = true
break
}
}
if !seenPackageJSON {
t.Fatal("sanitized tarball missing package/package.json")
}
}