diff --git a/cmd/component.go b/cmd/component.go
new file mode 100644
index 0000000000000000000000000000000000000000..9fae44c9bc01f5e9960d12f1438e271c04a9857c
--- /dev/null
+++ b/cmd/component.go
@@ -0,0 +1,64 @@
+// Copyright 2017 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 cmd
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/spf13/cobra"
+
+	"github.com/ksonnet/ksonnet/pkg/kubecfg"
+)
+
+func init() {
+	RootCmd.AddCommand(componentCmd)
+
+	componentCmd.AddCommand(componentListCmd)
+}
+
+var componentCmd = &cobra.Command{
+	Use:   "component",
+	Short: "Manage ksonnet components",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if len(args) != 0 {
+			return fmt.Errorf("%s is not a valid subcommand\n\n%s", strings.Join(args, " "), cmd.UsageString())
+		}
+		return fmt.Errorf("Command 'component' requires a subcommand\n\n%s", cmd.UsageString())
+	},
+}
+
+var componentListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "List known components",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if len(args) != 0 {
+			return fmt.Errorf("'component list' takes zero arguments")
+		}
+
+		c := kubecfg.NewComponentListCmd()
+
+		return c.Run(cmd.OutOrStdout())
+	},
+	Long: `
+The ` + "`list`" + ` command displays all known components.
+
+### Syntax
+`,
+	Example: `
+# List all components
+ks component list`,
+}
diff --git a/docs/cli-reference/ks.md b/docs/cli-reference/ks.md
index 043fd039b998372d8632c04efdc46614692c19d8..23cd79afaf38f1ab9dfbdc0d3c8912989824a3c6 100644
--- a/docs/cli-reference/ks.md
+++ b/docs/cli-reference/ks.md
@@ -20,6 +20,7 @@ application configuration to remote clusters.
 
 ### SEE ALSO
 * [ks apply](ks_apply.md)	 - Apply local Kubernetes manifests (components) to remote clusters
+* [ks component](ks_component.md)	 - Manage ksonnet components
 * [ks delete](ks_delete.md)	 - Remove component-specified Kubernetes resources from remote clusters
 * [ks diff](ks_diff.md)	 - Compare manifests, based on environment or location (local or remote)
 * [ks env](ks_env.md)	 - Manage ksonnet environments
