mirror of https://github.com/lukechilds/node.git
isaacs
13 years ago
40 changed files with 2542 additions and 1 deletions
@ -0,0 +1 @@ |
|||
node_modules |
@ -0,0 +1,99 @@ |
|||
ansi.js |
|||
========= |
|||
### Advanced ANSI formatting tool for Node.js |
|||
|
|||
![](http://f.cl.ly/items/0D3w3d1W443f2z3X361G/Screen%20Shot%202012-01-26%20at%202.18.31%20AM.png) |
|||
|
|||
`ansi.js` is a module for Node.js that provides an easy-to-use API for |
|||
writing ANSI escape codes to `Stream` instances. ANSI escape codes are used to do |
|||
fancy things in a terminal window, like render text in colors, delete characters, |
|||
lines, the entire window, or hide and show the cursor, and lots more! |
|||
|
|||
The code for the example in the screenshot above can be found in the |
|||
`examples/imgcat` directory. |
|||
|
|||
#### Features: |
|||
|
|||
* 256 color support for the terminal! |
|||
* Make a beep sound from your terminal! |
|||
* Works with *any* writable `Stream` instance. |
|||
* Allows you to move the cursor anywhere on the terminal window. |
|||
* Allows you to delete existing contents from the terminal window. |
|||
* Allows you to hide and show the cursor. |
|||
* Converts CSS color codes and RGB values into ANSI escape codes. |
|||
* Low-level; you are in control of when escape codes are used, it's not abstracted. |
|||
|
|||
|
|||
Installation |
|||
------------ |
|||
|
|||
Install with `npm`: |
|||
|
|||
``` bash |
|||
$ npm install ansi |
|||
``` |
|||
|
|||
|
|||
Example |
|||
------- |
|||
|
|||
``` js |
|||
var ansi = require('ansi') |
|||
, cursor = ansi(process.stdout) |
|||
|
|||
// You can chain your calls forever: |
|||
cursor |
|||
.red() // Set font color to red |
|||
.bg.grey() // Set background color to grey |
|||
.write('Hello World!') // Write 'Hello World!' to stdout |
|||
.bg.reset() // Reset the bgcolor before writing the trailing \n, |
|||
// to avoid Terminal glitches |
|||
.write('\n') // And a final \n to wrap things up |
|||
|
|||
// Rendering modes are persistent: |
|||
cursor.hex('#660000').bold().underline() |
|||
|
|||
// You can use the regular logging functions, text will be green |
|||
console.log('This is blood red, bold text') |
|||
|
|||
// To reset just the foreground color: |
|||
cursor.fg.reset() |
|||
|
|||
console.log('This will still be bold') |
|||
|
|||
// Clean up after yourself! |
|||
cursor.reset() |
|||
``` |
|||
|
|||
|
|||
License |
|||
------- |
|||
|
|||
(The MIT License) |
|||
|
|||
Copyright (c) 2012 Nathan Rajlich <nathan@tootallnate.net> |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining |
|||
a copy of this software and associated documentation files (the |
|||
'Software'), to deal in the Software without restriction, including |
|||
without limitation the rights to use, copy, modify, merge, publish, |
|||
distribute, sublicense, and/or sell copies of the Software, and to |
|||
permit persons to whom the Software is furnished to do so, subject to |
|||
the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be |
|||
included in all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, |
|||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
|||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
|||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
|||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
|||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
---------- |
|||
|
|||
Additionally: |
|||
|
|||
* The `yoshi.png` file inside `examples/imgcat` is copyright to Nintendo, Inc. |
@ -0,0 +1,368 @@ |
|||
|
|||
/** |
|||
* References: |
|||
* |
|||
* - http://en.wikipedia.org/wiki/ANSI_escape_code
|
|||
* - http://www.termsys.demon.co.uk/vtansi.htm
|
|||
* |
|||
*/ |
|||
|
|||
/** |
|||
* Module dependencies. |
|||
*/ |
|||
|
|||
var emitNewlineEvents = require('./newlines') |
|||
, prefix = '\033[' // For all escape codes
|
|||
, suffix = 'm' // Only for color codes
|
|||
|
|||
/** |
|||
* The ANSI escape sequences. |
|||
*/ |
|||
|
|||
var codes = { |
|||
up: 'A' |
|||
, down: 'B' |
|||
, forward: 'C' |
|||
, back: 'D' |
|||
, nextLine: 'E' |
|||
, previousLine: 'F' |
|||
, horizontalAbsolute: 'G' |
|||
, eraseData: 'J' |
|||
, eraseLine: 'K' |
|||
, scrollUp: 'S' |
|||
, scrollDown: 'T' |
|||
, savePosition: 's' |
|||
, restorePosition: 'u' |
|||
, queryPosition: '6n' |
|||
, hide: '?25l' |
|||
, show: '?25h' |
|||
} |
|||
|
|||
/** |
|||
* Rendering ANSI codes. |
|||
*/ |
|||
|
|||
var styles = { |
|||
bold: 1 |
|||
, italic: 3 |
|||
, underline: 4 |
|||
, inverse: 7 |
|||
} |
|||
|
|||
/** |
|||
* The negating ANSI code for the rendering modes. |
|||
*/ |
|||
|
|||
var reset = { |
|||
bold: 22 |
|||
, italic: 23 |
|||
, underline: 24 |
|||
, inverse: 27 |
|||
} |
|||
|
|||
/** |
|||
* The standard, styleable ANSI colors. |
|||
*/ |
|||
|
|||
var colors = { |
|||
white: 37 |
|||
, black: 30 |
|||
, blue: 34 |
|||
, cyan: 36 |
|||
, green: 32 |
|||
, magenta: 35 |
|||
, red: 31 |
|||
, yellow: 33 |
|||
, grey: 90 |
|||
, brightBlack: 90 |
|||
, brightRed: 91 |
|||
, brightGreen: 92 |
|||
, brightYellow: 93 |
|||
, brightBlue: 94 |
|||
, brightMagenta: 95 |
|||
, brightCyan: 96 |
|||
, brightWhite: 97 |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Creates a Cursor instance based off the given `writable stream` instance. |
|||
*/ |
|||
|
|||
function ansi (stream, options) { |
|||
if (stream._ansicursor) { |
|||
return stream._ansicursor |
|||
} else { |
|||
return stream._ansicursor = new Cursor(stream, options) |
|||
} |
|||
} |
|||
module.exports = exports = ansi |
|||
|
|||
/** |
|||
* The `Cursor` class. |
|||
*/ |
|||
|
|||
function Cursor (stream, options) { |
|||
if (!(this instanceof Cursor)) { |
|||
return new Cursor(stream, options) |
|||
} |
|||
if (typeof stream != 'object' || typeof stream.write != 'function') { |
|||
throw new Error('a valid Stream instance must be passed in') |
|||
} |
|||
|
|||
// the stream to use
|
|||
this.stream = stream |
|||
|
|||
// when 'enabled' is false then all the functions are no-ops except for write()
|
|||
this.enabled = options && options.enabled |
|||
if (typeof this.enabled === 'undefined') { |
|||
this.enabled = stream.isTTY |
|||
} |
|||
this.enabled = !!this.enabled |
|||
|
|||
// controls the foreground and background colors
|
|||
this.fg = this.foreground = new Colorer(this, 0) |
|||
this.bg = this.background = new Colorer(this, 10) |
|||
|
|||
// defaults
|
|||
this.Bold = false |
|||
this.Italic = false |
|||
this.Underline = false |
|||
this.Inverse = false |
|||
|
|||
// keep track of the number of "newlines" that get encountered
|
|||
this.newlines = 0 |
|||
emitNewlineEvents(stream) |
|||
stream.on('newline', function () { |
|||
this.newlines++ |
|||
}.bind(this)) |
|||
} |
|||
exports.Cursor = Cursor |
|||
|
|||
/** |
|||
* Helper function that calls `write()` on the underlying Stream. |
|||
* Returns `this` instead of the write() return value to keep |
|||
* the chaining going. |
|||
*/ |
|||
|
|||
Cursor.prototype.write = function () { |
|||
this.stream.write.apply(this.stream, arguments) |
|||
return this |
|||
} |
|||
|
|||
|
|||
/** |
|||
* The `Colorer` class manages both the background and foreground colors. |
|||
*/ |
|||
|
|||
function Colorer (cursor, base) { |
|||
this.current = null |
|||
this.cursor = cursor |
|||
this.base = base |
|||
} |
|||
exports.Colorer = Colorer |
|||
|
|||
/** |
|||
* Write an ANSI color code, ensuring that the same code doesn't get rewritten. |
|||
*/ |
|||
|
|||
Colorer.prototype._setColorCode = function setColorCode (code) { |
|||
var c = String(code) |
|||
if (this.current === c) return |
|||
this.cursor.enabled && this.cursor.write(prefix + c + suffix) |
|||
this.current = c |
|||
return this |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Set up the positional ANSI codes. |
|||
*/ |
|||
|
|||
Object.keys(codes).forEach(function (name) { |
|||
var code = String(codes[name]) |
|||
Cursor.prototype[name] = function () { |
|||
var c = code |
|||
if (arguments.length > 0) { |
|||
c = toArray(arguments).map(Math.round).join(';') + code |
|||
} |
|||
this.enabled && this.write(prefix + c) |
|||
return this |
|||
} |
|||
}) |
|||
|
|||
/** |
|||
* Set up the functions for the rendering ANSI codes. |
|||
*/ |
|||
|
|||
Object.keys(styles).forEach(function (style) { |
|||
var name = style[0].toUpperCase() + style.substring(1) |
|||
, c = styles[style] |
|||
, r = reset[style] |
|||
|
|||
Cursor.prototype[style] = function () { |
|||
if (this[name]) return |
|||
this.enabled && this.write(prefix + c + suffix) |
|||
this[name] = true |
|||
return this |
|||
} |
|||
|
|||
Cursor.prototype['reset' + name] = function () { |
|||
if (!this[name]) return |
|||
this.enabled && this.write(prefix + r + suffix) |
|||
this[name] = false |
|||
return this |
|||
} |
|||
}) |
|||
|
|||
/** |
|||
* Setup the functions for the standard colors. |
|||
*/ |
|||
|
|||
Object.keys(colors).forEach(function (color) { |
|||
var code = colors[color] |
|||
|
|||
Colorer.prototype[color] = function () { |
|||
this._setColorCode(this.base + code) |
|||
return this.cursor |
|||
} |
|||
|
|||
Cursor.prototype[color] = function () { |
|||
return this.foreground[color]() |
|||
} |
|||
}) |
|||
|
|||
/** |
|||
* Makes a beep sound! |
|||
*/ |
|||
|
|||
Cursor.prototype.beep = function () { |
|||
this.enabled && this.write('\007') |
|||
return this |
|||
} |
|||
|
|||
/** |
|||
* Moves cursor to specific position |
|||
*/ |
|||
|
|||
Cursor.prototype.goto = function (x, y) { |
|||
x = x | 0 |
|||
y = y | 0 |
|||
this.enabled && this.write(prefix + y + ';' + x + 'H') |
|||
return this |
|||
} |
|||
|
|||
/** |
|||
* Resets the color. |
|||
*/ |
|||
|
|||
Colorer.prototype.reset = function () { |
|||
this._setColorCode(this.base + 39) |
|||
return this.cursor |
|||
} |
|||
|
|||
/** |
|||
* Resets all ANSI formatting on the stream. |
|||
*/ |
|||
|
|||
Cursor.prototype.reset = function () { |
|||
this.enabled && this.write(prefix + '0' + suffix) |
|||
this.Bold = false |
|||
this.Italic = false |
|||
this.Underline = false |
|||
this.Inverse = false |
|||
this.foreground.current = null |
|||
this.background.current = null |
|||
return this |
|||
} |
|||
|
|||
/** |
|||
* Sets the foreground color with the given RGB values. |
|||
* The closest match out of the 216 colors is picked. |
|||
*/ |
|||
|
|||
Colorer.prototype.rgb = function (r, g, b) { |
|||
var base = this.base + 38 |
|||
, code = rgb(r, g, b) |
|||
this._setColorCode(base + ';5;' + code) |
|||
return this.cursor |
|||
} |
|||
|
|||
/** |
|||
* Same as `cursor.fg.rgb(r, g, b)`. |
|||
*/ |
|||
|
|||
Cursor.prototype.rgb = function (r, g, b) { |
|||
return this.foreground.rgb(r, g, b) |
|||
} |
|||
|
|||
/** |
|||
* Accepts CSS color codes for use with ANSI escape codes. |
|||
* For example: `#FF000` would be bright red. |
|||
*/ |
|||
|
|||
Colorer.prototype.hex = function (color) { |
|||
return this.rgb.apply(this, hex(color)) |
|||
} |
|||
|
|||
/** |
|||
* Same as `cursor.fg.hex(color)`. |
|||
*/ |
|||
|
|||
Cursor.prototype.hex = function (color) { |
|||
return this.foreground.hex(color) |
|||
} |
|||
|
|||
|
|||
// UTIL FUNCTIONS //
|
|||
|
|||
/** |
|||
* Translates a 255 RGB value to a 0-5 ANSI RGV value, |
|||
* then returns the single ANSI color code to use. |
|||
*/ |
|||
|
|||
function rgb (r, g, b) { |
|||
var red = r / 255 * 5 |
|||
, green = g / 255 * 5 |
|||
, blue = b / 255 * 5 |
|||
return rgb5(red, green, blue) |
|||
} |
|||
|
|||
/** |
|||
* Turns rgb 0-5 values into a single ANSI color code to use. |
|||
*/ |
|||
|
|||
function rgb5 (r, g, b) { |
|||
var red = Math.round(r) |
|||
, green = Math.round(g) |
|||
, blue = Math.round(b) |
|||
return 16 + (red*36) + (green*6) + blue |
|||
} |
|||
|
|||
/** |
|||
* Accepts a hex CSS color code string (# is optional) and |
|||
* translates it into an Array of 3 RGB 0-255 values, which |
|||
* can then be used with rgb(). |
|||
*/ |
|||
|
|||
function hex (color) { |
|||
var c = color[0] === '#' ? color.substring(1) : color |
|||
, r = c.substring(0, 2) |
|||
, g = c.substring(2, 4) |
|||
, b = c.substring(4, 6) |
|||
return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)] |
|||
} |
|||
|
|||
/** |
|||
* Turns an array-like object into a real array. |
|||
*/ |
|||
|
|||
function toArray (a) { |
|||
var i = 0 |
|||
, l = a.length |
|||
, rtn = [] |
|||
for (; i<l; i++) { |
|||
rtn.push(a[i]) |
|||
} |
|||
return rtn |
|||
} |
@ -0,0 +1,71 @@ |
|||
|
|||
/** |
|||
* Accepts any node Stream instance and hijacks its "write()" function, |
|||
* so that it can count any newlines that get written to the output. |
|||
* |
|||
* When a '\n' byte is encountered, then a "newline" event will be emitted |
|||
* on the stream, with no arguments. It is up to the listeners to determine |
|||
* any necessary deltas required for their use-case. |
|||
* |
|||
* Ex: |
|||
* |
|||
* var cursor = ansi(process.stdout) |
|||
* , ln = 0 |
|||
* process.stdout.on('newline', function () { |
|||
* ln++ |
|||
* }) |
|||
*/ |
|||
|
|||
/** |
|||
* Module dependencies. |
|||
*/ |
|||
|
|||
var assert = require('assert') |
|||
var NEWLINE = '\n'.charCodeAt(0) |
|||
|
|||
function emitNewlineEvents (stream) { |
|||
if (stream._emittingNewlines) { |
|||
// already emitting newline events
|
|||
return |
|||
} |
|||
|
|||
var write = stream.write |
|||
|
|||
stream.write = function (data) { |
|||
// first write the data
|
|||
var rtn = write.apply(stream, arguments) |
|||
|
|||
if (stream.listeners('newline').length > 0) { |
|||
var len = data.length |
|||
, i = 0 |
|||
// now try to calculate any deltas
|
|||
if (typeof data == 'string') { |
|||
for (; i<len; i++) { |
|||
processByte(stream, data.charCodeAt(i)) |
|||
} |
|||
} else { |
|||
// buffer
|
|||
for (; i<len; i++) { |
|||
processByte(stream, data[i]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
return rtn |
|||
} |
|||
|
|||
stream._emittingNewlines = true |
|||
} |
|||
module.exports = emitNewlineEvents |
|||
|
|||
|
|||
/** |
|||
* Processes an individual byte being written to a stream |
|||
*/ |
|||
|
|||
function processByte (stream, b) { |
|||
assert.equal(typeof b, 'number') |
|||
if (b === NEWLINE) { |
|||
stream.emit('newline') |
|||
} |
|||
} |
@ -0,0 +1,56 @@ |
|||
{ |
|||
"name": "ansi", |
|||
"description": "Advanced ANSI formatting tool for Node.js", |
|||
"keywords": [ |
|||
"ansi", |
|||
"formatting", |
|||
"cursor", |
|||
"color", |
|||
"terminal", |
|||
"rgb", |
|||
"256", |
|||
"stream" |
|||
], |
|||
"version": "0.1.2", |
|||
"author": { |
|||
"name": "Nathan Rajlich", |
|||
"email": "nathan@tootallnate.net", |
|||
"url": "http://tootallnate.net" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git://github.com/TooTallNate/ansi.js.git" |
|||
}, |
|||
"main": "./lib/ansi.js", |
|||
"bin": { |
|||
"beep": "./examples/beep/index.js", |
|||
"clear": "./examples/clear/index.js", |
|||
"imgcat": "./examples/imgcat/index.js", |
|||
"starwars": "./examples/starwars.js" |
|||
}, |
|||
"scripts": { |
|||
"test": "mocha --reporter spec" |
|||
}, |
|||
"devDependencies": { |
|||
"mocha": "*", |
|||
"canvas": "*" |
|||
}, |
|||
"engines": { |
|||
"node": "*" |
|||
}, |
|||
"_npmUser": { |
|||
"name": "isaacs", |
|||
"email": "i@izs.me" |
|||
}, |
|||
"_id": "ansi@0.1.2", |
|||
"dependencies": {}, |
|||
"optionalDependencies": {}, |
|||
"_engineSupported": true, |
|||
"_npmVersion": "1.1.24", |
|||
"_nodeVersion": "v0.7.10-pre", |
|||
"_defaultsLoaded": true, |
|||
"dist": { |
|||
"shasum": "6139b23459bcd74b04572cf56b36102983d0a7d4" |
|||
}, |
|||
"_from": "ansi@~0.1.2" |
|||
} |
@ -0,0 +1,25 @@ |
|||
Copyright (c) Isaac Z. Schlueter |
|||
All rights reserved. |
|||
|
|||
The BSD License |
|||
|
|||
Redistribution and use in source and binary forms, with or without |
|||
modification, are permitted provided that the following conditions |
|||
are met: |
|||
1. Redistributions of source code must retain the above copyright |
|||
notice, this list of conditions and the following disclaimer. |
|||
2. Redistributions in binary form must reproduce the above copyright |
|||
notice, this list of conditions and the following disclaimer in the |
|||
documentation and/or other materials provided with the distribution. |
|||
|
|||
THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS |
|||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
|||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS |
|||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
|||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
|||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
|||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
|||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|||
POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,25 @@ |
|||
Copyright (c) Isaac Z. Schlueter |
|||
All rights reserved. |
|||
|
|||
The BSD License |
|||
|
|||
Redistribution and use in source and binary forms, with or without |
|||
modification, are permitted provided that the following conditions |
|||
are met: |
|||
1. Redistributions of source code must retain the above copyright |
|||
notice, this list of conditions and the following disclaimer. |
|||
2. Redistributions in binary form must reproduce the above copyright |
|||
notice, this list of conditions and the following disclaimer in the |
|||
documentation and/or other materials provided with the distribution. |
|||
|
|||
THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS |
|||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
|||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS |
|||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
|||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
|||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
|||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
|||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|||
POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,2 @@ |
|||
node_modules/ |
|||
npm-debug.log |
@ -0,0 +1,5 @@ |
|||
--- /dev/null |
|||
+++ .gitignore |
|||
@@ -0,0 +1,2 @@ |
|||
+node_modules/ |
|||
+npm-debug.log |
@ -0,0 +1,2 @@ |
|||
node_modules/ |
|||
npm-debug.log |
@ -0,0 +1,4 @@ |
|||
language: node_js |
|||
node_js: |
|||
- 0.4 |
|||
- 0.6 |
@ -0,0 +1,46 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
var ansi = require('../') |
|||
, cursor = ansi(process.stdout) |
|||
, tty = require('tty') |
|||
, Canvas = require('canvas') |
|||
, imageFile = process.argv[2] || __dirname + '/yoshi.png' |
|||
, image = require('fs').readFileSync(imageFile) |
|||
, pixel = ' ' |
|||
, alphaThreshold = 0 |
|||
|
|||
var img = new Canvas.Image(); |
|||
img.src = image; |
|||
|
|||
function draw () { |
|||
var width = process.stdout.getWindowSize()[0] / pixel.length | 0 |
|||
, scaleW = img.width > width ? width / img.width : 1 |
|||
, w = Math.floor(img.width * scaleW) |
|||
, h = Math.floor(img.height * scaleW); |
|||
|
|||
var canvas = new Canvas(w, h) |
|||
, ctx = canvas.getContext('2d'); |
|||
|
|||
ctx.drawImage(img, 0, 0, w, h); |
|||
|
|||
var data = ctx.getImageData(0, 0, w, h).data; |
|||
|
|||
for (var i=0, l=data.length; i<l; i+=4) { |
|||
var r = data[i] |
|||
, g = data[i+1] |
|||
, b = data[i+2] |
|||
, alpha = data[i+3]; |
|||
if (alpha > alphaThreshold) { |
|||
cursor.bg.rgb(r, g, b); |
|||
} else { |
|||
cursor.bg.reset(); |
|||
} |
|||
process.stdout.write(pixel); |
|||
if ((i/4|0) % w === (w-1)) { |
|||
cursor.bg.reset(); |
|||
process.stdout.write('\n'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
draw(); |
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,9 @@ |
|||
var Glob = require("../").Glob |
|||
|
|||
var pattern = "test/a/**/[cg]/../[cg]" |
|||
console.log(pattern) |
|||
|
|||
var mg = new Glob(pattern, {mark: true, sync:true}, function (er, matches) { |
|||
console.log("matches", matches) |
|||
}) |
|||
console.log("after") |
@ -0,0 +1,9 @@ |
|||
var Glob = require("../").Glob |
|||
|
|||
var pattern = "{./*/*,/*,/usr/local/*}" |
|||
console.log(pattern) |
|||
|
|||
var mg = new Glob(pattern, {mark: true}, function (er, matches) { |
|||
console.log("matches", matches) |
|||
}) |
|||
console.log("after") |
@ -0,0 +1,61 @@ |
|||
// just a little pre-run script to set up the fixtures.
|
|||
// zz-finish cleans it up
|
|||
|
|||
var mkdirp = require("mkdirp") |
|||
var path = require("path") |
|||
var i = 0 |
|||
var tap = require("tap") |
|||
var fs = require("fs") |
|||
var rimraf = require("rimraf") |
|||
|
|||
var files = |
|||
[ "a/.abcdef/x/y/z/a" |
|||
, "a/abcdef/g/h" |
|||
, "a/abcfed/g/h" |
|||
, "a/b/c/d" |
|||
, "a/bc/e/f" |
|||
, "a/c/d/c/b" |
|||
, "a/cb/e/f" |
|||
] |
|||
|
|||
var symlinkTo = path.resolve(__dirname, "a/symlink/a/b/c") |
|||
var symlinkFrom = "../.." |
|||
|
|||
files = files.map(function (f) { |
|||
return path.resolve(__dirname, f) |
|||
}) |
|||
|
|||
tap.test("remove fixtures", function (t) { |
|||
rimraf(path.resolve(__dirname, "a"), function (er) { |
|||
t.ifError(er, "remove fixtures") |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
files.forEach(function (f) { |
|||
tap.test(f, function (t) { |
|||
var d = path.dirname(f) |
|||
mkdirp(d, 0755, function (er) { |
|||
if (er) { |
|||
t.fail(er) |
|||
return t.bailout() |
|||
} |
|||
fs.writeFile(f, "i like tests", function (er) { |
|||
t.ifError(er, "make file") |
|||
t.end() |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
tap.test("symlinky", function (t) { |
|||
var d = path.dirname(symlinkTo) |
|||
console.error("mkdirp", d) |
|||
mkdirp(d, 0755, function (er) { |
|||
t.ifError(er) |
|||
fs.symlink(symlinkFrom, symlinkTo, function (er) { |
|||
t.ifError(er, "make symlink") |
|||
t.end() |
|||
}) |
|||
}) |
|||
}) |
@ -0,0 +1,119 @@ |
|||
// basic test
|
|||
// show that it does the same thing by default as the shell.
|
|||
var tap = require("tap") |
|||
, child_process = require("child_process") |
|||
|
|||
// put more patterns here.
|
|||
, globs = |
|||
[ |
|||
"test/a/*/+(c|g)/./d" |
|||
,"test/a/**/[cg]/../[cg]" |
|||
,"test/a/{b,c,d,e,f}/**/g" |
|||
,"test/a/b/**" |
|||
,"test/**/g" |
|||
,"test/a/abc{fed,def}/g/h" |
|||
,"test/a/abc{fed/g,def}/**/" |
|||
,"test/a/abc{fed/g,def}/**///**/" |
|||
,"test/**/a/**/" |
|||
,"test/+(a|b|c)/a{/,bc*}/**" |
|||
,"test/*/*/*/f" |
|||
,"test/**/f" |
|||
,"test/a/symlink/a/b/c/a/b/c/a/b/c//a/b/c////a/b/c/**/b/c/**" |
|||
,"{./*/*,/usr/local/*}" |
|||
,"{/*,*}" // evil owl face! how you taunt me!
|
|||
] |
|||
, glob = require("../") |
|||
, path = require("path") |
|||
|
|||
// run from the root of the project
|
|||
// this is usually where you're at anyway, but be sure.
|
|||
process.chdir(path.resolve(__dirname, "..")) |
|||
|
|||
function alphasort (a, b) { |
|||
a = a.toLowerCase() |
|||
b = b.toLowerCase() |
|||
return a > b ? 1 : a < b ? -1 : 0 |
|||
} |
|||
|
|||
globs.forEach(function (pattern) { |
|||
var echoOutput |
|||
tap.test(pattern, function (t) { |
|||
var bashPattern = pattern |
|||
, cmd = "shopt -s globstar && " + |
|||
"shopt -s extglob && " + |
|||
"shopt -s nullglob && " + |
|||
// "shopt >&2; " +
|
|||
"eval \'for i in " + bashPattern + "; do echo $i; done\'" |
|||
, cp = child_process.spawn("bash", ["-c",cmd]) |
|||
, out = [] |
|||
, globResult |
|||
cp.stdout.on("data", function (c) { |
|||
out.push(c) |
|||
}) |
|||
cp.stderr.on("data", function (c) { |
|||
process.stderr.write(c) |
|||
}) |
|||
cp.stdout.on("close", function () { |
|||
echoOutput = flatten(out) |
|||
if (!echoOutput) echoOutput = [] |
|||
else { |
|||
echoOutput = echoOutput.split(/\r*\n/).map(function (m) { |
|||
// Bash is a oddly inconsistent with slashes in the
|
|||
// the results. This implementation is a bit more
|
|||
// normalized. Account for this in the test results.
|
|||
return m.replace(/\/+/g, "/").replace(/\/$/, "") |
|||
}).sort(alphasort).reduce(function (set, f) { |
|||
if (f !== set[set.length - 1]) set.push(f) |
|||
return set |
|||
}, []).sort(alphasort) |
|||
} |
|||
next() |
|||
}) |
|||
|
|||
glob(pattern, function (er, matches) { |
|||
// sort and unpark, just to match the shell results
|
|||
matches = matches.map(function (m) { |
|||
return m.replace(/\/+/g, "/").replace(/\/$/, "") |
|||
}).sort(alphasort).reduce(function (set, f) { |
|||
if (f !== set[set.length - 1]) set.push(f) |
|||
return set |
|||
}, []).sort(alphasort) |
|||
|
|||
t.ifError(er, pattern + " should not error") |
|||
globResult = matches |
|||
next() |
|||
}) |
|||
|
|||
function next () { |
|||
if (!echoOutput || !globResult) return |
|||
|
|||
t.deepEqual(globResult, echoOutput, "should match shell") |
|||
t.end() |
|||
} |
|||
}) |
|||
|
|||
tap.test(pattern + " sync", function (t) { |
|||
var matches = glob.sync(pattern).map(function (m) { |
|||
return m.replace(/\/+/g, "/").replace(/\/$/, "") |
|||
}).sort(alphasort).reduce(function (set, f) { |
|||
if (f !== set[set.length - 1]) set.push(f) |
|||
return set |
|||
}, []).sort(alphasort) |
|||
|
|||
t.deepEqual(matches, echoOutput, "should match shell") |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
function flatten (chunks) { |
|||
var s = 0 |
|||
chunks.forEach(function (c) { s += c.length }) |
|||
var out = new Buffer(s) |
|||
s = 0 |
|||
chunks.forEach(function (c) { |
|||
c.copy(out, s) |
|||
s += c.length |
|||
}) |
|||
|
|||
return out.toString().trim() |
|||
} |
@ -0,0 +1,55 @@ |
|||
var tap = require("tap") |
|||
|
|||
var origCwd = process.cwd() |
|||
process.chdir(__dirname) |
|||
|
|||
tap.test("changing cwd and searching for **/d", function (t) { |
|||
var glob = require('../') |
|||
var path = require('path') |
|||
t.test('.', function (t) { |
|||
glob('**/d', function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, [ 'a/b/c/d', 'a/c/d' ]) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('a', function (t) { |
|||
glob('**/d', {cwd:path.resolve('a')}, function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, [ 'b/c/d', 'c/d' ]) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('a/b', function (t) { |
|||
glob('**/d', {cwd:path.resolve('a/b')}, function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, [ 'c/d' ]) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('a/b/', function (t) { |
|||
glob('**/d', {cwd:path.resolve('a/b/')}, function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, [ 'c/d' ]) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('.', function (t) { |
|||
glob('**/d', {cwd: process.cwd()}, function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, [ 'a/b/c/d', 'a/c/d' ]) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('cd -', function (t) { |
|||
process.chdir(origCwd) |
|||
t.end() |
|||
}) |
|||
|
|||
t.end() |
|||
}) |
@ -0,0 +1,98 @@ |
|||
// show that no match events happen while paused.
|
|||
var tap = require("tap") |
|||
, child_process = require("child_process") |
|||
// just some gnarly pattern with lots of matches
|
|||
, pattern = "test/a/symlink/a/b/c/a/b/c/a/b/c//a/b/c////a/b/c/**/b/c/**" |
|||
, glob = require("../") |
|||
, Glob = glob.Glob |
|||
, path = require("path") |
|||
|
|||
// run from the root of the project
|
|||
// this is usually where you're at anyway, but be sure.
|
|||
process.chdir(path.resolve(__dirname, "..")) |
|||
|
|||
function alphasort (a, b) { |
|||
a = a.toLowerCase() |
|||
b = b.toLowerCase() |
|||
return a > b ? 1 : a < b ? -1 : 0 |
|||
} |
|||
|
|||
function cleanResults (m) { |
|||
// normalize discrepancies in ordering, duplication,
|
|||
// and ending slashes.
|
|||
return m.map(function (m) { |
|||
return m.replace(/\/+/g, "/").replace(/\/$/, "") |
|||
}).sort(alphasort).reduce(function (set, f) { |
|||
if (f !== set[set.length - 1]) set.push(f) |
|||
return set |
|||
}, []).sort(alphasort) |
|||
} |
|||
|
|||
function flatten (chunks) { |
|||
var s = 0 |
|||
chunks.forEach(function (c) { s += c.length }) |
|||
var out = new Buffer(s) |
|||
s = 0 |
|||
chunks.forEach(function (c) { |
|||
c.copy(out, s) |
|||
s += c.length |
|||
}) |
|||
|
|||
return out.toString().trim() |
|||
} |
|||
var bashResults |
|||
tap.test("get bash output", function (t) { |
|||
var bashPattern = pattern |
|||
, cmd = "shopt -s globstar && " + |
|||
"shopt -s extglob && " + |
|||
"shopt -s nullglob && " + |
|||
// "shopt >&2; " +
|
|||
"eval \'for i in " + bashPattern + "; do echo $i; done\'" |
|||
, cp = child_process.spawn("bash", ["-c",cmd]) |
|||
, out = [] |
|||
, globResult |
|||
cp.stdout.on("data", function (c) { |
|||
out.push(c) |
|||
}) |
|||
cp.stderr.on("data", function (c) { |
|||
process.stderr.write(c) |
|||
}) |
|||
cp.stdout.on("close", function () { |
|||
bashResults = flatten(out) |
|||
if (!bashResults) return t.fail("Didn't get results from bash") |
|||
else { |
|||
bashResults = cleanResults(bashResults.split(/\r*\n/)) |
|||
} |
|||
t.ok(bashResults.length, "got some results") |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
var globResults = [] |
|||
tap.test("use a Glob object, and pause/resume it", function (t) { |
|||
var g = new Glob(pattern) |
|||
, paused = false |
|||
, res = [] |
|||
|
|||
g.on("match", function (m) { |
|||
t.notOk(g.paused, "must not be paused") |
|||
globResults.push(m) |
|||
g.pause() |
|||
t.ok(g.paused, "must be paused") |
|||
setTimeout(g.resume.bind(g), 1) |
|||
}) |
|||
|
|||
g.on("end", function (matches) { |
|||
t.pass("reached glob end") |
|||
globResults = cleanResults(globResults) |
|||
matches = cleanResults(matches) |
|||
t.deepEqual(matches, globResults, |
|||
"end event matches should be the same as match events") |
|||
|
|||
t.deepEqual(matches, bashResults, |
|||
"glob matches should be the same as bash results") |
|||
|
|||
t.end() |
|||
}) |
|||
}) |
|||
|
@ -0,0 +1,39 @@ |
|||
var tap = require("tap") |
|||
|
|||
var origCwd = process.cwd() |
|||
process.chdir(__dirname) |
|||
|
|||
tap.test("changing root and searching for /b*/**", function (t) { |
|||
var glob = require('../') |
|||
var path = require('path') |
|||
t.test('.', function (t) { |
|||
glob('/b*/**', { globDebug: true, root: '.', nomount: true }, function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, []) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('a', function (t) { |
|||
glob('/b*/**', { globDebug: true, root: path.resolve('a'), nomount: true }, function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, [ '/b', '/b/c', '/b/c/d', '/bc', '/bc/e', '/bc/e/f' ]) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('root=a, cwd=a/b', function (t) { |
|||
glob('/b*/**', { globDebug: true, root: 'a', cwd: path.resolve('a/b'), nomount: true }, function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, [ '/b', '/b/c', '/b/c/d', '/bc', '/bc/e', '/bc/e/f' ]) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('cd -', function (t) { |
|||
process.chdir(origCwd) |
|||
t.end() |
|||
}) |
|||
|
|||
t.end() |
|||
}) |
@ -0,0 +1,43 @@ |
|||
var tap = require("tap") |
|||
|
|||
var origCwd = process.cwd() |
|||
process.chdir(__dirname) |
|||
|
|||
tap.test("changing root and searching for /b*/**", function (t) { |
|||
var glob = require('../') |
|||
var path = require('path') |
|||
t.test('.', function (t) { |
|||
glob('/b*/**', { globDebug: true, root: '.' }, function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, []) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('a', function (t) { |
|||
glob('/b*/**', { globDebug: true, root: path.resolve('a') }, function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, [ '/b', '/b/c', '/b/c/d', '/bc', '/bc/e', '/bc/e/f' ].map(function (m) { |
|||
return path.join(path.resolve('a'), m) |
|||
})) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('root=a, cwd=a/b', function (t) { |
|||
glob('/b*/**', { globDebug: true, root: 'a', cwd: path.resolve('a/b') }, function (er, matches) { |
|||
t.ifError(er) |
|||
t.like(matches, [ '/b', '/b/c', '/b/c/d', '/bc', '/bc/e', '/bc/e/f' ].map(function (m) { |
|||
return path.join(path.resolve('a'), m) |
|||
})) |
|||
t.end() |
|||
}) |
|||
}) |
|||
|
|||
t.test('cd -', function (t) { |
|||
process.chdir(origCwd) |
|||
t.end() |
|||
}) |
|||
|
|||
t.end() |
|||
}) |
@ -0,0 +1,11 @@ |
|||
// remove the fixtures
|
|||
var tap = require("tap") |
|||
, rimraf = require("rimraf") |
|||
, path = require("path") |
|||
|
|||
tap.test("cleanup fixtures", function (t) { |
|||
rimraf(path.resolve(__dirname, "a"), function (er) { |
|||
t.ifError(er, "removed") |
|||
t.end() |
|||
}) |
|||
}) |
@ -0,0 +1 @@ |
|||
test/fixtures/cache |
@ -0,0 +1,129 @@ |
|||
# npm-registry-client |
|||
|
|||
The code that npm uses to talk to the registry. |
|||
|
|||
It handles all the caching and HTTP calls. |
|||
|
|||
## Usage |
|||
|
|||
```javascript |
|||
var RegClient = require('npm-registry-client') |
|||
var client = new RegClient(options) |
|||
|
|||
client.get("npm", "latest", 1000, function (er, data, raw, res) { |
|||
// error is an error if there was a problem. |
|||
// data is the parsed data object |
|||
// raw is the json string |
|||
// res is the response from couch |
|||
}) |
|||
``` |
|||
|
|||
# Options |
|||
|
|||
* `registry` **Required** {String} URL to the registry |
|||
* `cache` **Required** {String} Path to the cache folder |
|||
* `alwaysAuth` {Boolean} Auth even for GET requests. |
|||
* `auth` {String} A base64-encoded `username:password` |
|||
* `email` {String} User's email address |
|||
* `tag` {String} The default tag to use when publishing new packages. |
|||
Default = `"latest"` |
|||
* `ca` {String} Cerficate signing authority certificates to trust. |
|||
* `strictSSL` {Boolean} Whether or not to be strict with SSL |
|||
certificates. Default = `true` |
|||
* `userAgent` {String} User agent header to send. Default = |
|||
`"node/{process.version}"` |
|||
* `log` {Object} The logger to use. Defaults to `require("npmlog")` if |
|||
that works, otherwise logs are disabled. |
|||
|
|||
# client.request(method, where, [what], [etag], [nofollow], cb) |
|||
|
|||
* `method` {String} HTTP method |
|||
* `where` {String} Path to request on the server |
|||
* `what` {Stream | Buffer | String | Object} The request body. Objects |
|||
that are not Buffers or Streams are encoded as JSON. |
|||
* `etag` {String} The cached ETag |
|||
* `nofollow` {Boolean} Prevent following 302/301 responses |
|||
* `cb` {Function} |
|||
* `error` {Error | null} |
|||
* `data` {Object} the parsed data object |
|||
* `raw` {String} the json |
|||
* `res` {Response Object} response from couch |
|||
|
|||
Make a request to the registry. All the other methods are wrappers |
|||
around this. one. |
|||
|
|||
# client.adduser(username, password, email, cb) |
|||
|
|||
* `username` {String} |
|||
* `password` {String} |
|||
* `email` {String} |
|||
* `cb` {Function} |
|||
|
|||
Add a user account to the registry, or verify the credentials. |
|||
|
|||
# client.get(url, [timeout], [nofollow], [staleOk], cb) |
|||
|
|||
* `url` {String} The url path to fetch |
|||
* `timeout` {Number} Number of seconds old that a cached copy must be |
|||
before a new request will be made. |
|||
* `nofollow` {Boolean} Do not follow 301/302 responses |
|||
* `staleOk` {Boolean} If there's cached data available, then return that |
|||
to the callback quickly, and update the cache the background. |
|||
|
|||
Fetches data from the registry via a GET request, saving it in |
|||
the cache folder with the ETag. |
|||
|
|||
# client.publish(data, tarball, [readme], cb) |
|||
|
|||
* `data` {Object} Package data |
|||
* `tarball` {String | Stream} Filename or stream of the package tarball |
|||
* `readme` {String} Contents of the README markdown file |
|||
* `cb` {Function} |
|||
|
|||
Publish a package to the registry. |
|||
|
|||
Note that this does not create the tarball from a folder. However, it |
|||
can accept a gzipped tar stream or a filename to a tarball. |
|||
|
|||
# client.star(package, starred, cb) |
|||
|
|||
* `package` {String} Name of the package to star |
|||
* `starred` {Boolean} True to star the package, false to unstar it. |
|||
* `cb` {Function} |
|||
|
|||
Star or unstar a package. |
|||
|
|||
Note that the user does not have to be the package owner to star or |
|||
unstar a package, though other writes do require that the user be the |
|||
package owner. |
|||
|
|||
# client.tag(project, version, tag, cb) |
|||
|
|||
* `project` {String} Project name |
|||
* `version` {String} Version to tag |
|||
* `tag` {String} Tag name to apply |
|||
* `cb` {Function} |
|||
|
|||
Mark a version in the `dist-tags` hash, so that `pkg@tag` |
|||
will fetch the specified version. |
|||
|
|||
# client.unpublish(name, [ver], cb) |
|||
|
|||
* `name` {String} package name |
|||
* `ver` {String} version to unpublish. Leave blank to unpublish all |
|||
versions. |
|||
* `cb` {Function} |
|||
|
|||
Remove a version of a package (or all versions) from the registry. When |
|||
the last version us unpublished, the entire document is removed from the |
|||
database. |
|||
|
|||
# client.upload(where, file, [etag], [nofollow], cb) |
|||
|
|||
* `where` {String} URL path to upload to |
|||
* `file` {String | Stream} Either the filename or a readable stream |
|||
* `etag` {String} Cache ETag |
|||
* `nofollow` {Boolean} Do not follow 301/302 responses |
|||
* `cb` {Function} |
|||
|
|||
Upload an attachment. Mostly used by `client.publish()`. |
@ -0,0 +1,65 @@ |
|||
|
|||
// utilities for working with the js-registry site.
|
|||
|
|||
module.exports = RegClient |
|||
|
|||
var fs = require('fs') |
|||
, url = require('url') |
|||
, path = require('path') |
|||
, npmlog |
|||
|
|||
try { |
|||
npmlog = require("npmlog") |
|||
} catch (er) { |
|||
npmlog = { error: noop, warn: noop, info: noop, |
|||
verbose: noop, silly: noop, http: silly, |
|||
pause: noop, resume: noop } |
|||
} |
|||
|
|||
function noop () {} |
|||
|
|||
function RegClient (options) { |
|||
// a registry url must be provided.
|
|||
var registry = url.parse(options.registry) |
|||
if (!registry.protocol) throw new Error( |
|||
'Invalid registry: ' + registry.url) |
|||
this.registry = registry.href |
|||
|
|||
this.cache = options.cache |
|||
if (!this.cache) throw new Error("Cache dir is required") |
|||
|
|||
this.alwaysAuth = options.alwaysAuth || false |
|||
|
|||
this.auth = options.auth || null |
|||
if (this.auth) { |
|||
var a = new Buffer(this.auth, "base64").toString() |
|||
a = a.split(":") |
|||
this.username = a.shift() |
|||
this.password = a.join(":") |
|||
} |
|||
this.email = options.email || null |
|||
this.defaultTag = options.tag || "latest" |
|||
|
|||
this.ca = options.ca || null |
|||
|
|||
this.strictSSL = options.strictSSL |
|||
if (this.strictSSL === undefined) this.strictSSL = true |
|||
|
|||
this.userAgent = options.userAgent |
|||
if (this.userAgent === undefined) { |
|||
this.userAgent = 'node/' + process.version |
|||
} |
|||
|
|||
this.cacheMin = options.cacheMin || 0 |
|||
this.cacheMax = options.cacheMax || Infinity |
|||
|
|||
this.proxy = options.proxy |
|||
this.httpsProxy = options.httpsProxy || options.proxy |
|||
|
|||
this.log = options.log || npmlog |
|||
} |
|||
|
|||
require('fs').readdirSync(__dirname + "/lib").forEach(function (f) { |
|||
if (!f.match(/\.js$/)) return |
|||
RegClient.prototype[f.replace(/\.js$/, '')] = require('./lib/' + f) |
|||
}) |
@ -0,0 +1,99 @@ |
|||
|
|||
module.exports = adduser |
|||
|
|||
var uuid = require("node-uuid") |
|||
, crypto |
|||
|
|||
try { |
|||
} catch (ex) {} |
|||
|
|||
function sha (s) { |
|||
return crypto.createHash("sha1").update(s).digest("hex") |
|||
} |
|||
|
|||
function adduser (username, password, email, cb) { |
|||
if (!crypto) crypto = require("crypto") |
|||
|
|||
password = ("" + (password || "")).trim() |
|||
if (!password) return cb(new Error("No password supplied.")) |
|||
|
|||
email = ("" + (email || "")).trim() |
|||
if (!email) return cb(new Error("No email address supplied.")) |
|||
if (!email.match(/^[^@]+@[^\.]+\.[^\.]+/)) { |
|||
return cb(new Error("Please use a real email address.")) |
|||
} |
|||
|
|||
if (password.indexOf(":") !== -1) return cb(new Error( |
|||
"Sorry, ':' chars are not allowed in passwords.\n"+ |
|||
"See <https://issues.apache.org/jira/browse/COUCHDB-969> for why.")) |
|||
|
|||
var salt = uuid() |
|||
, userobj = |
|||
{ name : username |
|||
, salt : salt |
|||
, password_sha : sha(password + salt) |
|||
, email : email |
|||
, _id : 'org.couchdb.user:'+username |
|||
, type : "user" |
|||
, roles : [] |
|||
, date: new Date().toISOString() |
|||
} |
|||
|
|||
cb = done.call(this, cb) |
|||
|
|||
this.log.verbose("adduser", "before first PUT", userobj) |
|||
this.request('PUT' |
|||
, '/-/user/org.couchdb.user:'+encodeURIComponent(username) |
|||
, userobj |
|||
, function (error, data, json, response) { |
|||
// if it worked, then we just created a new user, and all is well.
|
|||
// but if we're updating a current record, then it'll 409 first
|
|||
if (error && !this.auth) { |
|||
// must be trying to re-auth on a new machine.
|
|||
// use this info as auth
|
|||
var b = new Buffer(username + ":" + password) |
|||
this.auth = b.toString("base64") |
|||
} |
|||
|
|||
if (!error || !response || response.statusCode !== 409) { |
|||
return cb(error, data, json, response) |
|||
} |
|||
|
|||
this.log.verbose("adduser", "update existing user") |
|||
return this.request('GET' |
|||
, '/-/user/org.couchdb.user:'+encodeURIComponent(username) |
|||
, function (er, data, json, response) { |
|||
Object.keys(data).forEach(function (k) { |
|||
userobj[k] = data[k] |
|||
}) |
|||
this.log.verbose("adduser", "userobj", userobj) |
|||
this.request('PUT' |
|||
, '/-/user/org.couchdb.user:'+encodeURIComponent(username) |
|||
+ "/-rev/" + userobj._rev |
|||
, userobj |
|||
, cb ) |
|||
}.bind(this)) |
|||
}.bind(this)) |
|||
} |
|||
|
|||
function done (cb) { |
|||
return function (error, data, json, response) { |
|||
if (!error && (!response || response.statusCode === 201)) { |
|||
return cb(error, data, json, response) |
|||
} |
|||
this.log.verbose("adduser", "back", [error, data, json]) |
|||
if (!error) { |
|||
error = new Error( (response && response.statusCode || "") + " "+ |
|||
"Could not create user\n"+JSON.stringify(data)) |
|||
} |
|||
if (response |
|||
&& (response.statusCode === 401 || response.statusCode === 403)) { |
|||
this.log.warn("adduser", "Incorrect username or password\n" |
|||
+"You can reset your account by visiting:\n" |
|||
+"\n" |
|||
+" http://admin.npmjs.org/reset\n") |
|||
} |
|||
|
|||
return cb(error) |
|||
}.bind(this) |
|||
} |
@ -0,0 +1,172 @@ |
|||
|
|||
module.exports = get |
|||
|
|||
var fs = require("graceful-fs") |
|||
, path = require("path") |
|||
, mkdir = require("mkdirp") |
|||
, chownr = require("chownr") |
|||
|
|||
function get (uri, timeout, nofollow, staleOk, cb) { |
|||
if (typeof cb !== "function") cb = staleOk, staleOk = false |
|||
if (typeof cb !== "function") cb = nofollow, nofollow = false |
|||
if (typeof cb !== "function") cb = timeout, timeout = -1 |
|||
if (typeof cb !== "function") cb = version, version = null |
|||
|
|||
timeout = Math.min(timeout, this.cacheMax) |
|||
timeout = Math.max(timeout, this.cacheMin) |
|||
|
|||
if ( process.env.COMP_CWORD !== undefined |
|||
&& process.env.COMP_LINE !== undefined |
|||
&& process.env.COMP_POINT !== undefined |
|||
) timeout = Math.max(timeout, 60000) |
|||
|
|||
// /-/all is special.
|
|||
// It uses timestamp-based caching and partial updates,
|
|||
// because it is a monster.
|
|||
if (uri === "/-/all") { |
|||
return requestAll.call(this, cb) |
|||
} |
|||
|
|||
var cache = path.join(this.cache, uri, ".cache.json") |
|||
fs.stat(cache, function (er, stat) { |
|||
if (!er) fs.readFile(cache, function (er, data) { |
|||
try { data = JSON.parse(data) } |
|||
catch (ex) { data = null } |
|||
get_.call(this, uri, timeout, cache, stat, data, nofollow, staleOk, cb) |
|||
}.bind(this)) |
|||
else get_.call(this, uri, timeout, cache, null, null, nofollow, staleOk, cb) |
|||
}.bind(this)) |
|||
} |
|||
|
|||
function requestAll (cb) { |
|||
var cache = path.join(this.cache, "/-/all", ".cache.json") |
|||
|
|||
mkdir(path.join(this.cache, "-", "all"), function (er) { |
|||
fs.readFile(cache, function (er, data) { |
|||
if (er) return requestAll_(0, {}, cb) |
|||
try { |
|||
data = JSON.parse(data) |
|||
} catch (ex) { |
|||
fs.writeFile(cache, "{}", function (er) { |
|||
if (er) return cb(new Error("Broken cache.")) |
|||
return requestAll_.call(this, 0, {}, cb) |
|||
}) |
|||
} |
|||
var t = +data._updated || 0 |
|||
requestAll_.call(this, t, data, cb) |
|||
}.bind(this)) |
|||
}.bind(this)) |
|||
} |
|||
|
|||
function requestAll_ (c, data, cb) { |
|||
// use the cache and update in the background if it's not too old
|
|||
if (Date.now() - c < 60000) { |
|||
cb(null, data) |
|||
cb = function () {} |
|||
} |
|||
|
|||
var uri = "/-/all/since?stale=update_after&startkey=" + c |
|||
|
|||
if (c === 0) { |
|||
this.log.warn("", "Building the local index for the first time, please be patient") |
|||
uri = "/-/all" |
|||
} |
|||
|
|||
var cache = path.join(this.cache, "-/all", ".cache.json") |
|||
this.request('GET', uri, function (er, updates, _, res) { |
|||
if (er) return cb(er, data) |
|||
var headers = res.headers |
|||
, updated = Date.parse(headers.date) |
|||
Object.keys(updates).forEach(function (p) { |
|||
data[p] = updates[p] |
|||
}) |
|||
data._updated = updated |
|||
fs.writeFile( cache, JSON.stringify(data) |
|||
, function (er) { |
|||
delete data._updated |
|||
return cb(er, data) |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
function get_ (uri, timeout, cache, stat, data, nofollow, staleOk, cb) { |
|||
var etag |
|||
if (data && data._etag) etag = data._etag |
|||
if (timeout && timeout > 0 && stat && data) { |
|||
if ((Date.now() - stat.mtime.getTime())/1000 < timeout) { |
|||
this.log.verbose("registry.get", uri, "not expired, no request") |
|||
delete data._etag |
|||
return cb(null, data, JSON.stringify(data), {statusCode:304}) |
|||
} |
|||
if (staleOk) { |
|||
this.log.verbose("registry.get", uri, "staleOk, background update") |
|||
delete data._etag |
|||
process.nextTick(cb.bind( null, null, data, JSON.stringify(data) |
|||
, {statusCode: 304} )) |
|||
cb = function () {} |
|||
} |
|||
} |
|||
|
|||
this.request('GET', uri, etag, nofollow, function (er, remoteData, raw, response) { |
|||
// if we get an error talking to the registry, but we have it
|
|||
// from the cache, then just pretend we got it.
|
|||
if (er && cache && data && !data.error) { |
|||
er = null |
|||
response = {statusCode: 304} |
|||
} |
|||
|
|||
if (response) { |
|||
this.log.silly("registry.get", "cb", [response.statusCode, response.headers]) |
|||
if (response.statusCode === 304 && etag) { |
|||
remoteData = data |
|||
this.log.verbose("etag", uri+" from cache") |
|||
} |
|||
} |
|||
|
|||
data = remoteData |
|||
if (!data) { |
|||
er = er || new Error("failed to fetch from registry: " + uri) |
|||
} |
|||
|
|||
if (er) return cb(er, data, raw, response) |
|||
|
|||
// just give the write the old college try. if it fails, whatever.
|
|||
function saved () { |
|||
delete data._etag |
|||
cb(er, data, raw, response) |
|||
} |
|||
|
|||
saveToCache.call(this, cache, data, saved) |
|||
}.bind(this)) |
|||
} |
|||
|
|||
function saveToCache (cache, data, saved) { |
|||
if (this.cacheStat) { |
|||
var cs = this.cacheStat |
|||
return saveToCache_.call(this, cache, data, cs.uid, cs.gid, saved) |
|||
} |
|||
fs.stat(this.cache, function (er, st) { |
|||
if (er) { |
|||
return fs.stat(process.env.HOME || "", function (er, st) { |
|||
// if this fails, oh well.
|
|||
if (er) return saved() |
|||
this.cacheStat = st |
|||
return saveToCache.call(this, cache, data, saved) |
|||
}.bind(this)) |
|||
} |
|||
this.cacheStat = st || { uid: null, gid: null } |
|||
return saveToCache.call(this, cache, data, saved) |
|||
}.bind(this)) |
|||
} |
|||
|
|||
function saveToCache_ (cache, data, uid, gid, saved) { |
|||
mkdir(path.dirname(cache), function (er, made) { |
|||
if (er) return saved() |
|||
fs.writeFile(cache, JSON.stringify(data), function (er) { |
|||
if (er || uid === null || gid === null) { |
|||
return saved() |
|||
} |
|||
chownr(made || cache, uid, gid, saved) |
|||
}) |
|||
}) |
|||
} |
@ -0,0 +1,119 @@ |
|||
|
|||
module.exports = publish |
|||
|
|||
var path = require("path") |
|||
, url = require("url") |
|||
|
|||
function publish (data, tarball, readme, cb) { |
|||
if (typeof cb !== "function") cb = readme, readme = "" |
|||
|
|||
if (!this.email || !this.auth || !this.username) { |
|||
return cb(new Error("auth and email required for publishing")) |
|||
} |
|||
|
|||
// add the dist-url to the data, pointing at the tarball.
|
|||
// if the {name} isn't there, then create it.
|
|||
// if the {version} is already there, then fail.
|
|||
// then:
|
|||
// PUT the data to {config.registry}/{data.name}/{data.version}
|
|||
var registry = this.registry |
|||
|
|||
readme = readme ? "" + readme : "" |
|||
|
|||
var fullData = |
|||
{ _id : data.name |
|||
, name : data.name |
|||
, description : data.description |
|||
, "dist-tags" : {} |
|||
, versions : {} |
|||
, readme: readme |
|||
, maintainers : |
|||
[ { name : this.username |
|||
, email : this.email |
|||
} |
|||
] |
|||
} |
|||
|
|||
var tbName = data.name + "-" + data.version + ".tgz" |
|||
, tbURI = data.name + "/-/" + tbName |
|||
|
|||
data._id = data.name+"@"+data.version |
|||
data.dist = data.dist || {} |
|||
data.dist.tarball = url.resolve(registry, tbURI) |
|||
.replace(/^https:\/\//, "http://") |
|||
|
|||
|
|||
// first try to just PUT the whole fullData, and this will fail if it's
|
|||
// already there, because it'll be lacking a _rev, so couch'll bounce it.
|
|||
this.request("PUT", encodeURIComponent(data.name), fullData, |
|||
function (er, parsed, json, response) { |
|||
// get the rev and then upload the attachment
|
|||
// a 409 is expected here, if this is a new version of an existing package.
|
|||
if (er |
|||
&& !(response && response.statusCode === 409) |
|||
&& !( parsed |
|||
&& parsed.reason === |
|||
"must supply latest _rev to update existing package" )) { |
|||
this.log.error("publish", "Failed PUT response " |
|||
+(response && response.statusCode)) |
|||
return cb(er) |
|||
} |
|||
var dataURI = encodeURIComponent(data.name) |
|||
+ "/" + encodeURIComponent(data.version) |
|||
|
|||
var tag = data.tag || this.defaultTag || "latest" |
|||
dataURI += "/-tag/" + tag |
|||
|
|||
// let's see what versions are already published.
|
|||
// could be that we just need to update the bin dist values.
|
|||
this.request("GET", data.name, function (er, fullData) { |
|||
if (er) return cb(er) |
|||
|
|||
var exists = fullData.versions && fullData.versions[data.version] |
|||
if (exists) return cb(conflictError.call(this, data._id)) |
|||
|
|||
// this way, it'll also get attached to packages that were previously
|
|||
// published with a version of npm that lacked this feature.
|
|||
if (!fullData.readme) { |
|||
data.readme = readme |
|||
} |
|||
|
|||
this.request("PUT", dataURI, data, function (er) { |
|||
if (er) { |
|||
if (er.message.indexOf("conflict Document update conflict.") === 0) { |
|||
return cb(conflictError.call(this, data._id)) |
|||
} |
|||
this.log.error("publish", "Error sending version data") |
|||
return cb(er) |
|||
} |
|||
|
|||
this.log.verbose("publish", "attach 2", [data.name, tarball, tbName]) |
|||
attach.call(this, data.name, tarball, tbName, function (er) { |
|||
this.log.verbose("publish", "attach 3" |
|||
,[er, data.name]) |
|||
return cb(er) |
|||
}.bind(this)) |
|||
}.bind(this)) |
|||
}.bind(this)) |
|||
}.bind(this)) // pining for fat arrows.
|
|||
} |
|||
|
|||
function conflictError (pkgid) { |
|||
var e = new Error("publish fail") |
|||
e.code = "EPUBLISHCONFLICT" |
|||
e.pkgid = pkgid |
|||
return e |
|||
} |
|||
|
|||
function attach (doc, file, filename, cb) { |
|||
doc = encodeURIComponent(doc) |
|||
this.request("GET", doc, function (er, d) { |
|||
if (er) return cb(er) |
|||
if (!d) return cb(new Error( |
|||
"Attempting to upload to invalid doc "+doc)) |
|||
var rev = "-rev/"+d._rev |
|||
, attURI = doc + "/-/" + encodeURIComponent(filename) + "/" + rev |
|||
this.log.verbose("uploading", [attURI, file]) |
|||
this.upload(attURI, file, cb) |
|||
}.bind(this)) |
|||
} |
@ -0,0 +1,202 @@ |
|||
module.exports = regRequest |
|||
|
|||
var url = require("url") |
|||
, fs = require("graceful-fs") |
|||
, rm = require("rimraf") |
|||
, asyncMap = require("slide").asyncMap |
|||
, Stream = require("stream").Stream |
|||
, request = require("request") |
|||
|
|||
function regRequest (method, where, what, etag, nofollow, cb_) { |
|||
if (typeof cb_ !== "function") cb_ = nofollow, nofollow = false |
|||
if (typeof cb_ !== "function") cb_ = etag, etag = null |
|||
if (typeof cb_ !== "function") cb_ = what, what = null |
|||
|
|||
// Since there are multiple places where an error could occur,
|
|||
// don't let the cb be called more than once.
|
|||
var errState = null |
|||
function cb (er) { |
|||
if (errState) return |
|||
if (er) errState = er |
|||
cb_.apply(null, arguments) |
|||
} |
|||
|
|||
if (where.match(/^\/?favicon.ico/)) { |
|||
return cb(new Error("favicon.ico isn't a package, it's a picture.")) |
|||
} |
|||
|
|||
var registry = this.registry |
|||
|
|||
var adduserChange = /^\/?-\/user\/org\.couchdb\.user:([^\/]+)\/-rev/ |
|||
, adduserNew = /^\/?-\/user\/org\.couchdb\.user:([^\/]+)/ |
|||
, authRequired = (what || this.alwaysAuth) |
|||
&& !where.match(adduserNew) |
|||
|| where.match(adduserChange) |
|||
|| method === "DELETE" |
|||
|
|||
// resolve to a full url on the registry
|
|||
if (!where.match(/^https?:\/\//)) { |
|||
this.log.verbose("url raw", where) |
|||
|
|||
var q = where.split("?") |
|||
where = q.shift() |
|||
q = q.join("?") |
|||
|
|||
if (where.charAt(0) !== "/") where = "/" + where |
|||
where = "." + where.split("/").map(function (p) { |
|||
p = p.trim() |
|||
if (p.match(/^org.couchdb.user/)) { |
|||
return p.replace(/\//g, encodeURIComponent("/")) |
|||
} |
|||
return encodeURIComponent(p) |
|||
}).join("/") |
|||
if (q) where += "?" + q |
|||
this.log.verbose("url resolving", [registry, where]) |
|||
where = url.resolve(registry, where) |
|||
this.log.verbose("url resolved", where) |
|||
} |
|||
|
|||
var remote = url.parse(where) |
|||
, auth = authRequired && this.auth |
|||
|
|||
if (authRequired && !auth) { |
|||
return cb(new Error( |
|||
"Cannot insert data into the registry without auth")) |
|||
} |
|||
|
|||
if (auth) { |
|||
remote.auth = new Buffer(auth, "base64").toString("utf8") |
|||
} |
|||
|
|||
makeRequest.call(this, method, remote, where, what, etag, nofollow, cb) |
|||
} |
|||
|
|||
function makeRequest (method, remote, where, what, etag, nofollow, cb) { |
|||
var opts = { url: remote |
|||
, method: method |
|||
, ca: this.ca |
|||
, strictSSL: this.strictSSL } |
|||
, headers = opts.headers = {} |
|||
if (etag) { |
|||
this.log.verbose("etag", etag) |
|||
headers[method === "GET" ? "if-none-match" : "if-match"] = etag |
|||
} |
|||
|
|||
headers.accept = "application/json" |
|||
|
|||
headers["user-agent"] = this.userAgent |
|||
|
|||
opts.proxy = remote.protocol === "https:" |
|||
? this.httpsProxy : this.proxy |
|||
|
|||
// figure out wth 'what' is
|
|||
if (what) { |
|||
if (Buffer.isBuffer(what) || typeof what === "string") { |
|||
opts.body = what |
|||
headers["content-type"] = "application/json" |
|||
headers["content-length"] = Buffer.byteLength(what) |
|||
} else if (what instanceof Stream) { |
|||
headers["content-type"] = "application/octet-stream" |
|||
if (what.size) headers["content-length"] = what.size |
|||
} else { |
|||
delete what._etag |
|||
opts.json = what |
|||
} |
|||
} |
|||
|
|||
if (nofollow) { |
|||
opts.followRedirect = false |
|||
} |
|||
|
|||
this.log.http(method, remote.href || "/") |
|||
|
|||
var done = requestDone.call(this, method, where, cb) |
|||
var req = request(opts, done) |
|||
|
|||
req.on("error", cb) |
|||
|
|||
if (what && (what instanceof Stream)) { |
|||
what.pipe(req) |
|||
} |
|||
} |
|||
|
|||
// cb(er, parsed, raw, response)
|
|||
function requestDone (method, where, cb) { |
|||
return function (er, response, data) { |
|||
if (er) return cb(er) |
|||
|
|||
this.log.http(response.statusCode, url.parse(where).href) |
|||
|
|||
var parsed |
|||
|
|||
if (Buffer.isBuffer(data)) { |
|||
data = data.toString() |
|||
} |
|||
|
|||
if (data && typeof data === "string" && response.statusCode !== 304) { |
|||
try { |
|||
parsed = JSON.parse(data) |
|||
} catch (ex) { |
|||
ex.message += "\n" + data |
|||
this.log.verbose("bad json", data) |
|||
this.log.error("registry", "error parsing json") |
|||
return cb(ex, null, data, response) |
|||
} |
|||
} else if (data) { |
|||
parsed = data |
|||
data = JSON.stringify(parsed) |
|||
} |
|||
|
|||
// expect data with any error codes
|
|||
if (!data && response.statusCode >= 400) { |
|||
return cb( response.statusCode + " " |
|||
+ require("http").STATUS_CODES[response.statusCode] |
|||
, null, data, response ) |
|||
} |
|||
|
|||
var er = null |
|||
if (parsed && response.headers.etag) { |
|||
parsed._etag = response.headers.etag |
|||
} |
|||
|
|||
if (parsed && parsed.error && response.statusCode >= 400) { |
|||
var w = url.parse(where).pathname.substr(1) |
|||
if (!w.match(/^-/) && parsed.error === "not_found") { |
|||
w = w.split("/") |
|||
name = w[w.indexOf("_rewrite") + 1] |
|||
er = new Error("404 Not Found: "+name) |
|||
er.code = "E404" |
|||
er.pkgid = name |
|||
} else { |
|||
er = new Error( |
|||
parsed.error + " " + (parsed.reason || "") + ": " + w) |
|||
} |
|||
} else if (method !== "HEAD" && method !== "GET") { |
|||
// invalidate cache
|
|||
// This is irrelevant for commands that do etag caching, but
|
|||
// ls and view also have a timed cache, so this keeps the user
|
|||
// from thinking that it didn't work when it did.
|
|||
// Note that failure is an acceptable option here, since the
|
|||
// only result will be a stale cache for some helper commands.
|
|||
var path = require("path") |
|||
, p = url.parse(where).pathname.split("/") |
|||
, _ = "/" |
|||
, caches = p.map(function (part) { |
|||
return _ = path.join(_, part) |
|||
}).map(function (cache) { |
|||
return path.join(this.cache, cache, ".cache.json") |
|||
}, this) |
|||
|
|||
// if the method is DELETE, then also remove the thing itself.
|
|||
// Note that the search index is probably invalid. Whatever.
|
|||
// That's what you get for deleting stuff. Don't do that.
|
|||
if (method === "DELETE") { |
|||
p = p.slice(0, p.indexOf("-rev")) |
|||
caches.push(path.join(this.cache, p.join("/"))) |
|||
} |
|||
|
|||
asyncMap(caches, rm, function () {}) |
|||
} |
|||
return cb(er, parsed, data, response) |
|||
}.bind(this) |
|||
} |
@ -0,0 +1,29 @@ |
|||
|
|||
module.exports = star |
|||
|
|||
function star (package, starred, cb) { |
|||
if (!this.username) return cb(new Error( |
|||
"Must be logged in to star/unstar packages")) |
|||
|
|||
var users = {} |
|||
|
|||
this.request("GET", package, function (er, fullData) { |
|||
if (er) return cb(er) |
|||
|
|||
fullData = { _id: fullData._id |
|||
, _rev: fullData._rev |
|||
, users: fullData.users || {} } |
|||
|
|||
if (starred) { |
|||
this.log.info("starring", fullData._id) |
|||
fullData.users[this.username] = true |
|||
this.log.verbose("starring", fullData) |
|||
} else { |
|||
delete fullData.users[this.username] |
|||
this.log.info("unstarring", fullData._id) |
|||
this.log.verbose("unstarring", fullData) |
|||
} |
|||
|
|||
return this.request("PUT", package, fullData, cb) |
|||
}.bind(this)) |
|||
} |
@ -0,0 +1,6 @@ |
|||
|
|||
module.exports = tag |
|||
|
|||
function tag (project, version, tag, cb) { |
|||
this.request("PUT", project+"/"+tag, JSON.stringify(version), cb) |
|||
} |
@ -0,0 +1,103 @@ |
|||
|
|||
// fetch the data
|
|||
// modify to remove the version in question
|
|||
// If no versions remaining, then DELETE
|
|||
// else, PUT the modified data
|
|||
// delete the tarball
|
|||
|
|||
module.exports = unpublish |
|||
|
|||
var semver = require("semver") |
|||
, url = require("url") |
|||
, chain = require("slide").chain |
|||
|
|||
function unpublish (name, ver, cb) { |
|||
if (typeof cb !== "function") cb = ver, ver = null |
|||
|
|||
this.get(name, null, -1, true, function (er, data) { |
|||
if (er) { |
|||
this.log.info("unpublish", name+" not published") |
|||
return cb() |
|||
} |
|||
// remove all if no version specified
|
|||
if (!ver) { |
|||
this.log.info("unpublish", "No version specified, removing all") |
|||
return this.request("DELETE", name+'/-rev/'+data._rev, cb) |
|||
} |
|||
|
|||
var versions = data.versions || {} |
|||
, versionPublic = versions.hasOwnProperty(ver) |
|||
|
|||
if (!versionPublic) { |
|||
this.log.info("unpublish", name+"@"+ver+" not published") |
|||
} else { |
|||
var dist = versions[ver].dist |
|||
this.log.verbose("unpublish", "removing attachments", dist) |
|||
} |
|||
|
|||
delete versions[ver] |
|||
// if it was the only version, then delete the whole package.
|
|||
if (!Object.keys(versions).length) { |
|||
this.log.info("unpublish", "No versions remain, removing entire package") |
|||
return this.request("DELETE", name+"/-rev/"+data._rev, cb) |
|||
} |
|||
|
|||
if (!versionPublic) return cb() |
|||
|
|||
var latestVer = data["dist-tags"].latest |
|||
for (var tag in data["dist-tags"]) { |
|||
if (data["dist-tags"][tag] === ver) delete data["dist-tags"][tag] |
|||
} |
|||
|
|||
if (latestVer === ver) { |
|||
data["dist-tags"].latest = |
|||
Object.getOwnPropertyNames(versions).sort(semver.compare).pop() |
|||
} |
|||
|
|||
var rev = data._rev |
|||
delete data._revisions |
|||
delete data._attachments |
|||
var cb_ = detacher.call(this, data, dist, cb) |
|||
this.request("PUT", name+"/-rev/"+rev, data, function (er) { |
|||
if (er) { |
|||
this.log.error("unpublish", "Failed to update data") |
|||
} |
|||
cb_(er) |
|||
}.bind(this)) |
|||
}.bind(this)) |
|||
} |
|||
|
|||
function detacher (data, dist, cb) { |
|||
return function (er) { |
|||
if (er) return cb(er) |
|||
this.get(data.name, function (er, data) { |
|||
if (er) return cb(er) |
|||
|
|||
var tb = url.parse(dist.tarball) |
|||
|
|||
detach.call(this, data, tb.pathname, data._rev, function (er) { |
|||
if (er || !dist.bin) return cb(er) |
|||
chain(Object.keys(dist.bin).map(function (bt) { |
|||
return function (cb) { |
|||
var d = dist.bin[bt] |
|||
detach.call(this, data, url.parse(d.tarball).pathname, null, cb) |
|||
}.bind(this) |
|||
}, this), cb) |
|||
}.bind(this)) |
|||
}.bind(this)) |
|||
}.bind(this) |
|||
} |
|||
|
|||
function detach (data, path, rev, cb) { |
|||
if (rev) { |
|||
path += "/-rev/" + rev |
|||
this.log.info("detach", path) |
|||
return this.request("DELETE", path, cb) |
|||
} |
|||
this.get(data.name, function (er, data) { |
|||
rev = data._rev |
|||
if (!rev) return cb(new Error( |
|||
"No _rev found in "+data._id)) |
|||
detach.call(this, data, path, rev, cb) |
|||
}.bind(this)) |
|||
} |
@ -0,0 +1,22 @@ |
|||
module.exports = upload |
|||
|
|||
var fs = require('fs') |
|||
, Stream = require("stream").Stream |
|||
|
|||
function upload (where, file, etag, nofollow, cb) { |
|||
if (typeof nofollow === "function") cb = nofollow, nofollow = false |
|||
if (typeof etag === "function") cb = etag, etag = null |
|||
|
|||
if (file instanceof Stream) { |
|||
return this.request("PUT", where, file, etag, nofollow, cb) |
|||
} |
|||
|
|||
fs.stat(file, function (er, stat) { |
|||
if (er) return cb(er) |
|||
var s = fs.createReadStream(file) |
|||
s.size = stat.size |
|||
s.on("error", cb) |
|||
|
|||
this.request("PUT", where, s, etag, nofollow, cb) |
|||
}.bind(this)) |
|||
} |
@ -0,0 +1,47 @@ |
|||
{ |
|||
"author": { |
|||
"name": "Isaac Z. Schlueter", |
|||
"email": "i@izs.me", |
|||
"url": "http://blog.izs.me/" |
|||
}, |
|||
"name": "npm-registry-client", |
|||
"description": "Client for the npm registry", |
|||
"version": "0.0.4", |
|||
"repository": { |
|||
"url": "git://github.com/isaacs/npm-registry-client" |
|||
}, |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"test": "tap test/*.js" |
|||
}, |
|||
"dependencies": { |
|||
"node-uuid": "~1.3.3", |
|||
"request": "~2.9.202", |
|||
"graceful-fs": "~1.1.8", |
|||
"semver": "~1.0.14", |
|||
"slide": "~1.1.3", |
|||
"chownr": "0", |
|||
"mkdirp": "~0.3.3", |
|||
"rimraf": "~2.0.1", |
|||
"npmlog": "" |
|||
}, |
|||
"devDependencies": { |
|||
"tap": "" |
|||
}, |
|||
"optionalDependencies": { |
|||
"npmlog": "" |
|||
}, |
|||
"engines": { |
|||
"node": "*" |
|||
}, |
|||
"_npmUser": { |
|||
"name": "isaacs", |
|||
"email": "i@izs.me" |
|||
}, |
|||
"_id": "npm-registry-client@0.0.4", |
|||
"_engineSupported": true, |
|||
"_npmVersion": "1.1.25", |
|||
"_nodeVersion": "v0.7.10-pre", |
|||
"_defaultsLoaded": true, |
|||
"_from": "npm-registry-client@0" |
|||
} |
@ -0,0 +1,25 @@ |
|||
Copyright (c) Isaac Z. Schlueter |
|||
All rights reserved. |
|||
|
|||
The BSD License |
|||
|
|||
Redistribution and use in source and binary forms, with or without |
|||
modification, are permitted provided that the following conditions |
|||
are met: |
|||
1. Redistributions of source code must retain the above copyright |
|||
notice, this list of conditions and the following disclaimer. |
|||
2. Redistributions in binary form must reproduce the above copyright |
|||
notice, this list of conditions and the following disclaimer in the |
|||
documentation and/or other materials provided with the distribution. |
|||
|
|||
THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS |
|||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
|||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS |
|||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
|||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
|||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
|||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
|||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|||
POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,153 @@ |
|||
# npmlog |
|||
|
|||
The logger util that npm uses. |
|||
|
|||
This logger is very basic. It does the logging for npm. It supports |
|||
custom levels and colored output. |
|||
|
|||
By default, logs are written to stderr. If you want to send log messages |
|||
to outputs other than streams, then you can change the `log.stream` |
|||
member, or you can just listen to the events that it emits, and do |
|||
whatever you want with them. |
|||
|
|||
# Basic Usage |
|||
|
|||
``` |
|||
var log = require('npmlog') |
|||
|
|||
// additional stuff ---------------------------+ |
|||
// message ----------+ | |
|||
// prefix ----+ | | |
|||
// level -+ | | | |
|||
// v v v v |
|||
log.info('fyi', 'I have a kitty cat: %j', myKittyCat) |
|||
``` |
|||
|
|||
## log.level |
|||
|
|||
* {String} |
|||
|
|||
The level to display logs at. Any logs at or above this level will be |
|||
displayed. The special level `silent` will prevent anything from being |
|||
displayed ever. |
|||
|
|||
## log.record |
|||
|
|||
* {Array} |
|||
|
|||
An array of all the log messages that have been entered. |
|||
|
|||
## log.maxRecordSize |
|||
|
|||
* {Number} |
|||
|
|||
The maximum number of records to keep. If log.record gets bigger than |
|||
10% over this value, then it is sliced down to 90% of this value. |
|||
|
|||
The reason for the 10% window is so that it doesn't have to resize a |
|||
large array on every log entry. |
|||
|
|||
## log.prefixStyle |
|||
|
|||
* {Object} |
|||
|
|||
A style object that specifies how prefixes are styled. (See below) |
|||
|
|||
## log.headingStyle |
|||
|
|||
* {Object} |
|||
|
|||
A style object that specifies how the heading is styled. (See below) |
|||
|
|||
## log.heading |
|||
|
|||
* {String} Default: "" |
|||
|
|||
If set, a heading that is printed at the start of every line. |
|||
|
|||
## log.stream |
|||
|
|||
* {Stream} Default: `process.stderr` |
|||
|
|||
The stream where output is written. |
|||
|
|||
## log.enableColor() |
|||
|
|||
Force colors to be used on all messages, regardless of the output |
|||
stream. |
|||
|
|||
## log.disableColor() |
|||
|
|||
Disable colors on all messages. |
|||
|
|||
## log.pause() |
|||
|
|||
Stop emitting messages to the stream, but do not drop them. |
|||
|
|||
## log.resume() |
|||
|
|||
Emit all buffered messages that were written while paused. |
|||
|
|||
## log.log(level, prefix, message, ...) |
|||
|
|||
* `level` {String} The level to emit the message at |
|||
* `prefix` {String} A string prefix. Set to "" to skip. |
|||
* `message...` Arguments to `util.format` |
|||
|
|||
Emit a log message at the specified level. |
|||
|
|||
## log\[level](prefix, message, ...) |
|||
|
|||
For example, |
|||
|
|||
* log.silly(prefix, message, ...) |
|||
* log.verbose(prefix, message, ...) |
|||
* log.info(prefix, message, ...) |
|||
* log.http(prefix, message, ...) |
|||
* log.warn(prefix, message, ...) |
|||
* log.error(prefix, message, ...) |
|||
|
|||
Like `log.log(level, prefix, message, ...)`. In this way, each level is |
|||
given a shorthand, so you can do `log.info(prefix, message)`. |
|||
|
|||
## log.addLevel(level, n, style, disp) |
|||
|
|||
* `level` {String} Level indicator |
|||
* `n` {Number} The numeric level |
|||
* `style` {Object} Object with fg, bg, inverse, etc. |
|||
* `disp` {String} Optional replacement for `level` in the output. |
|||
|
|||
Sets up a new level with a shorthand function and so forth. |
|||
|
|||
Note that if the number is `Infinity`, then setting the level to that |
|||
will cause all log messages to be suppressed. If the number is |
|||
`-Infinity`, then the only way to show it is to enable all log messages. |
|||
|
|||
# Events |
|||
|
|||
Events are all emitted with the message object. |
|||
|
|||
* `log` Emitted for all messages |
|||
* `log.<level>` Emitted for all messages with the `<level>` level. |
|||
* `<prefix>` Messages with prefixes also emit their prefix as an event. |
|||
|
|||
# Style Objects |
|||
|
|||
Style objects can have the following fields: |
|||
|
|||
* `fg` {String} Color for the foreground text |
|||
* `bg` {String} Color for the background |
|||
* `bold`, `inverse`, `underline` {Boolean} Set the associated property |
|||
* `bell` {Boolean} Make a noise (This is pretty annoying, probably.) |
|||
|
|||
# Message Objects |
|||
|
|||
Every log event is emitted with a message object, and the `log.record` |
|||
list contains all of them that have been created. They have the |
|||
following fields: |
|||
|
|||
* `id` {Number} |
|||
* `level` {String} |
|||
* `prefix` {String} |
|||
* `message` {String} Result of `util.format()` |
|||
* `messageRaw` {Array} Arguments to `util.format()` |
@ -0,0 +1,154 @@ |
|||
var EE = require('events').EventEmitter |
|||
var log = exports = module.exports = new EE |
|||
var util = require('util') |
|||
|
|||
var ansi = require('ansi') |
|||
log.cursor = ansi(process.stderr) |
|||
log.stream = process.stderr |
|||
|
|||
// by default, let ansi decide based on tty-ness.
|
|||
var colorEnabled = undefined |
|||
log.enableColor = function () { |
|||
colorEnabled = true |
|||
this.cursor.enabled = true |
|||
} |
|||
log.disableColor = function () { |
|||
colorEnabled = false |
|||
this.cursor.enabled = false |
|||
} |
|||
|
|||
// default level
|
|||
log.level = 'info' |
|||
|
|||
// temporarily stop emitting, but don't drop
|
|||
log.pause = function () { |
|||
this._paused = true |
|||
} |
|||
|
|||
log.resume = function () { |
|||
if (!this._paused) return |
|||
this._paused = false |
|||
|
|||
var b = this._buffer |
|||
this._buffer = [] |
|||
b.forEach(function (m) { |
|||
this.emitLog(m) |
|||
}, this) |
|||
} |
|||
|
|||
log._buffer = [] |
|||
|
|||
var id = 0 |
|||
log.record = [] |
|||
log.maxRecordSize = 10000 |
|||
log.log = function (lvl, prefix, message) { |
|||
var l = this.levels[lvl] |
|||
if (l === undefined) { |
|||
return this.emit('error', new Error(util.format( |
|||
'Undefined log level: %j', lvl))) |
|||
} |
|||
|
|||
var a = new Array(arguments.length - 2) |
|||
var stack = null |
|||
for (var i = 2; i < arguments.length; i ++) { |
|||
var arg = a[i-2] = arguments[i] |
|||
|
|||
// resolve stack traces to a plain string.
|
|||
if (typeof arg === 'object' && arg && |
|||
(arg instanceof Error) && arg.stack) { |
|||
arg.stack = stack = arg.stack + '' |
|||
} |
|||
} |
|||
if (stack) a.unshift(stack + '\n') |
|||
message = util.format.apply(util, a) |
|||
|
|||
var m = { id: id++, |
|||
level: lvl, |
|||
prefix: String(prefix || ''), |
|||
message: message, |
|||
messageRaw: a } |
|||
|
|||
this.emit('log', m) |
|||
this.emit('log.' + lvl, m) |
|||
if (m.prefix) this.emit(m.prefix, m) |
|||
|
|||
this.record.push(m) |
|||
var mrs = this.maxRecordSize |
|||
var n = this.record.length - mrs |
|||
if (n > mrs / 10) { |
|||
var newSize = Math.floor(mrs * 0.9) |
|||
this.record = this.record.slice(-1 * newSize) |
|||
} |
|||
|
|||
this.emitLog(m) |
|||
} |
|||
|
|||
log.emitLog = function (m) { |
|||
if (this._paused) { |
|||
this._buffer.push(m) |
|||
return |
|||
} |
|||
var l = this.levels[m.level] |
|||
if (l === undefined) return |
|||
if (l < this.levels[this.level]) return |
|||
if (l > 0 && !isFinite(l)) return |
|||
|
|||
var style = log.style[m.level] |
|||
var disp = log.disp[m.level] || m.level |
|||
m.message.split(/\r?\n/).forEach(function (line) { |
|||
if (this.heading) { |
|||
this.write(this.heading, this.headingStyle) |
|||
this.write(' ') |
|||
} |
|||
this.write(disp, log.style[m.level]) |
|||
var p = m.prefix || '' |
|||
if (p) this.write(' ') |
|||
this.write(p, this.prefixStyle) |
|||
this.write(' ' + line + '\n') |
|||
}, this) |
|||
} |
|||
|
|||
log.write = function (msg, style) { |
|||
if (!this.cursor) return |
|||
if (this.stream !== this.cursor.stream) { |
|||
this.cursor = ansi(this.stream, { enabled: colorEnabled }) |
|||
} |
|||
|
|||
style = style || {} |
|||
if (style.fg) this.cursor.fg[style.fg]() |
|||
if (style.bg) this.cursor.bg[style.bg]() |
|||
if (style.bold) this.cursor.bold() |
|||
if (style.underline) this.cursor.underline() |
|||
if (style.inverse) this.cursor.inverse() |
|||
if (style.beep) this.cursor.beep() |
|||
this.cursor.write(msg).reset() |
|||
} |
|||
|
|||
log.addLevel = function (lvl, n, style, disp) { |
|||
if (!disp) disp = lvl |
|||
this.levels[lvl] = n |
|||
this.style[lvl] = style |
|||
if (!this[lvl]) this[lvl] = function () { |
|||
var a = new Array(arguments.length + 1) |
|||
a[0] = lvl |
|||
for (var i = 0; i < arguments.length; i ++) { |
|||
a[i + 1] = arguments[i] |
|||
} |
|||
return this.log.apply(this, a) |
|||
} |
|||
this.disp[lvl] = disp |
|||
} |
|||
|
|||
log.prefixStyle = { fg: 'magenta' } |
|||
log.headingStyle = { fg: 'white', bg: 'black' } |
|||
|
|||
log.style = {} |
|||
log.levels = {} |
|||
log.disp = {} |
|||
log.addLevel('silly', -Infinity, { inverse: true }, 'sill') |
|||
log.addLevel('verbose', 1000, { fg: 'blue', bg: 'black' }, 'verb') |
|||
log.addLevel('info', 2000, { fg: 'green' }) |
|||
log.addLevel('http', 3000, { fg: 'green', bg: 'black' }) |
|||
log.addLevel('warn', 4000, { fg: 'black', bg: 'red' }, 'WARN') |
|||
log.addLevel('error', 5000, { fg: 'red', bg: 'black' }, 'ERR!') |
|||
log.addLevel('silent', Infinity) |
@ -0,0 +1,42 @@ |
|||
{ |
|||
"author": { |
|||
"name": "Isaac Z. Schlueter", |
|||
"email": "i@izs.me", |
|||
"url": "http://blog.izs.me/" |
|||
}, |
|||
"name": "npmlog", |
|||
"description": "logger for npm", |
|||
"version": "0.0.2", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git://github.com/isaacs/npmlog.git" |
|||
}, |
|||
"main": "log.js", |
|||
"scripts": { |
|||
"test": "tap test/*.js" |
|||
}, |
|||
"dependencies": { |
|||
"ansi": "~0.1.2" |
|||
}, |
|||
"devDependencies": { |
|||
"tap": "" |
|||
}, |
|||
"license": "BSD", |
|||
"_npmUser": { |
|||
"name": "isaacs", |
|||
"email": "i@izs.me" |
|||
}, |
|||
"_id": "npmlog@0.0.2", |
|||
"optionalDependencies": {}, |
|||
"engines": { |
|||
"node": "*" |
|||
}, |
|||
"_engineSupported": true, |
|||
"_npmVersion": "1.1.24", |
|||
"_nodeVersion": "v0.7.10-pre", |
|||
"_defaultsLoaded": true, |
|||
"dist": { |
|||
"shasum": "f0cf4b2c519950c00e91ba8e2868b62bf86254f6" |
|||
}, |
|||
"_from": "npmlog@0" |
|||
} |
@ -0,0 +1,25 @@ |
|||
Copyright (c) Isaac Z. Schlueter |
|||
All rights reserved. |
|||
|
|||
The BSD License |
|||
|
|||
Redistribution and use in source and binary forms, with or without |
|||
modification, are permitted provided that the following conditions |
|||
are met: |
|||
1. Redistributions of source code must retain the above copyright |
|||
notice, this list of conditions and the following disclaimer. |
|||
2. Redistributions in binary form must reproduce the above copyright |
|||
notice, this list of conditions and the following disclaimer in the |
|||
documentation and/or other materials provided with the distribution. |
|||
|
|||
THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS |
|||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
|||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS |
|||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
|||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
|||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
|||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
|||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|||
POSSIBILITY OF SUCH DAMAGE. |
Loading…
Reference in new issue