adding basic file - exploded jar - support
diff --git a/README.adoc b/README.adoc
index 0b4f225..5edea6e 100644
--- a/README.adoc
+++ b/README.adoc
@@ -80,4 +80,21 @@
     </plugins>
   </build>
 </project>
-----
\ No newline at end of file
+----
+
+How to add a command? Create a jar with this class:
+
+[source,java]
+----
+@Service
+@Command(name = "hello", scope = "test")
+public class MyCommand implements Action {
+    public Object execute() throws Exception {
+        System.out.println("Hello world");
+        return "hello world";
+    }
+}
+----
+
+Then package it as a normal jar - not even a bundle - and add it
+in the previous classpath. You can now run "test:hello".
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 778dac1..84b0826 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,7 +33,7 @@
     </dependency>
     <dependency>
       <groupId>org.apache.xbean</groupId>
-      <artifactId>xbean-finder</artifactId>
+      <artifactId>xbean-finder-shaded</artifactId>
       <version>4.11</version>
     </dependency>
     <dependency>
diff --git a/src/main/java/org/apache/karaf/framework/ContextualFramework.java b/src/main/java/org/apache/karaf/framework/ContextualFramework.java
index 539b1db..317c43d 100644
--- a/src/main/java/org/apache/karaf/framework/ContextualFramework.java
+++ b/src/main/java/org/apache/karaf/framework/ContextualFramework.java
@@ -15,6 +15,7 @@
 
 import static java.util.Arrays.asList;
 import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
 
 import java.io.File;
 import java.io.IOException;
@@ -28,12 +29,17 @@
 import java.time.ZoneId;
 import java.util.Collection;
 import java.util.Map;
+import java.util.ServiceLoader;
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.function.Predicate;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
 import org.apache.karaf.framework.deployer.OSGiBundleLifecycle;
 import org.apache.karaf.framework.scanner.StandaloneScanner;
+import org.apache.karaf.framework.scanner.manifest.KarafCommandManifestContributor;
+import org.apache.karaf.framework.scanner.manifest.ManifestContributor;
 import org.apache.karaf.framework.service.BundleRegistry;
 import org.apache.karaf.framework.service.OSGiServices;
 import org.slf4j.Logger;
@@ -63,6 +69,36 @@
 
         private File workDir = new File(System.getProperty("java.io.tmpdir"), "karaf-boot_" + UUID.randomUUID().toString());
         private Predicate<String> jarFilter = it -> DEFAULT_EXCLUSIONS.stream().anyMatch(it::startsWith);
+        private Collection<String> scanningIncludes;
+        private Collection<String> scanningExcludes;
+        private Collection<ManifestContributor> manifestContributors = Stream.concat(
+                Stream.of(new KarafCommandManifestContributor()), // built-in
+                StreamSupport.stream(ServiceLoader.load(ManifestContributor.class).spliterator(), false) // extensions
+        ).collect(toList());
+
+        public Collection<ManifestContributor> getManifestContributors() {
+            return manifestContributors;
+        }
+
+        public void setManifestContributors(final Collection<ManifestContributor> manifestContributors) {
+            this.manifestContributors = manifestContributors;
+        }
+
+        public Collection<String> getScanningIncludes() {
+            return scanningIncludes;
+        }
+
+        public void setScanningIncludes(final Collection<String> scanningIncludes) {
+            this.scanningIncludes = scanningIncludes;
+        }
+
+        public Collection<String> getScanningExcludes() {
+            return scanningExcludes;
+        }
+
+        public void setScanningExcludes(final Collection<String> scanningExcludes) {
+            this.scanningExcludes = scanningExcludes;
+        }
 
         public File getWorkDir() {
             return workDir;
@@ -107,9 +143,8 @@
             startTime = System.currentTimeMillis();
             LOGGER.info("Starting Apache Karaf Contextual Framework on {}",
                     LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneId.systemDefault()));