diff --git a/docs/cli-reference/ks_component.md b/docs/cli-reference/ks_component.md
new file mode 100644
index 0000000000000000000000000000000000000000..2a860d9dd9fa8b7ab7e7f745dd6960ef54624ce5
--- /dev/null
+++ b/docs/cli-reference/ks_component.md
@@ -0,0 +1,23 @@
+## ks component
+
+Manage ksonnet components
+
+### Synopsis
+
+
+Manage ksonnet components
+
+```
+ks component
+```
+
+### Options inherited from parent commands
+
+```
+  -v, --verbose count[=-1]   Increase verbosity. May be given multiple times.
+```
+
+### SEE ALSO
+* [ks](ks.md)	 - Configure your application to deploy to a Kubernetes cluster
+* [ks component list](ks_component_list.md)	 - List known components
+
diff --git a/docs/cli-reference/ks_component_list.md b/docs/cli-reference/ks_component_list.md
new file mode 100644
index 0000000000000000000000000000000000000000..0f7722cb4c9e9f69176c80f2d19587a8b373e648
--- /dev/null
+++ b/docs/cli-reference/ks_component_list.md
@@ -0,0 +1,34 @@
+## ks component list
+
+List known components
+
+### Synopsis
+
+
+
+The `list` command displays all known components.
+
+### Syntax
+
+
+```
+ks component list
+```
+
+### Examples
+
+```
+
+# List all components
+ks component list
+```
+
+### Options inherited from parent commands
+
+```
+  -v, --verbose count[=-1]   Increase verbosity. May be given multiple times.
+```
+
+### SEE ALSO
+* [ks component](ks_component.md)	 - Manage ksonnet components
+
diff --git a/metadata/component.go b/metadata/component.go
new file mode 100644
index 0000000000000000000000000000000000000000..e8a6f6611d65e87afbc5949cbc42428d658f3455
--- /dev/null
+++ b/metadata/component.go
@@ -0,0 +1,96 @@
+// Copyright 2017 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 metadata
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"strings"
+
+	param "github.com/ksonnet/ksonnet/metadata/params"
+	"github.com/ksonnet/ksonnet/prototype"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+)
+
+func (m *manager) ComponentPaths() (AbsPaths, error) {
+	paths := AbsPaths{}
+	err := afero.Walk(m.appFS, string(m.componentsPath), func(p string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		// Only add file paths and exclude the params.libsonnet file
+		if !info.IsDir() && path.Base(p) != componentParamsFile {
+			paths = append(paths, p)
+		}
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return paths, nil
+}
+
+func (m *manager) GetAllComponents() ([]string, error) {
+	componentPaths, err := m.ComponentPaths()
+	if err != nil {
+		return nil, err
+	}
+
+	var components []string
+	for _, p := range componentPaths {
+		component := strings.TrimSuffix(path.Base(p), path.Ext(p))
+		components = append(components, component)
+	}
+
+	return components, nil
+}
+
+func (m *manager) CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error {
+	if !isValidName(name) || strings.Contains(name, "/") {
+		return fmt.Errorf("Component name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
+	}
+
+	componentPath := string(appendToAbsPath(m.componentsPath, name))
+	switch templateType {
+	case prototype.YAML:
+		componentPath = componentPath + ".yaml"
+	case prototype.JSON:
+		componentPath = componentPath + ".json"
+	case prototype.Jsonnet:
+		componentPath = componentPath + ".jsonnet"
+	default:
+		return fmt.Errorf("Unrecognized prototype template type '%s'", templateType)
+	}
+
+	if exists, err := afero.Exists(m.appFS, componentPath); exists {
+		return fmt.Errorf("Component with name '%s' already exists", name)
+	} else if err != nil {
+		return fmt.Errorf("Could not check whether component '%s' exists:\n\n%v", name, err)
+	}
+
+	log.Infof("Writing component at '%s/%s'", componentsDir, name)
+	err := afero.WriteFile(m.appFS, componentPath, []byte(text), defaultFilePermissions)
+	if err != nil {
+		return err
+	}
+
+	log.Debugf("Writing component parameters at '%s/%s", componentsDir, name)
+	return m.writeComponentParams(name, params)
+}
diff --git a/metadata/component_test.go b/metadata/component_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c4a244a507b125f97e2c57454c6128323c5eef13
--- /dev/null
+++ b/metadata/component_test.go
@@ -0,0 +1,123 @@
+// Copyright 2017 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 metadata
+
+import (
+	"fmt"
+	"os"
+	"sort"
+	"strings"
+	"testing"
+)
+
+const (
+	componentsPath  = "/componentsPath"
+	componentSubdir = "subdir"
+	componentFile1  = "component1.jsonnet"
+	componentFile2  = "component2.jsonnet"
+)
+
+func populateComponentPaths(t *testing.T) *manager {
+	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
+	if err != nil {
+		t.Fatalf("Failed to parse cluster spec: %v", err)
+	}
+
+	appPath := AbsPath(componentsPath)
+	reg := newMockRegistryManager("incubator")
+	m, err := initManager("componentPaths", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS)
+	if err != nil {
+		t.Fatalf("Failed to init cluster spec: %v", err)
+	}
+
+	// Create empty app file.
+	components := appendToAbsPath(appPath, componentsDir)
+	appFile1 := appendToAbsPath(components, componentFile1)
+	f1, err := testFS.OpenFile(string(appFile1), os.O_RDONLY|os.O_CREATE, 0777)
+	if err != nil {
+		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
+	}
+	f1.Close()
+
+	// Create empty file in a nested directory.
+	appSubdir := appendToAbsPath(components, componentSubdir)
+	err = testFS.MkdirAll(string(appSubdir), os.ModePerm)
+	if err != nil {
+		t.Fatalf("Failed to create directory '%s'\n%v", appSubdir, err)
+	}
+	appFile2 := appendToAbsPath(appSubdir, componentFile2)
+	f2, err := testFS.OpenFile(string(appFile2), os.O_RDONLY|os.O_CREATE, 0777)
+	if err != nil {
+		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
+	}
+	f2.Close()
+
+	// Create a directory that won't be listed in the call to `ComponentPaths`.
+	unlistedDir := string(appendToAbsPath(components, "doNotListMe"))
+	err = testFS.MkdirAll(unlistedDir, os.ModePerm)
+	if err != nil {
+		t.Fatalf("Failed to create directory '%s'\n%v", unlistedDir, err)
+	}
+
+	return m
+}
+
+func cleanComponentPaths(t *testing.T) {
+	testFS.RemoveAll(componentsPath)
+}
+
+func TestComponentPaths(t *testing.T) {
+	m := populateComponentPaths(t)
+	defer cleanComponentPaths(t)
+
+	paths, err := m.ComponentPaths()
+	if err != nil {
+		t.Fatalf("Failed to find component paths: %v", err)
+	}
+
+	sort.Slice(paths, func(i, j int) bool { return paths[i] < paths[j] })
+
+	expectedPath1 := fmt.Sprintf("%s/components/%s", componentsPath, componentFile1)
+	expectedPath2 := fmt.Sprintf("%s/components/%s/%s", componentsPath, componentSubdir, componentFile2)
+
+	if len(paths) != 2 || paths[0] != expectedPath1 || paths[1] != expectedPath2 {
+		t.Fatalf("m.ComponentPaths failed; expected '%s', got '%s'", []string{expectedPath1, expectedPath2}, paths)
+	}
+}
+
+func TestGetAllComponents(t *testing.T) {
+	m := populateComponentPaths(t)
+	defer cleanComponentPaths(t)
+
+	components, err := m.GetAllComponents()
+	if err != nil {
+		t.Fatalf("Failed to get all components, %v", err)
+	}
+
+	expected1 := strings.TrimSuffix(componentFile1, ".jsonnet")
+	expected2 := strings.TrimSuffix(componentFile2, ".jsonnet")
+
+	if len(components) != 2 {
+		t.Fatalf("Expected exactly 2 components, got %d", len(components))
+	}
+
+	if components[0] != expected1 {
+		t.Fatalf("Expected component %s, got %s", expected1, components)
+	}
+
+	if components[1] != expected2 {
+		t.Fatalf("Expected component %s, got %s", expected2, components)
+	}
+}
diff --git a/metadata/interface.go b/metadata/interface.go
index 560e4faacd3d1da34c63970b77209b917b82e1af..e406a5715e3990d0b3b2927e71573dbd7c9d4d93 100644
--- a/metadata/interface.go
+++ b/metadata/interface.go
@@ -49,6 +49,7 @@ type Manager interface {
 
 	// Components API.
 	ComponentPaths() (AbsPaths, error)
+	GetAllComponents() ([]string, error)
 	CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error
 
 	// Params API.
diff --git a/metadata/manager.go b/metadata/manager.go
index 0865e10529401252c0b018d4f333cc6105113f6b..4aab3ffeff6dc5e9c25673a2ee52132107ac8ebb 100644
--- a/metadata/manager.go
+++ b/metadata/manager.go
@@ -17,17 +17,14 @@ package metadata
 
 import (
 	"fmt"
-	"os"
 	"os/user"
 	"path"
 	"path/filepath"
-	"strings"
 
 	"github.com/ghodss/yaml"
 	"github.com/ksonnet/ksonnet/metadata/app"
 	param "github.com/ksonnet/ksonnet/metadata/params"
 	"github.com/ksonnet/ksonnet/metadata/registry"
-	"github.com/ksonnet/ksonnet/prototype"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/afero"
 )
@@ -200,58 +197,6 @@ func (m *manager) Root() AbsPath {
 	return m.rootPath
 }
 
-func (m *manager) ComponentPaths() (AbsPaths, error) {
-	paths := AbsPaths{}
-	err := afero.Walk(m.appFS, string(m.componentsPath), func(path string, info os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-
-		if !info.IsDir() {
-			paths = append(paths, path)
-		}
-		return nil
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	return paths, nil
-}
-
-func (m *manager) CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error {
-	if !isValidName(name) || strings.Contains(name, "/") {
-		return fmt.Errorf("Component name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
-	}
-
-	componentPath := string(appendToAbsPath(m.componentsPath, name))
-	switch templateType {
-	case prototype.YAML:
-		componentPath = componentPath + ".yaml"
-	case prototype.JSON:
-		componentPath = componentPath + ".json"
-	case prototype.Jsonnet:
-		componentPath = componentPath + ".jsonnet"
-	default:
-		return fmt.Errorf("Unrecognized prototype template type '%s'", templateType)
-	}
-
-	if exists, err := afero.Exists(m.appFS, componentPath); exists {
-		return fmt.Errorf("Component with name '%s' already exists", name)
-	} else if err != nil {
-		return fmt.Errorf("Could not check whether component '%s' exists:\n\n%v", name, err)
-	}
-
-	log.Infof("Writing component at '%s/%s'", componentsDir, name)
-	err := afero.WriteFile(m.appFS, componentPath, []byte(text), defaultFilePermissions)
-	if err != nil {
-		return err
-	}
-
-	log.Debugf("Writing component parameters at '%s/%s", componentsDir, name)
-	return m.writeComponentParams(name, params)
-}
-
 func (m *manager) LibPaths(envName string) (libPath, vendorPath, envLibPath, envComponentPath, envParamsPath AbsPath) {
 	envPath := appendToAbsPath(m.environmentsPath, envName)
 	return m.libPath, m.vendorPath, appendToAbsPath(envPath, metadataDirName),
diff --git a/metadata/manager_test.go b/metadata/manager_test.go
index 6ea8b7b24b90c29f7cedda9874c8b0c5289f4354..72cb32d2f43aac834fd0e2b6e04f40bb817bba0d 100644
--- a/metadata/manager_test.go
+++ b/metadata/manager_test.go
@@ -19,7 +19,6 @@ import (
 	"os"
 	"os/user"
 	"path"
-	"sort"
 	"testing"
 
 	"github.com/spf13/afero"
@@ -207,60 +206,6 @@ func TestFindSuccess(t *testing.T) {
 	findSuccess(t, appPath, appFile)
 }
 
-func TestComponentPaths(t *testing.T) {
-	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
-	if err != nil {
-		t.Fatalf("Failed to parse cluster spec: %v", err)
-	}
-
-	appPath := AbsPath("/componentPaths")
-	reg := newMockRegistryManager("incubator")
-	m, err := initManager("componentPaths", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS)
-	if err != nil {
-		t.Fatalf("Failed to init cluster spec: %v", err)
-	}
-
-	// Create empty app file.
-	components := appendToAbsPath(appPath, componentsDir)
-	appFile1 := appendToAbsPath(components, "component1.jsonnet")
-	f1, err := testFS.OpenFile(string(appFile1), os.O_RDONLY|os.O_CREATE, 0777)
-	if err != nil {
-		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
-	}
-	f1.Close()
-
-	// Create empty file in a nested directory.
-	appSubdir := appendToAbsPath(components, "appSubdir")
-	err = testFS.MkdirAll(string(appSubdir), os.ModePerm)
-	if err != nil {
-		t.Fatalf("Failed to create directory '%s'\n%v", appSubdir, err)
-	}
-	appFile2 := appendToAbsPath(appSubdir, "component2.jsonnet")
-	f2, err := testFS.OpenFile(string(appFile2), os.O_RDONLY|os.O_CREATE, 0777)
-	if err != nil {
-		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
-	}
-	f2.Close()
-
-	// Create a directory that won't be listed in the call to `ComponentPaths`.
-	unlistedDir := string(appendToAbsPath(components, "doNotListMe"))
-	err = testFS.MkdirAll(unlistedDir, os.ModePerm)
-	if err != nil {
-		t.Fatalf("Failed to create directory '%s'\n%v", unlistedDir, err)
-	}
-
-	paths, err := m.ComponentPaths()
-	if err != nil {
-		t.Fatalf("Failed to find component paths: %v", err)
-	}
-
-	sort.Slice(paths, func(i, j int) bool { return paths[i] < paths[j] })
-
-	if len(paths) != 3 || paths[0] != string(appFile2) || paths[1] != string(appFile1) {
-		t.Fatalf("m.ComponentPaths failed; expected '%s', got '%s'", []string{string(appFile1), string(appFile2)}, paths)
-	}
-}
-
 func TestLibPaths(t *testing.T) {
 	appName := "test-lib-paths"
 	expectedVendorPath := path.Join(appName, vendorDir)
diff --git a/pkg/kubecfg/component.go b/pkg/kubecfg/component.go
new file mode 100644
index 0000000000000000000000000000000000000000..cdb16d93c7e9b8ae533fb9a45c7b5f00c3370a35
--- /dev/null
+++ b/pkg/kubecfg/component.go
@@ -0,0 +1,73 @@
+// Copyright 2017 The kubecfg 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 kubecfg
+
+import (
+	"fmt"
+	"io"
+	"sort"
+	"strings"
+
+	"github.com/ksonnet/ksonnet/utils"
+)
+
+const (
+	componentNameHeader = "COMPONENT"
+)
+
+// ComponentListCmd stores the information necessary to display components.
+type ComponentListCmd struct {
+}
+
+// NewComponentListCmd acts as a constructor for ComponentListCmd.
+func NewComponentListCmd() *ComponentListCmd {
+	return &ComponentListCmd{}
+}
+
+// Run executes the displaying of components.
+func (c *ComponentListCmd) Run(out io.Writer) error {
+	manager, err := manager()
+	if err != nil {
+		return err
+	}
+
+	components, err := manager.GetAllComponents()
+	if err != nil {
+		return err
+	}
+
+	_, err = printComponents(out, components)
+	return err
+}
+
+func printComponents(out io.Writer, components []string) (string, error) {
+	rows := [][]string{
+		[]string{componentNameHeader},
+		[]string{strings.Repeat("=", len(componentNameHeader))},
+	}
+
+	sort.Strings(components)
+	for _, component := range components {
+		rows = append(rows, []string{component})
+	}
+
+	formatted, err := utils.PadRows(rows)
+	if err != nil {
+		return "", err
+	}
+	_, err = fmt.Fprint(out, formatted)
+	return formatted, err
+}
diff --git a/pkg/kubecfg/component_test.go b/pkg/kubecfg/component_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e2cc526a422d11b6873617c5a78cfc763f366d86
--- /dev/null
+++ b/pkg/kubecfg/component_test.go
@@ -0,0 +1,61 @@
+// Copyright 2017 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 kubecfg
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestPrintComponents(t *testing.T) {
+	for _, tc := range []struct {
+		components []string
+		expected   string
+	}{
+		{
+			components: []string{"a", "b"},
+			expected: `COMPONENT
+=========
+a
+b
+`,
+		},
+		// Check that components are displayed in alphabetical order
+		{
+			components: []string{"b", "a"},
+			expected: `COMPONENT
+=========
+a
+b
+`,
+		},
+		// Check empty components scenario
+		{
+			components: []string{},
+			expected: `COMPONENT
+=========
+`,
+		},
+	} {
+		out, err := printComponents(os.Stdout, tc.components)
+		if err != nil {
+			t.Fatalf(err.Error())
+		}
+		require.EqualValues(t, tc.expected, out)
+	}
+}