First working version
authorChip Black <bytex64@bytex64.net>
Sat, 9 Aug 2014 05:03:58 +0000 (00:03 -0500)
committerChip Black <bytex64@bytex64.net>
Sat, 9 Aug 2014 05:03:58 +0000 (00:03 -0500)
27 files changed:
.gitignore [new file with mode: 0644]
.gitmodules [new file with mode: 0644]
config.json.example [new file with mode: 0644]
db-generate.pl [new file with mode: 0644]
debug.html [new file with mode: 0644]
deploy.json [new file with mode: 0644]
enyo [new submodule]
index.html [new file with mode: 0644]
lib/layout [new submodule]
lib/onyx [new submodule]
package.js [new file with mode: 0644]
source/Spotter.js [new file with mode: 0644]
source/boot.js [new file with mode: 0644]
source/data/SongCollection.js [new file with mode: 0644]
source/data/SongModel.js [new file with mode: 0644]
source/data/package.js [new file with mode: 0644]
source/package.js [new file with mode: 0644]
source/spotter/Main.js [new file with mode: 0644]
source/spotter/PlayControl.js [new file with mode: 0644]
source/spotter/Playlist.js [new file with mode: 0644]
source/spotter/Search.js [new file with mode: 0644]
source/spotter/SongItem.js [new file with mode: 0644]
source/spotter/package.js [new file with mode: 0644]
source/style/package.js [new file with mode: 0644]
source/style/spotter.less [new file with mode: 0644]
tools/deploy.bat [new file with mode: 0755]
tools/deploy.sh [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..cb3bc8d
--- /dev/null
@@ -0,0 +1,4 @@
+build/
+deploy/
+config.json
+listing.json
diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..52fa207
--- /dev/null
@@ -0,0 +1,18 @@
+[submodule "www/enyo"]
+       path = www/enyo
+       url = https://github.com/enyojs/enyo.git
+[submodule "www/lib/onyx"]
+       path = www/lib/onyx
+       url = https://github.com/enyojs/onyx.git
+[submodule "www/lib/layout"]
+       path = www/lib/layout
+       url = https://github.com/enyojs/layout.git
+[submodule "enyo"]
+       path = enyo
+       url = https://github.com/enyojs/enyo.git
+[submodule "lib/onyx"]
+       path = lib/onyx
+       url = https://github.com/enyojs/onyx.git
+[submodule "lib/layout"]
+       path = lib/layout
+       url = https://github.com/enyojs/layout.git
diff --git a/config.json.example b/config.json.example
new file mode 100644 (file)
index 0000000..986caab
--- /dev/null
@@ -0,0 +1,5 @@
+{
+       "mediaURL": "http://server.example.com/media/audio/",
+       "mediaPath": "/media/audio/",
+       "listing": "http://server.example.com/spotter/listing.js"
+}
diff --git a/db-generate.pl b/db-generate.pl
new file mode 100644 (file)
index 0000000..bd38de2
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/perl
+use Audio::TagLib;
+use File::Find;
+use JSON;
+use Encode qw/encode decode/;
+use strict;
+
+open CONFIG, 'config.json'
+    or die "Could not open config.json\n";
+my $config_json = join('', <CONFIG>);
+close CONFIG;
+my $config = JSON->new->utf8->decode($config_json);
+
+my @dirs;
+if (@ARGV) {
+    @dirs = @ARGV;
+} else {
+    @dirs = ($config->{mediaPath});
+}
+
+for my $d (@dirs) {
+    if ($d !~ /^$config->{mediaPath}/) {
+        die "$d is not within mediaPath\n";
+    } elsif (! -d $d) {
+        die "$d is not a valid directory\n";
+    }
+}
+
+my $json = JSON->new->utf8;
+
+print '[';
+
+find(sub {
+    return unless /\.mp3$/i;
+    my $f = Audio::TagLib::FileRef->new($_);
+    my $tags = $f->tag;
+    my $file = $File::Find::name;
+    $file =~ s|^$config->{mediaPath}||;
+    my $e = {
+        file => decode('utf8', $file)
+    };
+    if ($tags) {
+        my $artist = $tags->artist->toCString(1);
+        my $album  = $tags->album->toCString(1);
+        my $title  = $tags->title->toCString(1);
+        $e->{artist} = $artist
+            if $artist;
+        $e->{album} = $album
+            if $album;
+        $e->{title} = $title
+            if $title;
+    }
+    print $json->encode($e), ',';
+
+}, @dirs);
+
+print 'null]'
diff --git a/debug.html b/debug.html
new file mode 100644 (file)
index 0000000..60c3525
--- /dev/null
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+               <title>Spotter (Debug)</title>
+               <link rel="shortcut icon" href="assets/favicon.ico"/>
+               <!-- -->
+               <meta http-equiv="Content-Type" content="text/html; charset=utf8"/>
+               <meta name="apple-mobile-web-app-capable" content="yes"/>
+               <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+               <!-- Less.js (for client-side rendering of less stylesheets; comment to use pre-compiled CSS) -->
+               <script src="enyo/tools/less.js"></script>
+               <!-- enyo (debug) -->
+               <script src="enyo/enyo.js" charset="utf-8"></script>
+               <!-- application (debug) -->
+               <script src="source/package.js" charset="utf-8"></script>
+       </head>
+       <body class="enyo-unselectable">
+       </body>
+</html>
diff --git a/deploy.json b/deploy.json
new file mode 100644 (file)
index 0000000..c2f131f
--- /dev/null
@@ -0,0 +1,6 @@
+{
+       "enyo": "./enyo",
+       "packagejs": "./package.js",
+       "assets": ["./icon.png", "./index.html", "./assets"],
+       "libs": ["./lib/onyx", "./lib/layout"]
+}
diff --git a/enyo b/enyo
new file mode 160000 (submodule)
index 0000000..4e7164b
--- /dev/null
+++ b/enyo
@@ -0,0 +1 @@
+Subproject commit 4e7164be9532ce77beffd0afa5b604c055068dda
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..741e46a
--- /dev/null
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+               <title>Spotter</title>
+               <link rel="shortcut icon" href="assets/favicon.ico"/>
+               <!-- -->
+               <meta http-equiv="Content-Type" content="text/html; charset=utf8"/>
+               <meta name="apple-mobile-web-app-capable" content="yes"/>
+               <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+               <!-- css -->
+               <link href="build/enyo.css" rel="stylesheet"/>
+               <link href="build/app.css" rel="stylesheet"/>
+               <!-- js -->
+               <script src="build/enyo.js" charset="utf-8"></script>
+               <script src="build/app.js" charset="utf-8"></script>
+       </head>
+       <body class="enyo-unselectable">
+           <script type="text/javascript">if (undefined === window.enyo) location = "debug.html";</script>
+       </body>
+</html>
diff --git a/lib/layout b/lib/layout
new file mode 160000 (submodule)
index 0000000..11002ee
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit 11002ee2dfb85ec77961196679204791f2fca3dd
diff --git a/lib/onyx b/lib/onyx
new file mode 160000 (submodule)
index 0000000..48e936d
--- /dev/null
+++ b/lib/onyx
@@ -0,0 +1 @@
+Subproject commit 48e936d540f2a0b4d959fe18c7c79794e3661c8f
diff --git a/package.js b/package.js
new file mode 100644 (file)
index 0000000..ac5ed78
--- /dev/null
@@ -0,0 +1,5 @@
+/*
+       == DO NOT EDIT THIS FILE! ==
+       This is necessary to keep paths correct for the minification process
+*/
+enyo.depends("source");
diff --git a/source/Spotter.js b/source/Spotter.js
new file mode 100644 (file)
index 0000000..c12ce10
--- /dev/null
@@ -0,0 +1,5 @@
+enyo.kind({
+    name: "Spotter",
+    kind: "enyo.Application",
+    view: "spotter.Main"
+});
diff --git a/source/boot.js b/source/boot.js
new file mode 100644 (file)
index 0000000..217cbd7
--- /dev/null
@@ -0,0 +1,3 @@
+enyo.ready(function () {
+       new Spotter();
+});
diff --git a/source/data/SongCollection.js b/source/data/SongCollection.js
new file mode 100644 (file)
index 0000000..633f24a
--- /dev/null
@@ -0,0 +1,11 @@
+enyo.kind({
+    name: "SongCollection",
+    kind: "enyo.Collection",
+    model: "SongModel"
+});
+
+enyo.kind({
+    name: "RemoteSongCollection",
+    kind: "SongCollection",
+    url: "http://yomiko.bytex64.net/spotter/listing.json"
+});
diff --git a/source/data/SongModel.js b/source/data/SongModel.js
new file mode 100644 (file)
index 0000000..a92fd82
--- /dev/null
@@ -0,0 +1,13 @@
+enyo.kind({
+    name: "SongModel",
+    kind: "enyo.Model",
+    properties: {
+        filename: ""
+    },
+    bindings: [
+        {from: ".file", to: ".filename", transform: "filenameStrip"}
+    ],
+    filenameStrip: function(val) {
+        return val.replace(/^.*\//, '').replace(/\.mp3$/, '');
+    }
+});
diff --git a/source/data/package.js b/source/data/package.js
new file mode 100644 (file)
index 0000000..7203e62
--- /dev/null
@@ -0,0 +1,4 @@
+enyo.depends(
+    "SongModel.js",
+    "SongCollection.js"
+);
diff --git a/source/package.js b/source/package.js
new file mode 100644 (file)
index 0000000..d0ca62a
--- /dev/null
@@ -0,0 +1,9 @@
+enyo.depends(
+    "$lib/layout",
+    "$lib/onyx",
+    "style",
+    "data",
+    "spotter",
+    "Spotter.js",
+    "boot.js"
+);
diff --git a/source/spotter/Main.js b/source/spotter/Main.js
new file mode 100644 (file)
index 0000000..8840629
--- /dev/null
@@ -0,0 +1,59 @@
+config = {};
+(function() {
+    var req = new enyo.Ajax({
+        url: "config.json"
+    });
+    req.response(this, function(inSender, inResponse) {
+        config = inResponse;
+    });
+    req.error(this, function() {
+        enyo.log("Could not fetch config.json. Trying reasonable defaults.");
+        config = {
+            mediaURL: window.location + 'media/'
+        }
+    });
+    req.go();
+})();
+
+enyo.kind({
+    name: "spotter.Main",
+    kind: "FittableRows",
+    components: [
+        {name: "audioPlayer", kind: "Audio",
+         onPause: "sendPause",
+         onPlay: "sendPlay",
+         onEnded: "sendNextSong"},
+        {kind: "Signals",
+         onShowPlaylist: "showPlaylist",
+         onSongChange: "songChange",
+         onDoPause: "doPause",
+         onDoPlay: "doPlay"},
+        {name: "panels", kind: "Panels", arrangerKind: "CollapsingArranger", fit: true, components: [
+            {kind: "spotter.Search", classes: "spotter-search"},
+            {kind: "spotter.Playlist", classes: "spotter-playlist panel-shadow"}
+        ]},
+        {name: "playControl", kind: "spotter.PlayControl"}
+    ],
+    songChange: function(inSender, inEvent) {
+        this.$.audioPlayer.setSrc(config.mediaURL + inEvent.get('file'));
+        this.$.audioPlayer.play();
+    },
+    doPause: function(inSender, inEvent) {
+        this.$.audioPlayer.pause();
+    },
+    doPlay: function(inSender, inEvent) {
+        this.$.audioPlayer.play();
+    },
+    sendPause: function() {
+        enyo.Signals.send('onPaused');
+    },
+    sendPlay: function() {
+        enyo.Signals.send('onPlayed');
+    },
+    sendNextSong: function() {
+        enyo.Signals.send('onNextSong');
+    },
+    showPlaylist: function(inSender, inEvent) {
+        this.$.panels.setIndex(1);
+    }
+});
diff --git a/source/spotter/PlayControl.js b/source/spotter/PlayControl.js
new file mode 100644 (file)
index 0000000..e2931e3
--- /dev/null
@@ -0,0 +1,45 @@
+enyo.kind({
+    name: "spotter.PlayControl",
+    kind: "onyx.Toolbar",
+    layoutKind: "FittableColumnsLayout",
+    classes: "spotter-playcontrol",
+    components: [
+        {kind: "Signals",
+         onSongChange: "songChange",
+         onPaused: "paused",
+         onPlayed: "played"},
+        {components: [
+            {name: "playButton", kind: "onyx.Button", content: "▶", ontap: "doPlay"},
+            {name: "pauseButton", kind: "onyx.Button", content: "❙❙", ontap: "doPause", showing: false}
+        ]},
+        {name: "playingDisplay", classes: "spotter-playcontrol-display", fit: true}
+    ],
+    create: enyo.inherit(function(sup) {
+        return function() {
+            sup.apply(this, arguments);
+        };
+    }),
+    songChange: function(inSender, inEvent) {
+        var title;
+        if (inEvent.get('artist') && inEvent.get('title')) {
+            title = inEvent.get('artist') + ' - ' + inEvent.get('title');
+        } else {
+            title = inEvent.get('title');
+        }
+        this.$.playingDisplay.setContent(title);
+    },
+    paused: function(inSender, inEvent) {
+        this.$.pauseButton.hide();
+        this.$.playButton.show();
+    },
+    played: function(inSender, inEvent) {
+        this.$.playButton.hide();
+        this.$.pauseButton.show();
+    },
+    doPause: function(inSender, inEvent) {
+        enyo.Signals.send('onDoPause');
+    },
+    doPlay: function(inSender, inEvent) {
+        enyo.Signals.send('onDoPlay');
+    }
+});
diff --git a/source/spotter/Playlist.js b/source/spotter/Playlist.js
new file mode 100644 (file)
index 0000000..4eb229e
--- /dev/null
@@ -0,0 +1,59 @@
+enyo.kind({
+    name: "spotter.Playlist",
+    kind: "FittableRows",
+    published: {
+        collection: null
+    },
+    components: [
+        {kind: "Signals",
+         onAddToPlaylist: "addToPlaylist",
+         onNextSong: "nextSong"},
+        {kind: "onyx.Toolbar", layoutKind: "FittableColumnsLayout", components: [
+            {content: "Playlist", classes: "toolbar-label"},
+            {fit: true},
+            {name: "playlistStatus", content: "0 songs", classes: "toolbar-label"},
+            {kind: "onyx.Button", content: "Clear", ontap: "clearPlaylist"}
+        ]},
+        {name: "playList", kind: "enyo.DataList", fit: true, components: [
+            {kind: "spotter.SongItem", ontap: "playSong"}
+        ]}
+    ],
+    bindings: [
+        {from: ".collection", to: ".$.playList.collection"},
+        {from: ".collection.length", to: ".$.playlistStatus.content",
+         transform: function(v) {
+            return v + " song" + (v == 1 ? '' : 's');
+         }},
+    ],
+    create: enyo.inherit(function(sup) {
+        return function() {
+            this.collection = new SongCollection();
+            sup.apply(this, arguments);
+        }
+    }),
+    addToPlaylist: function(inSender, inEvent) {
+        this.collection.add(inEvent);
+        if (this.collection.length == 1) {
+            this.$.playList.select(0);
+            setTimeout(function() {
+                enyo.Signals.send('onSongChange', inEvent);
+            }, 0);
+        }
+    },
+    clearPlaylist: function(inSender, inEvent) {
+        this.collection.removeAll();
+    },
+    playSong: function(inSender, inEvent) {
+        enyo.Signals.send('onSongChange', inEvent.model);
+    },
+    nextSong: function(inSender, inEvent) {
+        var index = this.collection.indexOf(this.$.playList.selected());
+        index++;
+        if (index == this.collection.length) {
+            this.$.playList.deselectAll();
+        } else {
+            this.$.playList.select(index);
+            enyo.Signals.send('onSongChange', this.collection.at(index));
+        }
+    }
+});
diff --git a/source/spotter/Search.js b/source/spotter/Search.js
new file mode 100644 (file)
index 0000000..8d82a7e
--- /dev/null
@@ -0,0 +1,62 @@
+enyo.kind({
+    name: "spotter.Search",
+    kind: "FittableRows",
+    published: {
+        collection: null
+    },
+    searchDelay: null,
+    components: [
+        {kind: "onyx.Toolbar", components: [
+            {kind: "onyx.InputDecorator", classes: "search-input-decorator", components: [
+                {name: "searchInput", kind: "onyx.Input", placeholder: "Search", onkeyup: "deferredSearch"}
+            ]}
+        ]},
+        {name: "searchList", kind: "enyo.DataList", selection: false, fit: true, components: [
+            {kind: "spotter.SongItem", ontap: "addToPlaylist"}
+        ]}
+    ],
+    bindings: [
+        {from: ".collection", to: ".$.searchList.collection", transform: "searchFilter"}
+    ],
+    create: enyo.inherit(function(sup) {
+        return function() {
+            this.collection = new RemoteSongCollection({instanceAllRecords: true});
+            this.collection.fetch();
+            sup.apply(this, arguments);
+        };
+    }),
+    searchFilter: function(val, dir) {
+        if (this.$.searchInput.getValue() == "") {
+            return val;
+        }
+        try {
+            var term = new RegExp(this.$.searchInput.getValue(), 'i');
+        } catch(e) {
+            return val;
+        }
+        return new SongCollection(val.filter(function(v) {
+            var artist = v.get('artist');
+            var album = v.get('album');
+            var title = v.get('title');
+            return (artist && artist.search(term) != -1) ||
+                   (album && album.search(term) != -1) ||
+                   (title && title.search(term) != -1) ||
+                   v.get('filename').search(term) != -1;
+        }));
+    },
+    deferredSearch: function(inSender, inEvent) {
+        if (this.searchDelay != null) {
+            clearTimeout(this.searchDelay);
+        }
+
+        this.searchDelay = setTimeout(function() {
+            this.bindings[0].sync();
+        }.bind(this), 250);
+    },
+    goToPlaylist: function(inSender, inEvent) {
+        enyo.Signals.send("onShowPlaylist");
+    },
+    addToPlaylist: function(inSender, inEvent) {
+        enyo.Signals.send("onAddToPlaylist", inEvent.model);
+    }
+});
diff --git a/source/spotter/SongItem.js b/source/spotter/SongItem.js
new file mode 100644 (file)
index 0000000..f9a4ff2
--- /dev/null
@@ -0,0 +1,42 @@
+enyo.kind({
+    name: "spotter.SongItem",
+    kind: "Control",
+    classes: "spotter-song-item",
+    published: {
+        filename: ""
+    },
+    components: [
+        {name: "artist", classes: "artist-label"},
+        {name: "album", classes: "album-label"},
+        {name: "title", classes: "title-label"}
+    ],
+    bindings: [
+        {from: ".model.artist", to: ".$.artist.content"},
+        {from: ".model.album", to: ".$.album.content"},
+        {from: ".model.title", to: ".$.title.content"},
+        {from: ".model.filename", to: ".filename"}
+    ],
+    filenameChanged: function() {
+        if (this.$.title.getContent() == null) {
+            // Let's attempt some heuristics to suss out the actual values here
+            var parts = this.filename.split(/\s*-\s*/);
+            if (parts.length > 1) {
+                while (parts[0].search(/^\d+$/) != -1) {
+                    parts.shift();
+                }
+                if (parts.length == 3) {
+                    this.$.artist.setContent(parts[0]);
+                    this.$.album.setContent(parts[1]);
+                    this.$.title.setContent(parts[2]);
+                } else if (parts.length == 2) {
+                    this.$.artist.setContent(parts[0]);
+                    this.$.title.setContent(parts[1]);
+                } else {
+                    this.$.title.setContent(parts[0]);
+                }
+            } else {
+                this.$.title.setContent(this.filename);
+            }
+        }
+    }
+});
diff --git a/source/spotter/package.js b/source/spotter/package.js
new file mode 100644 (file)
index 0000000..55ff52b
--- /dev/null
@@ -0,0 +1,7 @@
+enyo.depends(
+    "SongItem.js",
+    "PlayControl.js",
+    "Search.js",
+    "Playlist.js",
+    "Main.js"
+);
diff --git a/source/style/package.js b/source/style/package.js
new file mode 100644 (file)
index 0000000..a6001cb
--- /dev/null
@@ -0,0 +1,3 @@
+enyo.depends(
+    "spotter.less"
+);
diff --git a/source/style/spotter.less b/source/style/spotter.less
new file mode 100644 (file)
index 0000000..506b1ef
--- /dev/null
@@ -0,0 +1,137 @@
+@primary-color: #3C85CF;
+@background-color: #F9F9FC;
+
+body {
+    background-color: @background-color;
+}
+
+.onyx-toolbar {
+    background-color: darken(@primary-color, 5%);
+    height: 55px;
+}
+
+.toolbar-label {
+    padding: 5px 0;
+}
+
+.spotter-search {
+    min-width: 50%;
+}
+
+.spotter-playlist {
+    background-color: @background-color;
+}
+
+.spotter-song-item {
+    background-color: @background-color;
+    color: darken(@primary-color, 15%);
+    font-size: 20px;
+    padding: 6px 10px;
+    height: 60px;
+    border-bottom: 1px solid darken(@primary-color, 15%);
+}
+
+.spotter-song-item:active, .spotter-song-item.selected {
+    background-color: lighten(@primary-color, 40%);
+    border-bottom: 1px solid darken(@primary-color, 15%);
+}
+
+.spotter-song-item > * {
+    display: inline-block;
+    vertical-align: middle;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+}
+
+.spotter-song-item > .artist-label {
+    font-size: 14px;
+    width: 49%;
+    padding-right: 1%;
+}
+
+.spotter-song-item > .album-label {
+    font-size: 14px;
+    width: 49%;
+    padding-right: 1%;
+}
+
+.spotter-song-item > .title-label {
+    width: 100%;
+    font-size: 22px;
+    margin-top: 6px;
+}
+
+.spotter-playcontrol {
+    height: 64px;
+}
+
+.spotter-playcontrol .onyx-button {
+    height: 43px;
+    width: 57px;
+}
+
+.spotter-playcontrol-display {
+    display: inline-block;
+    margin-left: 6px;
+    padding: 9px 0;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+}
+
+@media screen and (max-width: 700px) {
+    .spotter-search {
+        min-width: 100%;
+    }
+
+    .spotter-song-item {
+        height: 56px;
+        padding: 0px 6px;
+    }
+
+    .spotter-song-item > * {
+        padding: 0;
+    }
+
+    .spotter-song-item > .artist-label {
+        font-size: 12px;
+        height: 14px;
+        width: 39%;
+    }
+
+    .spotter-song-item > .album-label {
+        font-size: 12px;
+        height: 14px;
+        width: 59%;
+    }
+
+    .spotter-song-item > .title-label {
+        width: 100%;
+        font-size: 18px;
+        margin-top: 0;
+        height: auto;
+    }
+}
+
+.search-input-decorator {
+    width: 99.5%;
+    color: @background-color;
+}
+
+.search-input-decorator > .onyx-input {
+    width: 100%;
+    font-size: 18px !important;
+}
+
+.panel-shadow {
+    box-shadow: -4px 0px 8px rgba(0,0,0,0.3);
+}
+
+.centered-spinner {
+    position: fixed;
+    left: 50%;
+    top: 50%;
+    margin: -29px -28px;
+    z-index: 100;
+}
diff --git a/tools/deploy.bat b/tools/deploy.bat
new file mode 100755 (executable)
index 0000000..8247ad7
--- /dev/null
@@ -0,0 +1,40 @@
+@REM don't watch the sausage being made
+@ECHO OFF
+
+REM the folder this script is in (*/bootplate/tools)
+SET TOOLS=%~DP0
+
+REM application source location
+SET SRC=%TOOLS%\..
+
+REM enyo location
+SET ENYO=%SRC%\enyo
+
+REM deploy script location
+SET DEPLOY=%ENYO%\tools\deploy.js
+
+REM node location
+SET NODE=node.exe
+
+REM use node to invoke deploy.js with imported parameters
+ECHO %NODE% "%DEPLOY%" -T -s "%SRC%" -o "%SRC%\deploy" %*
+%NODE% "%DEPLOY%" -T -s "%SRC%" -o "%SRC%\deploy" %*
+
+REM copy files and package if deploying to cordova webos
+:again
+if not "%1" == "" (
+
+    if "%1" == "--cordova-webos" (
+       REM copy appinfo.json and cordova*.js files
+       for %%A in ("%~dp0./..") do SET DEST=%TOOLS%..\deploy\%%~nA
+       copy %SRC%\appinfo.json %DEST%
+       copy %SRC%\cordova*.js %DEST%
+
+       REM package it up
+       if not exist %SRC%\bin mkdir %SRC%\bin
+       palm-package.bat %DEST% --outdir=%SRC%\bin
+    )
+
+    shift
+    goto again
+)
diff --git a/tools/deploy.sh b/tools/deploy.sh
new file mode 100755 (executable)
index 0000000..da2328a
--- /dev/null
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+# the folder this script is in (*/bootplate/tools)
+TOOLS=$(cd `dirname $0` && pwd)
+
+# application root
+SRC="$TOOLS/.."
+
+# enyo location
+ENYO="$SRC/enyo"
+
+# deploy script location
+DEPLOY="$ENYO/tools/deploy.js"
+
+# check for node, but quietly
+if command -v node >/dev/null 2>&1; then
+       # use node to invoke deploy with imported parameters
+       echo "node $DEPLOY -T -s $SRC -o $SRC/deploy $@"
+       node "$DEPLOY" -T -s "$SRC" -o "$SRC/deploy" $@
+else
+       echo "No node found in path"
+       exit 1
+fi
+
+# copy files and package if deploying to cordova webos
+while [ "$1" != "" ]; do
+       case $1 in
+               -w | --cordova-webos )
+                       # copy appinfo.json and cordova*.js files
+                       DEST="$TOOLS/../deploy/"${PWD##*/}
+                       
+                       cp "$SRC"/appinfo.json "$DEST" -v
+                       cp "$SRC"/cordova*.js "$DEST" -v
+                       
+                       # package it up
+                       mkdir -p "$DEST/bin"
+                       palm-package "$DEST/bin"
+                       ;;
+       esac
+       shift
+done