-            new StandaloneScanner(configuration.getJarFilter())
-                    .findOSGiBundles()
-                    .stream()
+            final StandaloneScanner scanner = new StandaloneScanner(configuration, registry.getFramework());
+            Stream.concat(scanner.findOSGiBundles().stream(), scanner.findPotentialOSGiBundles().stream())
                     .sorted(comparing(b -> b.getJar().getName()))
                     .map(it -> new OSGiBundleLifecycle(it.getManifest(), it.getJar(), services, registry, configuration))
                     .peek(OSGiBundleLifecycle::start)
diff --git a/src/main/java/org/apache/karaf/framework/deployer/BundleImpl.java b/src/main/java/org/apache/karaf/framework/deployer/BundleImpl.java
index 63f8c76..a47bc3e 100644
--- a/src/main/java/org/apache/karaf/framework/deployer/BundleImpl.java
+++ b/src/main/java/org/apache/karaf/framework/deployer/BundleImpl.java
@@ -23,7 +23,14 @@
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Dictionary;
 import java.util.Enumeration;
 import java.util.Hashtable;
@@ -203,6 +210,22 @@
 
     @Override
     public Enumeration<String> getEntryPaths(final String path) {
+        if (file.isDirectory()) {
+            final Path base = file.toPath();
+            final Collection<String> paths = new ArrayList<>();
+            try {
+                Files.walkFileTree(base, new SimpleFileVisitor<Path>() {
+                    @Override
+                    public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
+                        paths.add(base.relativize(file).toString());
+                        return super.visitFile(file, attrs);
+                    }
+                });
+            } catch (final IOException e) {
+                throw new IllegalStateException(e);
+            }
+            return enumeration(paths);
+        }
         try (final JarFile jar = new JarFile(file)) {
             return enumeration(list(jar.entries()).stream()
                     .filter(it -> it.getName().startsWith(path))
@@ -228,35 +251,65 @@
         final Filter filter = filePattern == null ?
                 null : context.createFilter("(filename=" + filePattern + ")");
         final String prefix = path == null ? "" : (path.startsWith("/") ? path.substring(1) : path);
-        try (final JarFile jar = new JarFile(file)) { // todo: suport exploded folders
-            return enumeration(list(jar.entries()).stream()
-                      .filter(it -> it.getName().startsWith(prefix))
-                      .map(ZipEntry::getName)
-                      .filter(name -> !name.endsWith("/")) // folders
-                      .filter(name -> { // todo: enrich
-                          if (filter == null) {
-                              return true;
-                          }
-                          if (name.equals(prefix + '/' + filePattern)) {
-                              return true;
-                          }
-                          final Hashtable<String, Object> props = new Hashtable<>();
-                          props.put("filename", name);
-                          return filter.matches(props);
-                      })
-                      .map(name -> {
-                          try {
-                              return new URL("jar", null, file.toURI().toURL().toExternalForm() + "!/" + name);
-                          } catch (final MalformedURLException e) {
-                              throw new IllegalArgumentException(e);
-                          }
-                      })
-                      .collect(toList()));
-        } catch (final IOException e) {
-            throw new IllegalArgumentException(e);
+        final File baseFile = new File(file, prefix);
+        final Path base = baseFile.toPath();
+        if (baseFile.isDirectory()) {
+            if (!recurse) {
+                return enumeration(ofNullable(baseFile.listFiles())
+                        .map(Stream::of)
+                        .orElseGet(Stream::empty)
+                        .filter(file -> doFilterEntry(filter, base.relativize(file.toPath()).toString()))
+                        .map(f -> {
+                            try {
+                                return f.getAbsoluteFile().toURI().toURL();
+                            } catch (final MalformedURLException e) {
+                                throw new IllegalStateException(e);
+                            }
+                        })
+                        .collect(toList()));
+            } else {
+                final Collection<URL> files = new ArrayList<>();
+                try {
+                    Files.walkFileTree(base, new SimpleFileVisitor<Path>() {
+                        @Override
+                        public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
+                            if (doFilterEntry(filter, base.relativize(file).toString())) {
+                                files.add(file.toAbsolutePath().toUri().toURL());
+                            }
+                            return super.visitFile(file, attrs);
+                        }
+                    });
+                } catch (final IOException e) {
+                    throw new IllegalStateException(e);
+                }
+                return enumeration(files);
+            }
+        } else {
+            try (final JarFile jar = new JarFile(file)) {
+                return enumeration(list(jar.entries()).stream().filter(it -> it.getName().startsWith(prefix))
+                                                      .map(ZipEntry::getName).filter(name -> !name.endsWith("/")) // folders
+                                                      .filter(name -> doFilterEntry(filter, name)).map(name -> {
+                            try {
+                                return new URL("jar", null, file.toURI().toURL().toExternalForm() + "!/" + name);
+                            } catch (final MalformedURLException e) {
+                                throw new IllegalArgumentException(e);
+                            }
+                        }).collect(toList()));
+            } catch (final IOException e) {
+                throw new IllegalArgumentException(e);
+            }
         }
     }
 
+    private boolean doFilterEntry(final Filter filter, final String name) {
+        if (filter == null) {
+            return true;
+        }
+        final Hashtable<String, Object> props = new Hashtable<>();
+        props.put("filename", name);
+        return filter.matches(props);
+    }
+
     @Override
     public BundleContext getBundleContext() {
         return context;
diff --git a/src/main/java/org/apache/karaf/framework/scanner/KnownJarsFilter.java b/src/main/java/org/apache/karaf/framework/scanner/KnownJarsFilter.java
new file mode 100644
index 0000000..c998cad
--- /dev/null
+++ b/src/main/java/org/apache/karaf/framework/scanner/KnownJarsFilter.java
@@ -0,0 +1,230 @@
+/**
+ * Copyright (C) 2006-2018 Talend Inc. - www.talend.com
+ * <p>
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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 org.apache.karaf.framework.scanner;
+
+import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.function.Predicate;
+
+import org.apache.karaf.framework.ContextualFramework;
+
+public class KnownJarsFilter implements Predicate<String> {
+    private final Collection<String> forceIncludes = new HashSet<>(); // none for now
+    private final Collection<String> excludes = new HashSet<String>() {{
+        add("activation-");
+        add("activeio-");
+        add("activemq-");
+        add("aether-");
+        add("akka-");
+        add("ant-");
+        add("antlr-");
+        add("aopalliance-");
+        add("ApacheJMeter");
+        add("apiguardian-");
+        add("args4j-");
+        add("arquillian-");
+        add("asciidoctor");
+        add("asm-");
+        add("async-http-client-");
+        add("avalon-framework-");
+        add("axis");
+        add("batchee");
+        add("batik-");
+        add("bcprov-");
+        add("bootstrap");
+        add("bsf-");
+        add("bval");
+        add("c3p0-");
+        add("cassandra-driver-core");
+        add("catalina");
+        add("cglib-");
+        add("charsets.jar");
+        add("commons");
+        add("cryptacular-");
+        add("cssparser-");
+        add("cxf-");
+        add("deploy");
+        add("derby");
+        add("dom4j");
+        add("ecj-");
+        add("eclipselink-");
+        add("ehcache-");
+        add("el-api");
+        add("FastInfoset");
+        add("freeemarker-");
+        add("fusemq-leveldb-");
+        add("geronimo-");
+        add("google-");
+        add("gpars-");
+        add("gragent.jar");
+        add("groovy-");
+        add("gson-");
+        add("guava-");
+        add("guice-");
+        add("h2-");
+        add("hamcrest-");
+        add("hawt");
+        add("hibernate-");
+        add("howl-");
+        add("hsqldb-");
+        add("htmlunit-");
+        add("httpclient-");
+        add("httpcore-");
+        add("icu4j-");
+        add("idb-");
+        add("idea_rt.jar");
+        add("istack-commons-runtime-");
+        add("ivy-");
+        add("jackson-");
+        add("janino-");
+        add("jansi-");
+        add("jasper");
+        add("jasypt-");
+        add("java");
+        add("jaxb-");
+        add("jaxp-");
+        add("jbake-");
+        add("jboss");
+        add("jce.jar");
+        add("jcommander-");
+        add("jersey-");
+        add("jettison-");
+        add("jetty-");
+        add("jfr.jar");
+        add("jfxrt.jar");
+        add("jline");
+        add("jmdns-");
+        add("jna-");
+        add("jnr-");
+        add("joda-time-");
+        add("johnzon-");
+        add("jolokia-");
+        add("jruby-");
+        add("json");
+        add("jsoup-");
+        add("jsp");
+        add("jsr");
+        add("jsse.jar");
+        add("jul");
+        add("junit");
+        add("jython-");
+        add("kahadb-");
+        add("kotlin-runtime");
+        add("leveldb");
+        add("log");
+        add("lombok-");
+        add("lucene");
+        add("management-agent.jar");
+        add("maven-");
+        add("mbean-annotation-api-");
+        add("meecrowave-");
+        add("microprofile-");
+        add("mimepull-");
+        add("mina-");
+        add("mqtt-client-");
+        add("multiverse-core-");
+        add("myfaces-");
+        add("mysql-");
+        add("neethi-");
+        add("nekohtml-");
+        add("netty-");
+        add("openjpa-");
+        add("openmdx-");
+        add("opensaml-");
+        add("opentest4j-");
+        add("openwebbeans-");
+        add("openws-");
+        add("ops4j-");
+        add("org.apache.aries");
+        add("org.eclipse.");
+        add("org.jacoco.agent");
+        add("org.junit.");
+        add("org.osgi.");
+        add("orient-");
+        add("oro-");
+        add("pax");
+        add("PDFBox");
+        add("plexus-");
+        add("plugin.jar");
+        add("poi-");
+        add("qdox-");
+        add("quartz");
+        add("resources.jar");
+        add("rhino-");
+        add("rmock-");
+        add("rt.jar");
+        add("saaj-");
+        add("sac-");
+        add("scala");
+        add("scannotation-");
+        add("serializer-");
+        add("serp-");
+        add("servlet-api-");
+        add("shrinkwrap-");
+        add("sisu-guice");
+        add("sisu-inject");
+        add("slf4j-");
+        add("smack");
+        add("snappy-");
+        add("spring-");
+        add("sshd-");
+        add("stax");
+        add("sunec.jar");
+        add("surefire-");
+        add("swizzle-");
+        add("sxc-");
+        add("testng-");
+        add("tomcat");
+        add("tomee-");
+        add("tools.jar");
+        add("twitter4j-");
+        add("validation-api-");
+        add("velocity-");
+        add("wagon-");
+        add("webbeans");
+        add("websocket");
+        add("woodstox-core-");
+        add("ws-commons-util-");
+        add("wsdl4j-");
+        add("wss4j-");
+        add("wstx-asl-");
+        add("xalan-");
+        add("xbean-");
+        add("xercesImpl-");
+        add("xml");
+        add("XmlSchema-");
+        add("xstream-");
+        add("zipfs.jar");
+        add("ziplock-");
+    }};
+
+    public KnownJarsFilter(final ContextualFramework.Configuration config) {
+        ofNullable(config.getScanningIncludes()).ifPresent(i -> {
+            forceIncludes.clear();
+            forceIncludes.addAll(i.stream().map(String::trim).filter(j -> !j.isEmpty()).collect(toSet()));
+        });
+        ofNullable(config.getScanningExcludes())
+                .ifPresent(i -> excludes.addAll(i.stream().map(String::trim).filter(j -> !j.isEmpty()).collect(toSet())));
+    }
+
+    @Override
+    public boolean test(final String jarName) {
+        return forceIncludes.stream().anyMatch(jarName::startsWith) || excludes.stream().noneMatch(jarName::startsWith);
+    }
+}
diff --git a/src/main/java/org/apache/karaf/framework/scanner/StandaloneScanner.java b/src/main/java/org/apache/karaf/framework/scanner/StandaloneScanner.java
index 272595c..9175da8 100644
--- a/src/main/java/org/apache/karaf/framework/scanner/StandaloneScanner.java
+++ b/src/main/java/org/apache/karaf/framework/scanner/StandaloneScanner.java
@@ -14,57 +14,119 @@
 package org.apache.karaf.framework.scanner;
 
 import static java.util.stream.Collectors.toList;
+import static org.apache.xbean.finder.archive.ClasspathArchive.archive;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
 import java.util.Collection;
+import java.util.List;
 import java.util.Objects;
-import java.util.function.Predicate;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 
+import org.apache.karaf.framework.ContextualFramework;
+import org.apache.karaf.framework.scanner.manifest.ManifestCreator;
+import org.apache.xbean.finder.AnnotationFinder;
 import org.apache.xbean.finder.ClassLoaders;
 import org.apache.xbean.finder.UrlSet;
+import org.apache.xbean.finder.archive.Archive;
 import org.apache.xbean.finder.util.Files;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class StandaloneScanner {
-
+    private final static Logger LOGGER = LoggerFactory.getLogger(StandaloneScanner.class);
     private static final Attributes.Name OSGI_MANIFEST_MARKER = new Attributes.Name("Bundle-Version");
 
-    private final Predicate<String> filter;
+    private final List<URL> urls;
+    private final ContextualFramework.Configuration configuration;
+    private final ClassLoader loader;
+    private final File frameworkJar;
 
-    public StandaloneScanner(final Predicate<String> jarFilter) {
-        this.filter = jarFilter;
-    }
-
-    public Collection<BundleDefinition> findOSGiBundles() {
+    public StandaloneScanner(final ContextualFramework.Configuration configuration, final File frameworkJar) {
+        this.configuration = configuration;
+        this.frameworkJar = frameworkJar;
+        this.loader = Thread.currentThread().getContextClassLoader();
         try {
-            return new UrlSet(ClassLoaders.findUrls(Thread.currentThread().getContextClassLoader()))
+            this.urls = new UrlSet(ClassLoaders.findUrls(loader))
                     .excludeJvm()
-                    .getUrls()
-                    .stream()
-                    .map(Files::toFile)
-                    .filter(this::isIncluded)
-                    .map(this::toDefinition)
-                    .filter(Objects::nonNull)
-                    .collect(toList());
+                    .getUrls();
         } catch (final IOException e) {
             throw new IllegalStateException(e);
         }
     }
 
+    public Collection<BundleDefinition> findPotentialOSGiBundles() {
+        final KnownJarsFilter filter = new KnownJarsFilter(configuration);
+        return urls.stream()
+              .map(it -> new FileAndUrl(Files.toFile(it), it))
+              .filter(it -> !it.file.getAbsoluteFile().equals(frameworkJar))
+              .filter(it -> filter.test(it.file.getName()))
+              .filter(it -> toDefinition(it.file) == null)
+              .map(it -> {
+                  final Archive jarArchive = archive(loader, it.url);
+                  // we scan per archive to be able to create bundle after
+                  try {
+                      final AnnotationFinder archiveFinder = new AnnotationFinder(jarArchive);
+                      final ManifestCreator manifestCreator = new ManifestCreator(it.file.getName());
+                      configuration.getManifestContributors()
+                                   .forEach(c -> c.contribute(archiveFinder, manifestCreator));
+                      final Manifest manifest = manifestCreator.getManifest();
+                      if (manifest == null) {
+                          LOGGER.debug("{} was scanned for nothing, maybe adjust scanning exclusions", it.file);
+                          return null;
+                      }
+                      LOGGER.debug("{} was scanned and is converted to a bundle", it.file);
+                      return new BundleDefinition(manifest, it.file);
+                  } catch (final LinkageError e) {
+                      LOGGER.debug("{} is not scannable, maybe exclude it in framework configuration", it.file);
+                      return null;
+                  }
+              })
+              .filter(Objects::nonNull)
+              .collect(toList());
+    }
+
+    public Collection<BundleDefinition> findOSGiBundles() {
+        return urls
+                .stream()
+                .map(Files::toFile)
+                .filter(this::isIncluded)
+                .map(this::toDefinition)
+                .filter(Objects::nonNull)
+                .collect(toList());
+    }
+
     private boolean isIncluded(final File file) {
-        return !filter.test(file.getName());
+        return !configuration.getJarFilter().test(file.getName());
     }
 
     private BundleDefinition toDefinition(final File file) {
+        if (file.isDirectory()) {
+            final File manifest = new File(file, "META-INF/MANIFEST.MF");
+            if (manifest.exists()) {
+                try (final InputStream stream = new FileInputStream(manifest)) {
+                    final Manifest mf = new Manifest(stream);
+                    if (isOSGi(mf)) {
+                        return new BundleDefinition(mf, file);
+                    }
+                    return null;
+                } catch (final IOException e) {
+                    throw new IllegalStateException(e);
+                }
+            }
+            return null;
+        }
         try (final JarFile jar = new JarFile(file)) {
             final Manifest manifest = jar.getManifest();
             if (manifest == null) {
                 return null;
             }
-            if (manifest.getMainAttributes().containsKey(OSGI_MANIFEST_MARKER)) {
+            if (isOSGi(manifest)) {
                 return new BundleDefinition(manifest, file);
             }
             return null;
@@ -73,6 +135,10 @@
         }
     }
 
+    private boolean isOSGi(final Manifest mf) {
+        return mf.getMainAttributes().containsKey(OSGI_MANIFEST_MARKER);
+    }
+
     public static class BundleDefinition {
         private final Manifest manifest;
         private final File jar;
@@ -90,4 +156,14 @@
             return jar;
         }
     }
+
+    private static class FileAndUrl {
+        private final File file;
+        private final URL url;
+
+        private FileAndUrl(final File file, final URL url) {
+            this.file = file;
+            this.url = url;
+        }
+    }
 }
diff --git a/src/main/java/org/apache/karaf/framework/scanner/manifest/KarafCommandManifestContributor.java b/src/main/java/org/apache/karaf/framework/scanner/manifest/KarafCommandManifestContributor.java
new file mode 100644
index 0000000..3f44719
--- /dev/null
+++ b/src/main/java/org/apache/karaf/framework/scanner/manifest/KarafCommandManifestContributor.java
@@ -0,0 +1,45 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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 org.apache.karaf.framework.scanner.manifest;
+
+import static java.util.stream.Collectors.joining;
+
+import java.lang.annotation.Annotation;
+import java.util.Objects;
+import java.util.function.Supplier;
+import java.util.jar.Manifest;
+
+import org.apache.xbean.finder.AnnotationFinder;
+
+public class KarafCommandManifestContributor implements ManifestContributor {
+    @Override
+    public void contribute(final AnnotationFinder finder, final Supplier<Manifest> manifest) {
+        try {
+            final Class<? extends Annotation> commandMarker = (Class<? extends Annotation>)
+                    finder.getArchive().loadClass("org.apache.karaf.shell.api.action.lifecycle.Service");
+            final String packages = finder.findAnnotatedClasses(commandMarker)
+                                         .stream()
+                                         .map(Class::getPackage)
+                                         .filter(Objects::nonNull)
+                                         .map(Package::getName)
+                                         .distinct()
+                                         .collect(joining(","));
+            if (!packages.isEmpty()) {
+                manifest.get().getMainAttributes().putValue("Karaf-Commands", packages);
+            }
+        } catch (final ClassNotFoundException e) {
+            // no-op
+        }
+    }
+}
diff --git a/src/main/java/org/apache/karaf/framework/scanner/manifest/ManifestContributor.java b/src/main/java/org/apache/karaf/framework/scanner/manifest/ManifestContributor.java
new file mode 100644
index 0000000..6f12867
--- /dev/null
+++ b/src/main/java/org/apache/karaf/framework/scanner/manifest/ManifestContributor.java
@@ -0,0 +1,23 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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 org.apache.karaf.framework.scanner.manifest;
+
+import java.util.function.Supplier;
+import java.util.jar.Manifest;
+
+import org.apache.xbean.finder.AnnotationFinder;
+
+public interface ManifestContributor {
+    void contribute(final AnnotationFinder finder, final Supplier<Manifest> manifest);
+}
diff --git a/src/main/java/org/apache/karaf/framework/scanner/manifest/ManifestCreator.java b/src/main/java/org/apache/karaf/framework/scanner/manifest/ManifestCreator.java
new file mode 100644
index 0000000..3c85048
--- /dev/null
+++ b/src/main/java/org/apache/karaf/framework/scanner/manifest/ManifestCreator.java
@@ -0,0 +1,42 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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 org.apache.karaf.framework.scanner.manifest;
+
+import java.util.function.Supplier;
+import java.util.jar.Manifest;
+
+public class ManifestCreator implements Supplier<Manifest> {
+    private Manifest manifest;
+    private final String name;
+
+    public ManifestCreator(final String name) {
+        this.name = name;
+    }
+
+    @Override
+    public Manifest get() {
+        return manifest == null ? manifest = create() : manifest;
+    }
+
+    public Manifest getManifest() {
+        return manifest;
+    }
+
+    private Manifest create() {
+        final Manifest manifest = new Manifest();
+        manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
+        manifest.getMainAttributes().putValue("Bundle-SymbolicName", name);
+        return manifest;
+    }
+}
diff --git a/src/main/java/org/apache/karaf/framework/service/BundleRegistry.java b/src/main/java/org/apache/karaf/framework/service/BundleRegistry.java
index ab165fd..95a4d5d 100644
--- a/src/main/java/org/apache/karaf/framework/service/BundleRegistry.java
+++ b/src/main/java/org/apache/karaf/framework/service/BundleRegistry.java
@@ -25,19 +25,22 @@
 
 public class BundleRegistry {
     private final Map<Long, OSGiBundleLifecycle> bundles = new HashMap<>();
+    private final File framework;
 
     public BundleRegistry(final OSGiServices services, final ContextualFramework.Configuration configuration) {
+        this.framework = toFile(Thread.currentThread().getContextClassLoader().getResource(getClass().getName().replace('.', '/') + ".class"))
+            .getAbsoluteFile();
+
         // ensure we have the framework bundle simulated
         final Manifest frameworkManifest = new Manifest();
         frameworkManifest.getMainAttributes().putValue("Manifest-Version", "1.0");
         frameworkManifest.getMainAttributes().putValue("Bundle-Version", "1.0");
         frameworkManifest.getMainAttributes().putValue("Bundle-SymbolicName", "Contextual Framework");
-        bundles.put(0L, new OSGiBundleLifecycle(frameworkManifest, getThisJar(), services, this, configuration));
+        bundles.put(0L, new OSGiBundleLifecycle(frameworkManifest, framework, services, this, configuration));
     }
 
-    private File getThisJar() {
-        final String resource = getClass().getName().replace('.', '/') + ".class";
-        return toFile(Thread.currentThread().getContextClassLoader().getResource(resource));
+    public File getFramework() {
+        return framework;
     }
 
     public Map<Long, OSGiBundleLifecycle> getBundles() {
diff --git a/src/main/java/org/apache/karaf/framework/service/ImplicitManifestService.java b/src/main/java/org/apache/karaf/framework/service/ImplicitManifestService.java
new file mode 100644
index 0000000..65a6d20
--- /dev/null
+++ b/src/main/java/org/apache/karaf/framework/service/ImplicitManifestService.java
@@ -0,0 +1,18 @@
+/**
+ * Copyright (C) 2006-2018 Talend Inc. - www.talend.com
+ * <p>
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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 org.apache.karaf.framework.service;
+
+public class ImplicitManifestService {}