diff --git a/actions/prototype_list.go b/actions/prototype_list.go
new file mode 100644
index 0000000000000000000000000000000000000000..99e0c1bc25ef190517753559ae6b44a55ab5edda
--- /dev/null
+++ b/actions/prototype_list.go
@@ -0,0 +1,101 @@
+// Copyright 2018 The ksonnet authors
+//
+//
+//    Licensed under the Apache License, Version 2.0 (the "License");
+//    you may not use this file except in compliance with the License.
+//    You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+//    Upless required by applicable law or agreed to in writing, software
+//    distributed under the License is distributed on an "AS IS" BASIS,
+//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//    See the License for the specific language governing permissions and
+//    limitations under the License.
+
+package actions
+
+import (
+	"io"
+	"os"
+	"sort"
+
+	"github.com/ksonnet/ksonnet/metadata/app"
+	"github.com/ksonnet/ksonnet/pkg/pkg"
+	"github.com/ksonnet/ksonnet/pkg/util/table"
+	"github.com/ksonnet/ksonnet/prototype"
+)
+
+// RunPrototypeList runs `prototype list`
+func RunPrototypeList(ksApp app.App) error {
+	pl, err := NewPrototypeList(ksApp)
+	if err != nil {
+		return err
+	}
+
+	return pl.Run()
+}
+
+// PrototypeList lists available namespaces
+type PrototypeList struct {
+	app        app.App
+	out        io.Writer
+	prototypes func(app.App, pkg.Descriptor) (prototype.SpecificationSchemas, error)
+}
+
+// NewPrototypeList creates an instance of PrototypeList
+func NewPrototypeList(ksApp app.App) (*PrototypeList, error) {
+	pl := &PrototypeList{
+		app:        ksApp,
+		out:        os.Stdout,
+		prototypes: pkg.LoadPrototypes,
+	}
+
+	return pl, nil
+}
+
+// Run runs the env list action.
+func (pl *PrototypeList) Run() error {
+	libraries, err := pl.app.Libraries()
+	if err != nil {
+		return err
+	}
+
+	var prototypes prototype.SpecificationSchemas
+
+	for _, library := range libraries {
+		d := pkg.Descriptor{
+			Registry: library.Registry,
+			Part:     library.Name,
+		}
+
+		p, err := pl.prototypes(pl.app, d)
+		if err != nil {
+			return err
+		}
+
+		prototypes = append(prototypes, p...)
+	}
+
+	index := prototype.NewIndex(prototypes)
+	prototypes, err = index.List()
+	if err != nil {
+		return nil
+	}
+
+	var rows [][]string
+	for _, p := range prototypes {
+		rows = append(rows, []string{p.Name, p.Template.ShortDescription})
+	}
+
+	t := table.New(pl.out)
+	t.SetHeader([]string{"name", "description"})
+
+	sort.Slice(rows, func(i, j int) bool {
+		return rows[i][0] < rows[j][0]
+	})
+
+	t.AppendBulk(rows)
+
+	return t.Render()
+}
diff --git a/actions/prototype_list_test.go b/actions/prototype_list_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..618e8b8b2ec5083abc9723b6a3e64c7465f15f62
--- /dev/null
+++ b/actions/prototype_list_test.go
@@ -0,0 +1,44 @@
+// Copyright 2018 The ksonnet authors
+//
+//
+//    Licensed under the Apache License, Version 2.0 (the "License");
+//    you may not use this file except in compliance with the License.
+//    You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+//    Unless required by applicable law or agreed to in writing, software
+//    distributed under the License is distributed on an "AS IS" BASIS,
+//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//    See the License for the specific language governing permissions and
+//    limitations under the License.
+
+package actions
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/ksonnet/ksonnet/metadata/app"
+	amocks "github.com/ksonnet/ksonnet/metadata/app/mocks"
+	"github.com/stretchr/testify/require"
+)
+
+func TestPrototypeList(t *testing.T) {
+	withApp(t, func(appMock *amocks.App) {
+		libaries := app.LibraryRefSpecs{}
+
+		appMock.On("Libraries").Return(libaries, nil)
+
+		a, err := NewPrototypeList(appMock)
+		require.NoError(t, err)
+
+		var buf bytes.Buffer
+		a.out = &buf
+
+		err = a.Run()
+		require.NoError(t, err)
+
+		assertOutput(t, "prototype/list/output.txt", buf.String())
+	})
+}
diff --git a/actions/testdata/prototype/list/output.txt b/actions/testdata/prototype/list/output.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1c060c2b52caf0184c8f9a335bf495b7c37a33c5
--- /dev/null
+++ b/actions/testdata/prototype/list/output.txt
@@ -0,0 +1,7 @@
+NAME                                  DESCRIPTION
+====                                  ===========
+io.ksonnet.pkg.configMap              A simple config map with optional user-specified data
+io.ksonnet.pkg.deployed-service       A deployment exposed with a service
+io.ksonnet.pkg.namespace              Namespace with labels automatically populated from the name
+io.ksonnet.pkg.single-port-deployment Replicates a container n times, exposes a single port
+io.ksonnet.pkg.single-port-service    Service that exposes a single port
diff --git a/cmd/prototype.go b/cmd/prototype.go
index cb4faaddcadb13d517beb72fdb843a41ae7036e5..5985824cb8994d6afad22325eaedfed0cb465301 100644
--- a/cmd/prototype.go
+++ b/cmd/prototype.go
@@ -43,7 +43,6 @@ var protoShortDesc = map[string]string{
 func init() {
 	RootCmd.AddCommand(prototypeCmd)
 	RootCmd.AddCommand(generateCmd)
-	prototypeCmd.AddCommand(prototypeListCmd)
 	prototypeCmd.AddCommand(prototypeDescribeCmd)
 	prototypeCmd.AddCommand(prototypeSearchCmd)
 	prototypeCmd.AddCommand(prototypeUseCmd)
@@ -77,61 +76,6 @@ for your use case.
 `,
 }
 
-var prototypeListCmd = &cobra.Command{
-	Use:   "list",
-	Short: protoShortDesc["list"],
-	RunE: func(cmd *cobra.Command, args []string) error {
-		if len(args) != 0 {
-			return fmt.Errorf("Command 'prototype list' does not take any arguments")
-		}
-
-		cwd, err := os.Getwd()
-		if err != nil {
-			return err
-		}
-
-		manager, err := metadata.Find(cwd)
-		if err != nil {
-			return err
-		}
-
-		extProtos, err := manager.GetAllPrototypes()
-		if err != nil {
-			return err
-		}
-
-		index := prototype.NewIndex(extProtos)
-		protos, err := index.List()
-		if err != nil {
-			return err
-		} else if len(protos) == 0 {
-			return fmt.Errorf("No prototypes found")
-		}
-
-		fmt.Print(protos)
-
-		return nil
-	},
-	Long: `
-The ` + "`list`" + ` command displays all prototypes that are available locally, as
-well as brief descriptions of what they generate.
-
-ksonnet comes with a set of system prototypes that you can use out-of-the-box
-(e.g.` + " `io.ksonnet.pkg.configMap`" + `). However, you can use more advanced
-prototypes like ` + "`io.ksonnet.pkg.redis-stateless`" + ` by downloading extra packages
-from the *incubator* registry.
-
-### Related Commands
-
-* ` + "`ks prototype describe` " + `— ` + protoShortDesc["describe"] + `
-* ` + "`ks prototype preview` " + `— ` + protoShortDesc["preview"] + `
-* ` + "`ks prototype use` " + `— ` + protoShortDesc["use"] + `
-* ` + "`ks pkg install` " + pkgShortDesc["install"] + `
-
-### Syntax
-`,
-}
-
 var prototypeDescribeCmd = &cobra.Command{
 	Use:   "describe <prototype-name>",
 	Short: protoShortDesc["describe"],
diff --git a/cmd/prototype_list.go b/cmd/prototype_list.go
new file mode 100644
index 0000000000000000000000000000000000000000..da358d407ea8cea182b72d53e5c348571ec24058
--- /dev/null
+++ b/cmd/prototype_list.go
@@ -0,0 +1,42 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/ksonnet/ksonnet/actions"
+	"github.com/spf13/cobra"
+)
+
+var prototypeListCmd = &cobra.Command{
+	Use:   "list",
+	Short: protoShortDesc["list"],
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if len(args) != 0 {
+			return fmt.Errorf("Command 'prototype list' does not take any arguments")
+		}
+
+		return actions.RunPrototypeList(ka)
+	},
+	Long: `
+The ` + "`list`" + ` command displays all prototypes that are available locally, as
+well as brief descriptions of what they generate.
+
+ksonnet comes with a set of system prototypes that you can use out-of-the-box
+(e.g.` + " `io.ksonnet.pkg.configMap`" + `). However, you can use more advanced
+prototypes like ` + "`io.ksonnet.pkg.redis-stateless`" + ` by downloading extra packages
+from the *incubator* registry.
+
+### Related Commands
+
+* ` + "`ks prototype describe` " + `— ` + protoShortDesc["describe"] + `
+* ` + "`ks prototype preview` " + `— ` + protoShortDesc["preview"] + `
+* ` + "`ks prototype use` " + `— ` + protoShortDesc["use"] + `
+* ` + "`ks pkg install` " + pkgShortDesc["install"] + `
+
+### Syntax
+`,
+}
+
+func init() {
+	prototypeCmd.AddCommand(prototypeListCmd)
+}
diff --git a/e2e/prototype_test.go b/e2e/prototype_test.go
index e54967b6be749019fb38f8fc5d10eedae278f1cb..cadfbd704bfd4677a568a57b844163e8e06a783d 100644
--- a/e2e/prototype_test.go
+++ b/e2e/prototype_test.go
@@ -48,10 +48,20 @@ var _ = Describe("ks prototype", func() {
 	})
 
 	Describe("list", func() {
-		It("lists available prototypes", func() {
-			o := a.runKs("prototype", "list")
-			assertExitStatus(o, 0)
-			assertOutput("prototype/list/output.txt", o.stdout)
+		Context("with system prototypes", func() {
+			It("lists available prototypes", func() {
+				o := a.runKs("prototype", "list")
+				assertExitStatus(o, 0)
+				assertOutput("prototype/list/output.txt", o.stdout)
+			})
+		})
+		Context("with a part installed", func() {
+			It("lists available prototypes", func() {
+				a.pkgInstall("incubator/apache")
+				o := a.runKs("prototype", "list")
+				assertExitStatus(o, 0)
+				assertOutput("prototype/list/output-with-addition.txt", o.stdout)
+			})
 		})
 	})
 
diff --git a/e2e/testdata/output/prototype/list/output-with-addition.txt b/e2e/testdata/output/prototype/list/output-with-addition.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4fae5015a149170b24480e96594c04b4c297ca30
--- /dev/null
+++ b/e2e/testdata/output/prototype/list/output-with-addition.txt
@@ -0,0 +1,8 @@
+NAME                                  DESCRIPTION
+====                                  ===========
+io.ksonnet.pkg.apache-simple          A simple, stateless Apache HTTP server.
+io.ksonnet.pkg.configMap              A simple config map with optional user-specified data
+io.ksonnet.pkg.deployed-service       A deployment exposed with a service
+io.ksonnet.pkg.namespace              Namespace with labels automatically populated from the name
+io.ksonnet.pkg.single-port-deployment Replicates a container n times, exposes a single port
+io.ksonnet.pkg.single-port-service    Service that exposes a single port
diff --git a/pkg/pkg/pkg.go b/pkg/pkg/pkg.go
index dae545c5514c11ad49ea43f280d543e03d2f3526..9b8ff1c3df35c3bce41a3b0e169278a942c87cc6 100644
--- a/pkg/pkg/pkg.go
+++ b/pkg/pkg/pkg.go
@@ -36,7 +36,7 @@ type Package struct {
 
 // New creates a new new instance of Package using a part.
 func New(a app.App, d Descriptor, part *parts.Spec) (*Package, error) {
-	prototypes, err := loadPrototypes(a, d)
+	prototypes, err := LoadPrototypes(a, d)
 	if err != nil {
 		return nil, err
 	}
@@ -60,8 +60,8 @@ func NewFromData(a app.App, d Descriptor, data []byte) (*Package, error) {
 	return New(a, d, part)
 }
 
-// loadPrototypes returns prototypes for a Package.
-func loadPrototypes(a app.App, d Descriptor) (prototype.SpecificationSchemas, error) {
+// LoadPrototypes returns prototypes for a Package.
+func LoadPrototypes(a app.App, d Descriptor) (prototype.SpecificationSchemas, error) {
 	vp := vendorPath(a)
 
 	var prototypes prototype.SpecificationSchemas