@leverege/build-tools 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.publishrc +15 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE.md +3 -0
- package/README.md +32 -0
- package/lib/server/auditate/help.js +119 -0
- package/lib/server/auditate/index.js +23 -0
- package/lib/server/auditate/report.js +61 -0
- package/lib/server/circleate +549 -0
- package/lib/server/decrypt-secrets +91 -0
- package/lib/server/dnsify/CloudflareAPI.js +103 -0
- package/lib/server/dnsify/accounts.json.enc +0 -0
- package/lib/server/dnsify/index.js +92 -0
- package/lib/server/dockreate +338 -0
- package/lib/server/encrypt-secrets +31 -0
- package/lib/server/getfbcfg +23 -0
- package/lib/server/getjson +48 -0
- package/lib/server/helmdn +103 -0
- package/lib/server/helmify +289 -0
- package/lib/server/helmup +87 -0
- package/lib/server/overwhelm +303 -0
- package/lib/server/packleate/Packleator.js +269 -0
- package/lib/server/packleate/index.js +98 -0
- package/lib/server/packleate/logo-ansi.js +17 -0
- package/lib/server/pkgck +136 -0
- package/lib/server/pkglint/index.js +19 -0
- package/lib/server/test.json +4 -0
- package/lib/web/auditate/help.js +158 -0
- package/lib/web/auditate/index.js +27 -0
- package/lib/web/auditate/report.js +91 -0
- package/lib/web/dnsify/CloudflareAPI.js +250 -0
- package/lib/web/dnsify/index.js +147 -0
- package/lib/web/packleate/Packleator.js +645 -0
- package/lib/web/packleate/index.js +157 -0
- package/lib/web/packleate/logo-ansi.js +3 -0
- package/lib/web/pkglint/index.js +19 -0
- package/package.json +74 -0
- package/src/auditate/help.js +83 -0
- package/src/auditate/index.js +25 -0
- package/src/auditate/report.js +54 -0
- package/src/circleate +549 -0
- package/src/decrypt-secrets +91 -0
- package/src/dnsify/CloudflareAPI.js +99 -0
- package/src/dnsify/accounts.json.enc +0 -0
- package/src/dnsify/index.js +82 -0
- package/src/dockreate +338 -0
- package/src/encrypt-secrets +31 -0
- package/src/getfbcfg +23 -0
- package/src/getjson +48 -0
- package/src/helmdn +103 -0
- package/src/helmify +289 -0
- package/src/helmup +87 -0
- package/src/overwhelm +303 -0
- package/src/packleate/Packleator.js +271 -0
- package/src/packleate/index.js +86 -0
- package/src/packleate/logo-ansi.js +16 -0
- package/src/pkgck +136 -0
- package/src/pkglint/index.js +21 -0
- package/src/test.json +4 -0
package/src/overwhelm
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const chalk = require( 'chalk' )
|
|
4
|
+
const cliArgs = require( 'command-line-args' )
|
|
5
|
+
const cliHelp = require( 'command-line-usage' )
|
|
6
|
+
const exec = require( 'child_process' ).execSync
|
|
7
|
+
const fs = require( 'fs' )
|
|
8
|
+
const glob = require( 'glob' )
|
|
9
|
+
const parse = require( 'parse-gitignore' )
|
|
10
|
+
const ask = require( 'readline-sync' )
|
|
11
|
+
const YAML = require( 'js-yaml' )
|
|
12
|
+
|
|
13
|
+
const optionsDefinitions = [
|
|
14
|
+
{ name : 'section', alias : 's', type : String, default : 'default' },
|
|
15
|
+
{ name : 'key', alias : 'k', type : String },
|
|
16
|
+
{ name : 'export', alias : 'e', type : Boolean, default : false },
|
|
17
|
+
{ name : 'continue', alias : 'c', type : Boolean, default : false },
|
|
18
|
+
{ name : 'verbose', alias : 'v', type : Boolean, default : false },
|
|
19
|
+
]
|
|
20
|
+
const args = cliArgs( optionsDefinitions )
|
|
21
|
+
|
|
22
|
+
// The kubernetes context is equal to the git branch name.
|
|
23
|
+
const context = require( 'git-branch' ).sync()
|
|
24
|
+
|
|
25
|
+
if ( context == 'master' ) {
|
|
26
|
+
console.log( '***ERROR: master context is forbidden; change to a different branch\n\n' )
|
|
27
|
+
process.exit( 1 )
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Load the default values and then overlay with the project specific yaml
|
|
31
|
+
// values. This supports the notion of this type of approach in the
|
|
32
|
+
// overwhelm yaml settings:
|
|
33
|
+
// default:
|
|
34
|
+
// GCE_MACHINE : "n1-standard-2"
|
|
35
|
+
//
|
|
36
|
+
// my-project:
|
|
37
|
+
// GCE_MACHINE : "n1-highcpu-8"
|
|
38
|
+
//
|
|
39
|
+
// where the my-project specific setting may override the default if it
|
|
40
|
+
// needs a beefier deployment. Any setting can be defaulted in this
|
|
41
|
+
// fashion, but it is advised not to set a default region with this
|
|
42
|
+
// approach in order to keep us cognizant of exactly where a cluster is
|
|
43
|
+
// being formed.
|
|
44
|
+
//
|
|
45
|
+
const yaml = YAML.safeLoad( fs.readFileSync( 'overwhelm.yaml', 'utf-8' ) )
|
|
46
|
+
const over = yaml.default
|
|
47
|
+
const cfgs = yaml[context]
|
|
48
|
+
|
|
49
|
+
if ( !cfgs ) {
|
|
50
|
+
console.log( `***ERROR: Unknown context => [${context}]\n` )
|
|
51
|
+
console.log( ' Known k8s contexts are:' )
|
|
52
|
+
Object.keys( yaml ).forEach( ( key ) => {
|
|
53
|
+
if ( key !== 'default' ) { console.log( ` ${key}` ) }
|
|
54
|
+
} )
|
|
55
|
+
console.log( `Add a section into overwhelm.yaml for ${context}\n\n` )
|
|
56
|
+
process.exit( 1 )
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Object.keys( cfgs ).forEach( ( key ) => { over[key] = cfgs[key] || over[key] } )
|
|
60
|
+
|
|
61
|
+
// Expand any variables that reference other variable settings. A perfect
|
|
62
|
+
// example of where this is useful is in the setting of a PROJECT_NAME and
|
|
63
|
+
// a PROJECT_ID. Most of the time these values are identical, which is why
|
|
64
|
+
// this setting exists in the default section:
|
|
65
|
+
// default:
|
|
66
|
+
// PROJECT_ID : ${PROJECT_NAME}
|
|
67
|
+
//
|
|
68
|
+
// However, in the case of projects like Siren Marine, this is the more
|
|
69
|
+
// likely setup due to legacy naming schemes:
|
|
70
|
+
// sire-marine:
|
|
71
|
+
// PROJECT_NAME : siren-marine
|
|
72
|
+
// PROJECT_ID : prod-rt-data
|
|
73
|
+
//
|
|
74
|
+
const varErrors = []
|
|
75
|
+
Object.keys( over ).forEach( ( key ) => {
|
|
76
|
+
if ( !over[key] ) return // no value to work with - skip it
|
|
77
|
+
|
|
78
|
+
// Look for something in the form of "thing-${VAR1}-${VAR2}-${VARn}-stuff"
|
|
79
|
+
const matches = over[key].match( /\$\{\w+\}/g )
|
|
80
|
+
if ( !matches ) return
|
|
81
|
+
|
|
82
|
+
matches.forEach( ( replaceThis ) => {
|
|
83
|
+
// The matched variable names will be in the form of ${VARNAME} so drop
|
|
84
|
+
// the ${} characters before using the VARNAME to map into the
|
|
85
|
+
// overwhelm map containing the desired replacement value.
|
|
86
|
+
const withThat = over[replaceThis.replace( /\W+/g, '' )]
|
|
87
|
+
if ( !withThat ) {
|
|
88
|
+
varErrors.push( `${key} : ${over[key]}` )
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
// Store the replaced string back in the overwhelm value map.
|
|
92
|
+
over[key] = over[key].replace( replaceThis, withThat )
|
|
93
|
+
} )
|
|
94
|
+
} )
|
|
95
|
+
|
|
96
|
+
if ( varErrors.length > 0 ) {
|
|
97
|
+
console.log( '***ERROR: unkown variable reference(s) in overwhelm.yaml:\n' )
|
|
98
|
+
varErrors.forEach( ( error ) => {
|
|
99
|
+
console.log( ` ${error}` )
|
|
100
|
+
} )
|
|
101
|
+
console.log('' )
|
|
102
|
+
process.exit( 1 )
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Grab a list of the existing yaml templates.
|
|
106
|
+
const yams = glob.sync( 'values-*.yaml' )
|
|
107
|
+
|
|
108
|
+
// A function to apply the replacements to all of the matching yaml which
|
|
109
|
+
// will error out if it runs into an unknown OVH<tag>.
|
|
110
|
+
const doReplacements = ( yamls, replacements ) => {
|
|
111
|
+
let replYaml = yamls
|
|
112
|
+
|
|
113
|
+
Object.keys( replacements ).forEach( ( repl ) => {
|
|
114
|
+
const replaceThis = new RegExp( `OVH:<${repl}>`, 'g' )
|
|
115
|
+
const withThat = `${replacements[repl]}`
|
|
116
|
+
replYaml = replYaml.replace( replaceThis, withThat )
|
|
117
|
+
} )
|
|
118
|
+
|
|
119
|
+
// Verify all replaceables have been handled, or error out.
|
|
120
|
+
const missed = replYaml.match( /OVH:<(.*)>/g )
|
|
121
|
+
|
|
122
|
+
if ( missed ) {
|
|
123
|
+
console.log( '\n***ERROR: missing replacements in overwhelm.yaml for these tags:\n' )
|
|
124
|
+
missed.forEach( ( miss ) => {
|
|
125
|
+
console.log( ` TAG: ${miss.match( /OVH:<(.*)>/)[1]}` )
|
|
126
|
+
} )
|
|
127
|
+
console.log('' )
|
|
128
|
+
process.exit( 1 )
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return replYaml
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Collect all of the yaml from the yams, and always expect the default and
|
|
135
|
+
// platform yamls to be in that list.
|
|
136
|
+
const buildYaml = ( yamls, replacements ) => {
|
|
137
|
+
// We always start with default but values-platform is no longer needed.
|
|
138
|
+
const always = [ 'values-default.yaml' ]
|
|
139
|
+
const values = [ ...always, ...yamls ]
|
|
140
|
+
const loaded = []
|
|
141
|
+
let allYamls = ''
|
|
142
|
+
|
|
143
|
+
values.forEach( ( yaml ) => {
|
|
144
|
+
if ( !fs.existsSync( yaml ) ) {
|
|
145
|
+
console.log( `\n***ERROR: yaml file missing => ${yaml}\n` )
|
|
146
|
+
process.exit( 1 )
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if ( !loaded.includes( yaml ) ) {
|
|
150
|
+
allYamls += `\n# AHOY => ${yaml}\n\n`
|
|
151
|
+
allYamls += fs.readFileSync( yaml, { encoding : 'utf-8' } )
|
|
152
|
+
loaded.push( yaml )
|
|
153
|
+
}
|
|
154
|
+
} )
|
|
155
|
+
|
|
156
|
+
// Now apply all of the replacements from the overwhelm file.
|
|
157
|
+
return doReplacements( allYamls, replacements )
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Call buildYaml to replace the OVH strings in the yaml files.
|
|
161
|
+
const values = buildYaml( yams, over )
|
|
162
|
+
|
|
163
|
+
// Check to see if someone is requesting a value from the yaml.
|
|
164
|
+
if ( args.key ) {
|
|
165
|
+
const section = args.section || 'default'
|
|
166
|
+
const keys = yaml[section]
|
|
167
|
+
if ( args.verbose ) {
|
|
168
|
+
console.log( 'replacements' )
|
|
169
|
+
console.log( keys )
|
|
170
|
+
}
|
|
171
|
+
if ( args.export ) {
|
|
172
|
+
console.log( `export ${args.key}=${keys[args.key]}` )
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log( keys[args.key] )
|
|
176
|
+
}
|
|
177
|
+
if ( ! args.continue ) process.exit()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Find all of the potential target dirs. Utilize .gitignore to ignore files
|
|
181
|
+
const opt = {
|
|
182
|
+
ignore : parse( fs.readFileSync( '.gitignore' ) )
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ahoy = glob.sync( '*/', opt )
|
|
186
|
+
|
|
187
|
+
// Keel supports a "never" policy which will cause it to skip over a release
|
|
188
|
+
// if it is properly set. Adding a .nokeel file to a release directoy will
|
|
189
|
+
// cause the creation of the file if it does not already exist.
|
|
190
|
+
const generateKeelNeverPolicy = ( fileName ) => {
|
|
191
|
+
if ( fs.existsSync( fileName ) ) return
|
|
192
|
+
const neverPolicy = `
|
|
193
|
+
keel:
|
|
194
|
+
# keel policy (all/major/minor/patch/force)
|
|
195
|
+
policy: "never"
|
|
196
|
+
# approvals
|
|
197
|
+
# approvals: 1
|
|
198
|
+
# approvalDeadline: 24
|
|
199
|
+
# # images to track and update
|
|
200
|
+
# images:
|
|
201
|
+
# - repository: never.IMAGE # [1]
|
|
202
|
+
# tag: never.VERSION # [2]
|
|
203
|
+
`
|
|
204
|
+
fs.writeFileSync( fileName, neverPolicy )
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Generate the values.yaml files in directories not marked with .nohelm.
|
|
208
|
+
ahoy.forEach( ( dir ) => {
|
|
209
|
+
if ( !fs.existsSync( `${dir}/.nohelm` ) ) {
|
|
210
|
+
console.log( `Generating values in ${dir}` )
|
|
211
|
+
|
|
212
|
+
let valuesOut = [] // An accumulator for all of the output values.
|
|
213
|
+
|
|
214
|
+
// Add support for localized values for things like keel (or whatever)
|
|
215
|
+
const valuesLocal = `${dir}values-local.yaml`
|
|
216
|
+
|
|
217
|
+
if ( fs.existsSync( `${dir}.nokeel` ) ) {
|
|
218
|
+
generateKeelNeverPolicy( valuesLocal )
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if ( fs.existsSync( valuesLocal ) ) {
|
|
222
|
+
console.log( ` Local values in ${valuesLocal}` )
|
|
223
|
+
valuesOut += `\n# AHOY => values-local.yaml\n\n`
|
|
224
|
+
valuesOut += fs.readFileSync( valuesLocal, { encoding : 'utf-8' } )
|
|
225
|
+
}
|
|
226
|
+
valuesOut += `\n${values}`
|
|
227
|
+
valuesOut = doReplacements( valuesOut, over )
|
|
228
|
+
|
|
229
|
+
fs.writeFileSync( `${dir}/values.yaml`, valuesOut )
|
|
230
|
+
}
|
|
231
|
+
} )
|
|
232
|
+
|
|
233
|
+
// Set the GCP context for safety!
|
|
234
|
+
const clusterName = over['CLUSTER_NAME']
|
|
235
|
+
const projectName = over['PROJECT_NAME']
|
|
236
|
+
const zone = over['GCE_REGION'] + ( over['GCE_ZONE'] ? `-${over['GCE_ZONE']}` : '' )
|
|
237
|
+
|
|
238
|
+
if ( !clusterName ) {
|
|
239
|
+
console.log( chalk.yellow(
|
|
240
|
+
`
|
|
241
|
+
|
|
242
|
+
The overwhelm.yaml file in this branch is missing a CLUSTER_NAME setting,
|
|
243
|
+
which was added to support cluster names that do not match the project
|
|
244
|
+
name. Since the mismatch is rare, you can probably get by with simply
|
|
245
|
+
adding this line following PROJECT_NAME in the default section of
|
|
246
|
+
overwhelm.yaml:
|
|
247
|
+
|
|
248
|
+
${chalk.bold.green( `CLUSTER_NAME : "\${PROJECT_NAME}"` )}
|
|
249
|
+
|
|
250
|
+
However, if the CLUSTER_NAME is not the same as the PROJECT_NAME then go
|
|
251
|
+
ahead and add the CLUSTER_NAME setting to the ${chalk.bold.green(projectName)} section of the
|
|
252
|
+
overwhelm.yaml file.
|
|
253
|
+
|
|
254
|
+
` ) )
|
|
255
|
+
process.exit( 1 )
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let kubeout = '_no_current_context_'
|
|
259
|
+
try {
|
|
260
|
+
// if current is not set exec will throw
|
|
261
|
+
kubeout = exec( 'kubectl config current-context' )
|
|
262
|
+
} catch (err) { }
|
|
263
|
+
|
|
264
|
+
const currentCtx = kubeout.toString().trim()
|
|
265
|
+
const desiredCtx = `gke_${projectName}_${zone}_${clusterName}`
|
|
266
|
+
const matchedCtx = ( currentCtx === desiredCtx )
|
|
267
|
+
const cmd = `gcloud container clusters get-credentials ${clusterName} --zone ${zone} --project ${projectName}`
|
|
268
|
+
|
|
269
|
+
if ( matchedCtx ) {
|
|
270
|
+
console.log( `
|
|
271
|
+
|
|
272
|
+
Your current context matches. Do you want to update credentials anyway?
|
|
273
|
+
|
|
274
|
+
` )
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
console.log( `
|
|
278
|
+
|
|
279
|
+
You are about to change your GCP context to:
|
|
280
|
+
CLUSTER_NAME => ${chalk.bold.green(clusterName)}
|
|
281
|
+
PROJECT_NAME => ${chalk.bold.green(projectName)}
|
|
282
|
+
GCE_REGION => ${chalk.bold.green(zone)}
|
|
283
|
+
|
|
284
|
+
` )
|
|
285
|
+
}
|
|
286
|
+
const ans = ask.question( `Enter "${chalk.bold.green( 'yes' )}" if you wish to proceed => ` )
|
|
287
|
+
if ( ans !== 'yes' ) {
|
|
288
|
+
if ( matchedCtx ) {
|
|
289
|
+
console.log( chalk.bold.yellow( `\n\nSkipping a refetch of your GCP context and credentials\n\n` ) )
|
|
290
|
+
process.exit( 0 )
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log( chalk.bold.red( `\n\nYour GCP context and credentials do NOT match - this could be bad!\n\n` ) )
|
|
294
|
+
process.exit( 1 )
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.log( `\n\nExecuting: ${cmd}\n\n` )
|
|
298
|
+
exec( `${cmd}`, ( error, out ) => {
|
|
299
|
+
if ( error ) {
|
|
300
|
+
console.log( `\n\n***ERROR ${error}\n\n` )
|
|
301
|
+
process.exit( 1 )
|
|
302
|
+
}
|
|
303
|
+
} )
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
const exec = require( 'child_process' ).exec
|
|
2
|
+
const fs = require( 'fs-extra' )
|
|
3
|
+
const path = require( 'path' )
|
|
4
|
+
const async = require( 'async' )
|
|
5
|
+
const copy = require( 'recursive-copy' )
|
|
6
|
+
const ora = require( 'ora' )
|
|
7
|
+
const tar = require( 'tar' )
|
|
8
|
+
const semver = require( 'semver' )
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Packleator {
|
|
12
|
+
|
|
13
|
+
static PRIVATE_PACKAGES_DIRECTORY = 'private_packages'
|
|
14
|
+
|
|
15
|
+
constructor( options ) {
|
|
16
|
+
const { args } = options
|
|
17
|
+
this.privatePackages = new Set()
|
|
18
|
+
|
|
19
|
+
this.args = args ? {
|
|
20
|
+
...args,
|
|
21
|
+
output : args.output.substr( -1 ) === '/' ? args.output.substr( 0, args.output.length ) : args.output
|
|
22
|
+
} : {} // args || {}
|
|
23
|
+
this.spinner = ora( { text : 'Installing NPM dependencies...', spinner : 'monkey' } )
|
|
24
|
+
this.stderr = ''
|
|
25
|
+
|
|
26
|
+
this.tarredVersions = {}
|
|
27
|
+
|
|
28
|
+
this.outputPackageJson = {}
|
|
29
|
+
this.outputPackageLockJson = {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async run() {
|
|
33
|
+
try {
|
|
34
|
+
this.spinner.start()
|
|
35
|
+
await this.npmInstall()
|
|
36
|
+
|
|
37
|
+
this.spinner.text = 'Copying files...'
|
|
38
|
+
await this.copyFiles()
|
|
39
|
+
|
|
40
|
+
process.chdir( this.args.output )
|
|
41
|
+
|
|
42
|
+
this.outputPackageJson = await this.getPackageJson()
|
|
43
|
+
this.outputPackageLockJson = await this.getPackageJson( undefined, true )
|
|
44
|
+
|
|
45
|
+
this.spinner.text = 'Parsing private dependencies...'
|
|
46
|
+
const deps = await this.getPrivateDeps()
|
|
47
|
+
|
|
48
|
+
this.spinner.text = 'Packaging modules...'
|
|
49
|
+
async.eachOfSeries( deps, async ( item, key ) => {
|
|
50
|
+
const packageJson = await this.getPackageJson( item )
|
|
51
|
+
const { name, version } = packageJson
|
|
52
|
+
|
|
53
|
+
await this.pack( name, version, item )
|
|
54
|
+
}, async ( err ) => {
|
|
55
|
+
await this.rebuildPackageJson()
|
|
56
|
+
await this.rebuildPackageLockJson()
|
|
57
|
+
|
|
58
|
+
await fs.writeFile( 'package.json', JSON.stringify( this.outputPackageJson, null, 2 ) )
|
|
59
|
+
await fs.writeFile( 'package-lock.json', JSON.stringify( this.outputPackageLockJson, null, 2 ) )
|
|
60
|
+
|
|
61
|
+
await fs.remove( 'node_modules' )
|
|
62
|
+
|
|
63
|
+
const { name, version } = this.outputPackageJson
|
|
64
|
+
const fileName = `${name.replace( '@', '' ).replace( '/', '-' )}-${version}.tgz`
|
|
65
|
+
|
|
66
|
+
process.chdir( '..' )
|
|
67
|
+
|
|
68
|
+
this.spinner.text = 'Packaging results...'
|
|
69
|
+
await tar.c( {
|
|
70
|
+
file : fileName,
|
|
71
|
+
strict : true,
|
|
72
|
+
prefix : 'package/',
|
|
73
|
+
C : this.args.output
|
|
74
|
+
}, [ '.' ] )
|
|
75
|
+
|
|
76
|
+
this.spinner.text = 'Cleaning up...'
|
|
77
|
+
await this.cleanup()
|
|
78
|
+
|
|
79
|
+
this.spinner.stop()
|
|
80
|
+
} )
|
|
81
|
+
} catch ( ex ) {
|
|
82
|
+
this.spinner.stop()
|
|
83
|
+
throw ex
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async rebuildPackageJson() {
|
|
88
|
+
const packageJson = this.outputPackageJson
|
|
89
|
+
const tarredPackages = Object.keys( this.tarredVersions )
|
|
90
|
+
const { dependencies = {}, devDependencies = {} } = packageJson
|
|
91
|
+
|
|
92
|
+
tarredPackages.forEach( ( tarredPackage ) => {
|
|
93
|
+
if ( dependencies[tarredPackage] ) {
|
|
94
|
+
const version = semver.maxSatisfying( this.tarredVersions[tarredPackage], dependencies[tarredPackage] )
|
|
95
|
+
|
|
96
|
+
if ( version != null ) {
|
|
97
|
+
packageJson.dependencies[tarredPackage] = `file:${Packleator.PRIVATE_PACKAGES_DIRECTORY}${path.sep}${tarredPackage.replace( '@', '' ).replace( '/', '-' )}-${version}.tgz`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if ( devDependencies[tarredPackage] ) {
|
|
102
|
+
const version = semver.maxSatisfying( this.tarredVersions[tarredPackage], devDependencies[tarredPackage] )
|
|
103
|
+
|
|
104
|
+
if ( version != null ) {
|
|
105
|
+
packageJson.devDependencies[tarredPackage] = `file:${Packleator.PRIVATE_PACKAGES_DIRECTORY}${path.sep}${tarredPackage.replace( '@', '' ).replace( '/', '-' )}-${version}.tgz`
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} )
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async rebuildPackageLockJson() {
|
|
113
|
+
const packageLockJson = this.outputPackageLockJson
|
|
114
|
+
const tarredPackages = Object.keys( this.tarredVersions )
|
|
115
|
+
|
|
116
|
+
const recurseDeps = ( deps ) => {
|
|
117
|
+
Object
|
|
118
|
+
.keys( deps )
|
|
119
|
+
.forEach( ( key ) => {
|
|
120
|
+
const dep = deps[key]
|
|
121
|
+
|
|
122
|
+
tarredPackages.forEach( ( tarredPackage ) => {
|
|
123
|
+
if ( key === tarredPackage ) {
|
|
124
|
+
dep.version = `file:${Packleator.PRIVATE_PACKAGES_DIRECTORY}${path.sep}${key.replace( '@', '' ).replace( '/', '-' )}-${dep.version}.tgz`
|
|
125
|
+
delete dep.resolved
|
|
126
|
+
delete dep.integrity
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if ( dep.requires && dep.requires[tarredPackage] ) {
|
|
130
|
+
delete dep.requires[tarredPackage]
|
|
131
|
+
}
|
|
132
|
+
} )
|
|
133
|
+
|
|
134
|
+
if ( dep.dependencies ) {
|
|
135
|
+
recurseDeps( dep.dependencies )
|
|
136
|
+
}
|
|
137
|
+
} )
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
recurseDeps( packageLockJson.dependencies )
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async copyFiles() {
|
|
144
|
+
const startsWith = new RegExp( `^(dist|lib|image|package|${this.args.output})` )
|
|
145
|
+
const endsWith = /\.tgz$/
|
|
146
|
+
await fs.remove( this.args.output )
|
|
147
|
+
await fs.mkdirp( this.args.output )
|
|
148
|
+
await copy( '.', this.args.output, {
|
|
149
|
+
overwrite : true,
|
|
150
|
+
dot : true,
|
|
151
|
+
filter : ( item ) => {
|
|
152
|
+
if ( item === 'package.json' || item === 'package-lock.json' ) {
|
|
153
|
+
return true
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if ( startsWith.test( item ) || endsWith.test( item ) ) {
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if ( item.startsWith( '.' ) && item.indexOf( '.babelrc' ) === -1 ) {
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ( this.args.ignore.length > 0 ) {
|
|
165
|
+
return this.args.ignore.indexOf( item ) === -1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return true
|
|
169
|
+
}
|
|
170
|
+
} )
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async pack( name, version, dir ) {
|
|
174
|
+
const fileName = `${name.replace( '@', '' ).replace( '/', '-' )}-${version}.tgz`
|
|
175
|
+
|
|
176
|
+
await tar.c( {
|
|
177
|
+
file : fileName,
|
|
178
|
+
C : dir,
|
|
179
|
+
strict : true,
|
|
180
|
+
prefix : 'package/'
|
|
181
|
+
}, [ '.' ] )
|
|
182
|
+
|
|
183
|
+
await fs.move( path.join( process.cwd(), fileName ), path.join( process.cwd(), Packleator.PRIVATE_PACKAGES_DIRECTORY, fileName ), { overwrite : true } )
|
|
184
|
+
|
|
185
|
+
if ( this.tarredVersions[name] ) {
|
|
186
|
+
this.tarredVersions[name].push( version )
|
|
187
|
+
} else {
|
|
188
|
+
this.tarredVersions[name] = [ version ]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return fileName
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async cleanup() {
|
|
195
|
+
await copy( this.args.output, process.cwd(), {
|
|
196
|
+
overwrite : true,
|
|
197
|
+
filter : '*.tgz'
|
|
198
|
+
} )
|
|
199
|
+
await fs.remove( this.args.output )
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async npmInstall( dir = process.cwd() ) {
|
|
203
|
+
const currentDir = process.cwd()
|
|
204
|
+
|
|
205
|
+
return new Promise( ( resolve, reject ) => {
|
|
206
|
+
if ( currentDir !== dir ) {
|
|
207
|
+
process.chdir( dir )
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const npmInstall = exec( 'npm install' )
|
|
211
|
+
|
|
212
|
+
npmInstall
|
|
213
|
+
.stderr
|
|
214
|
+
.on( 'data', ( data ) => {
|
|
215
|
+
this.stderr += data.toString()
|
|
216
|
+
} )
|
|
217
|
+
|
|
218
|
+
npmInstall.on( 'exit', ( code ) => {
|
|
219
|
+
if ( currentDir !== dir ) {
|
|
220
|
+
process.chdir( currentDir )
|
|
221
|
+
}
|
|
222
|
+
resolve()
|
|
223
|
+
} )
|
|
224
|
+
|
|
225
|
+
npmInstall.on( 'error', ( err ) => {
|
|
226
|
+
if ( currentDir !== dir ) {
|
|
227
|
+
process.chdir( currentDir )
|
|
228
|
+
}
|
|
229
|
+
reject( err )
|
|
230
|
+
} )
|
|
231
|
+
} )
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async getPrivateDeps( depth ) {
|
|
235
|
+
return new Promise( ( resolve, reject ) => {
|
|
236
|
+
|
|
237
|
+
const depthArg = depth != null ? ` --depth ${depth}` : ''
|
|
238
|
+
exec( `npm ls --parseable --silent${depthArg}`, ( err, stdout, stderr ) => {
|
|
239
|
+
if ( err && !stdout ) {
|
|
240
|
+
reject( err )
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const dependencies = stdout.split( '\n' )
|
|
245
|
+
|
|
246
|
+
if ( dependencies ) {
|
|
247
|
+
resolve( dependencies.filter( ( item ) => {
|
|
248
|
+
const parts = item.split( path.sep )
|
|
249
|
+
return this.args.namespaces.indexOf( parts[parts.length - 2] ) !== -1
|
|
250
|
+
} ) )
|
|
251
|
+
} else {
|
|
252
|
+
reject( new Error( 'Unable to resolve package.json dependencies' ) )
|
|
253
|
+
}
|
|
254
|
+
} )
|
|
255
|
+
} )
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async getPackageJson( dir = process.cwd(), lock = false ) {
|
|
259
|
+
const lockModifier = lock ? '-lock' : ''
|
|
260
|
+
try {
|
|
261
|
+
const file = await fs.readFile( path.join( dir, `package${lockModifier}.json` ), 'utf-8' )
|
|
262
|
+
const json = JSON.parse( file )
|
|
263
|
+
|
|
264
|
+
return json
|
|
265
|
+
} catch ( ex ) {
|
|
266
|
+
throw new Error( 'Error getting package.json. Make sure the current directory contains a valid package.json file' )
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = Packleator
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const commandLineArgs = require( 'command-line-args' )
|
|
3
|
+
const commandLineUsage = require( 'command-line-usage' )
|
|
4
|
+
const path = require( 'path' )
|
|
5
|
+
const Packleator = require( './Packleator' )
|
|
6
|
+
const ansiLogo = require( './logo-ansi' )
|
|
7
|
+
|
|
8
|
+
const defaultConfig = {
|
|
9
|
+
namespaces : [ '@leverege' ],
|
|
10
|
+
output : 'lev_dest'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sections = [
|
|
14
|
+
{
|
|
15
|
+
content : ansiLogo,
|
|
16
|
+
raw : true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
content : 'Bundles private/proprietary npm packages and source code into a tarball for distribution'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
header : 'Options',
|
|
23
|
+
optionList : [
|
|
24
|
+
{
|
|
25
|
+
name : 'output',
|
|
26
|
+
alias : 'o',
|
|
27
|
+
description : 'The destination folder (optional)',
|
|
28
|
+
defaultValue : 'lev_dest'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name : 'ignore',
|
|
32
|
+
alias : 'i',
|
|
33
|
+
description : 'List of files or folders to ignore. Uses glob pattern matching (optional)',
|
|
34
|
+
multiple : true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name : 'namespaces',
|
|
38
|
+
alias : 'n',
|
|
39
|
+
description : 'List of npm namespaces to be considered private/proprietary (optional)',
|
|
40
|
+
multiple : true
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name : 'verbose',
|
|
44
|
+
alias : 'v',
|
|
45
|
+
description : 'Log a little bit louder now'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name : 'help',
|
|
49
|
+
description : 'Print this usage guide.'
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
const usage = commandLineUsage( sections )
|
|
56
|
+
|
|
57
|
+
const optionDefinitions = [
|
|
58
|
+
{ name : 'output', alias : 'o', type : String, defaultValue : defaultConfig.output },
|
|
59
|
+
{ name : 'ignore', alias : 'i', type : String, multiple : true, defaultValue : [] },
|
|
60
|
+
{ name : 'namespaces', alias : 'n', type : String, defaultValue : defaultConfig.namespaces },
|
|
61
|
+
{ name : 'verbose', alias : 'v', type : Boolean, defaultValue : false },
|
|
62
|
+
{ name : 'help', type : Boolean }
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const args = commandLineArgs( optionDefinitions )
|
|
66
|
+
|
|
67
|
+
async function main() {
|
|
68
|
+
if ( args.help ) {
|
|
69
|
+
console.log( usage )
|
|
70
|
+
} else {
|
|
71
|
+
const packleator = new Packleator( { ...defaultConfig, args } )
|
|
72
|
+
|
|
73
|
+
if ( args.verbose ) {
|
|
74
|
+
console.log( packleator.args )
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await packleator.getPackageJson()
|
|
79
|
+
await packleator.run()
|
|
80
|
+
} catch ( ex ) {
|
|
81
|
+
console.error( ex.message )
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module.exports =
|
|
2
|
+
`[0;5;37;47m . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . [0m
|
|
3
|
+
[0;5;37;47m . . S% . . . . . . . . . . . . . . . . . . [0m
|
|
4
|
+
[0;5;37;47m . .:[0;1;30;47m%[0;1;37;47m [0;5;37;47m: . . . .. . . . . . ...... . ... .. . . . .. .. . ..... . .... . .. . ..... . ... . ..... . .. . . ..... . . . . [0m
|
|
5
|
+
[0;5;37;47m . :[0;1;34;46m8[0;1;30;47m8[0;5;37;47m:.. . . .8: . [0;1;37;47m.[0;5;37;40m8[0;1;30;47m.[0;5;37;47m . .. [0;1;37;47m@[0;5;37;40m8[0;1;30;47mX@@@@@@8%[0;5;37;47m [0;1;37;47m@[0;1;30;47m88[0;5;37;47mX . . t[0;1;30;47mX8[0;1;37;47m%[0;5;37;47m 8[0;1;30;47m88@@@@@@@@[0;5;37;47mt. [0;1;37;47mt[0;5;37;40m8[0;1;30;47m@@@@@%[0;1;37;47m;[0;5;37;47m% . . [0;1;37;47m@[0;5;37;40m8[0;1;30;47m@@@@@@@8%[0;5;37;47m. .. ;[0;1;37;47mt[0;1;30;47m%88t[0;1;37;47m@[0;5;37;47m. .:[0;1;30;47mX8@@@@@@@8[0;1;37;47m8[0;5;37;47m . . .[0m
|
|
6
|
+
[0;5;37;47m . .. 8[0;5;34;44m8[0;1;30;46m8[0;5;37;47m8:. . .[0;1;37;47mX[0;1;31;47m8[0;5;37;47mS . . [0;5;37;40mS[0;34;40m%[0;5;35;40m [0;5;37;47m . . [0;1;30;47m:[0;34;40mS[0;5;30;40m@[0;5;35;40m;[0;5;36;40mt[0;5;35;40mt[0;5;36;40mt[0;5;35;40mt[0;5;36;40mt[0;5;35;40m%[0;5;36;40m.[0;5;37;47m S[0;5;35;40mt[0;34;40m%[0;5;37;40m8[0;5;37;47m.. . [0;1;30;47m@[0;34;40mS[0;5;35;40m;[0;5;37;47m@ [0;1;37;47mX[0;1;30;40mX[0;5;30;40m8[0;5;34;40mS[0;5;35;40m:[0;5;36;40mt[0;5;35;40mt[0;5;36;40mt[0;5;35;40mt[0;5;36;40mt[0;5;35;40mt[0;5;37;47m@ [0;1;30;47m8[0;34;40mS[0;5;30;40mS[0;5;35;40m;[0;5;36;40mt[0;5;35;40mt[0;5;36;40mt[0;5;34;40mS[0;1;30;40m8[0;5;30;40m8[0;5;35;40m [0;5;37;47mX.. [0;1;30;47m;[0;34;40mS[0;5;30;40mX[0;5;36;40m;[0;5;35;40mt[0;5;36;40mt[0;5;35;40mt[0;5;36;40mt[0;5;35;40mt[0;5;36;40m%[0;5;35;40m [0;5;37;47m. ..[0;1;37;47mt[0;5;36;40m [0;5;34;40m@[0;1;30;40m8[0;5;35;40mt[0;5;36;40mt[0;5;35;40mt[0;5;30;40mX8[0;5;34;40m8[0;5;33;40m [0;5;37;47mt. t[0;5;34;40m@[0;1;30;40m@[0;5;34;40mS[0;5;33;40m:[0;5;35;40mt[0;5;36;40m%[0;5;35;40m%[0;5;36;40mt[0;5;35;40mt[0;5;34;40mS[0;1;37;47mt[0;5;37;47m . . [0m
|
|
7
|
+
[0;5;37;47m . . @[0;5;36;44m8[0;34;46m8[0;1;30;47m8[0;5;37;47m8;. :[0;1;37;47mt[0;5;33;41m [0;5;37;43m@[0;5;37;47m8;. . [0;5;37;40m@[0;1;30;40m@[0;5;35;40m [0;5;37;47m . [0;1;30;47m.[0;1;30;40m8[0;5;34;40mS[0;5;37;47mS:;:;:: . [0;1;37;47m8[0;5;32;40mS[0;34;40mS[0;1;30;47m.[0;5;37;47m . . [0;1;37;47m;[0;5;30;40m8@[0;1;37;47m@[0;5;37;47m. [0;1;37;47mX[0;1;30;40m8[0;5;30;40m8[0;1;37;47m@[0;5;37;47m.;:;:;. . [0;1;30;47mX[0;1;30;40m@[0;5;36;40m:[0;5;37;47m;::;.;[0;1;30;47mX[0;1;30;40m8[0;5;35;40mt[0;5;37;47m%. [0;1;30;47m.[0;1;30;40m8[0;5;34;40mS[0;5;37;47mX:;:;:: .%[0;5;35;40m [0;1;30;40m8[0;5;35;40m:[0;1;37;47m:[0;5;37;47mt;:: S[0;1;30;47m [0;1;37;47mS[0;5;37;47m . t[0;5;30;40mS[0;1;30;40mX[0;1;30;47mt[0;5;37;47m.;;:;:. . [0m
|
|
8
|
+
[0;5;37;47m . [0;1;37;47m [0;5;34;44m8[0;1;34;46m8[0;36;44m8[0;1;37;47m;[0;5;37;47m .t[0;5;37;41m8[0;5;33;41m [0;5;33;43m.[0;1;35;47mX[0;5;37;47m:. . [0;5;37;40mX[0;34;40mS[0;5;37;40m@[0;5;37;47m . . [0;1;30;47m:[0;1;30;40mX[0;5;30;40mS[0;5;37;47mt . . . [0;1;30;47mS[0;1;30;40m8[0;5;34;40mS[0;5;37;47mS . :[0;5;36;40m.[0;34;40m%[0;1;30;47mX[0;5;37;47m : [0;1;37;47m@[0;1;30;40m@[0;5;30;40m8[0;1;37;47m8[0;5;37;47m . . . [0;1;30;47m@[0;34;40mS[0;5;35;40m [0;5;37;47m. . [0;1;37;47m8[0;5;30;40m88[0;5;37;47m8 [0;1;30;47m:[0;1;30;40mX[0;5;30;40mS[0;5;37;47m; . .. [0;1;30;47m@[0;1;30;40m@[0;5;34;40m@[0;5;37;47m8.. .... t[0;5;34;40m%[0;1;30;40mS[0;1;30;47m:[0;5;37;47m.. . . . . [0m
|
|
9
|
+
[0;5;37;47m . . . ..[0;1;30;47m8[0;5;36;44m8[0;1;30;46m8[0;5;34;44mS[0;1;30;47mt[0;5;37;47mS[0;1;37;47m [0;5;1;33;41m8[0;5;33;41m:[0;1;37;47m [0;5;37;47m... . . [0;5;37;40m@[0;1;30;40m@[0;5;35;40m [0;5;37;47m .. [0;1;30;47m:[0;1;30;40mX[0;5;30;40m@[0;5;36;40m [0;5;35;40m [0;5;36;40m [0;5;37;47mt ..%[0;5;35;40m;[0;32;40mt[0;1;30;47m@[0;5;37;47m.. [0;1;30;47m;[0;1;30;40m@[0;5;35;40m.[0;5;37;47mS . [0;1;37;47mX[0;1;30;40m8[0;5;34;40m8[0;5;33;40m;[0;5;35;40m.[0;5;36;40m [0;5;35;40m .[0;1;37;47m8[0;5;37;47m : [0;1;30;47mX[0;1;30;40m@[0;5;35;40m.[0;5;37;47m: ...t[0;1;30;47m8[0;34;40mX[0;5;36;40m.[0;5;37;47mt. [0;1;30;47m:[0;1;30;40m@[0;5;30;40m@[0;5;35;40m [0;5;37;47mS .[0;5;35;40m.[0;34;40mS[0;5;37;40m8[0;5;37;47m.. . %X; t[0;5;30;40mX[0;1;30;40m8[0;5;36;40m;[0;5;35;40m.[0;5;36;40m .[0;1;30;47m [0;5;37;47m . . . [0m
|
|
10
|
+
[0;5;37;47m . . t[0;36;47m8[0;5;34;44m8[0;1;30;46m8[0;5;36;44m8[0;5;36;40mX[0;1;37;47m [0;5;37;47m%8t . .. [0;5;37;40m@[0;34;40mS[0;5;37;40mX[0;5;37;47m . . [0;1;30;47m:[0;1;30;40mX[0;5;34;40mX[0;1;30;47m [0;1;37;47m;;;;:t[0;5;37;47m: ..[0;1;37;47m8[0;5;34;40mS[0;1;30;40m@[0;1;37;47m:[0;5;37;47m.[0;1;37;47mX[0;5;30;40m88[0;1;37;47m8[0;5;37;47m .. [0;1;37;47mX[0;1;30;40m88[0;1;30;47mS[0;1;37;47m::;;;.[0;5;37;47m% . [0;1;30;47m@[0;1;30;40mX[0;5;34;40m@[0;5;33;40m%[0;5;34;40mS[0;5;33;40m;[0;5;35;40m%[0;5;36;40mX[0;1;30;40m8[0;5;30;40mX[0;5;37;40m@[0;5;37;47mX . [0;1;30;47m:[0;34;40m@[0;5;33;40mt[0;1;30;47m;[0;1;37;47m;;;;:;[0;5;37;47m;. [0;5;37;40m%[0;1;30;40m@[0;5;33;40m [0;5;37;47m.. .. [0;5;36;40m [0;34;40m%[0;1;37;47mt[0;5;37;47m. t[0;5;34;40m%[0;1;30;40mX[0;5;37;40m8[0;1;37;47m;:;;; [0;5;37;47m8.. . [0m
|
|
11
|
+
[0;5;37;47m . . . .:8[0;1;30;46m8[0;5;34;44mX[0;5;36;40mt[0;5;34;44mt[0;34;46m8[0;1;30;47m8[0;5;37;47m . . [0;5;37;40m@[0;1;30;40mX[0;5;35;40m [0;5;37;47m ..... [0;1;30;47m:[0;1;30;40mX[0;5;34;40mS[0;5;37;47mS..:..:.. . [0;1;30;47m@[0;1;30;40mX[0;5;35;40m;[0;5;37;47m:[0;5;35;40m [0;34;40mS[0;1;30;47mS[0;5;37;47m . . [0;1;37;47mX[0;1;30;40m8[0;5;30;40m8[0;1;37;47m@[0;5;37;47m ::.:.. . [0;1;30;47m@[0;1;30;40mX[0;5;30;40mX[0;1;30;47m@X@@[0;5;30;40mS[0;1;30;40m@[0;1;30;47mt[0;5;37;47m ... [0;1;30;47m.[0;1;30;40m@[0;5;34;40m@[0;5;37;47mS ::..:.. [0;1;37;47m;[0;1;30;40m88[0;1;30;47mt[0;5;37;47m ..... [0;5;37;40mS[0;1;30;40mX[0;1;37;47m:[0;5;37;47m t[0;5;30;40mS[0;1;30;40mX[0;1;30;47m;[0;5;37;47m ::.:... . [0m
|
|
12
|
+
[0;5;37;47m . . [0;1;37;47m:[0;1;34;46m8[0;1;30;46m8[0;1;34;46m8[0;1;30;45m8[0;5;37;47m% . . [0;5;37;40m@[0;1;30;40mX[0;5;35;40m [0;5;37;47mS8@@8@: [0;1;30;47m:[0;1;30;40mX[0;5;30;40mS[0;1;37;47m8[0;5;37;47mX@X@X@X: . %[0;5;35;40m;[0;1;30;40m8[0;5;36;40m [0;5;34;40m@[0;5;33;40m%[0;5;37;47mX . . [0;1;37;47m@[0;1;30;40m8[0;5;34;40m8[0;1;37;47m:[0;5;37;47mS@X@X@@;. [0;1;30;47m@[0;34;40mX[0;5;36;40m [0;5;37;47m. ..[0;1;37;47m8[0;5;34;40mX[0;1;30;40m8[0;1;30;47m;[0;5;37;47m .. [0;1;30;47m:[0;1;30;40mX[0;5;30;40mS[0;1;37;47m8[0;5;37;47mS@X@X@@: [0;1;30;47mt[0;5;30;40m88[0;5;36;40m [0;1;37;47mt[0;5;37;47m8%S[0;1;37;47m8:[0;5;36;40m;[0;34;40mt[0;1;37;47m:[0;5;37;47m t[0;5;34;40m%[0;1;30;40mX[0;1;30;47mS[0;5;37;47mX@@X@X@% . . [0m
|
|
13
|
+
[0;5;37;47m . . . . . [0;1;37;47m:[0;5;34;44mX[0;1;30;47m8[0;5;37;47m@.: . . [0;5;37;40m8[0;5;30;40m8[0;5;34;40mX[0;5;30;40m8@8[0;5;34;40m8[0;5;30;40m8S[0;5;37;47mS [0;1;30;47m.[0;5;30;40m8[0;5;34;40m@[0;5;30;40m@88[0;5;34;40m8[0;5;30;40m8@[0;5;34;40m8[0;5;30;40m8[0;5;37;47m8.. ..[0;1;37;47m8[0;5;34;40mS[0;5;30;40m@X[0;1;37;47m8[0;5;37;47m . . [0;1;37;47m@[0;5;30;40m8@@[0;5;34;40m8[0;5;30;40m88[0;5;34;40m8[0;5;30;40m8[0;5;34;40m@[0;1;30;40m8[0;1;37;47m;[0;5;37;47m [0;1;30;47mS[0;5;30;40m8[0;5;36;40m [0;5;37;47m. . .@[0;5;35;40m.[0;5;30;40m8[0;5;37;40mS[0;5;37;47mt. [0;1;30;47m.[0;5;30;40m8[0;5;34;40m@[0;5;30;40m88[0;5;34;40m8[0;5;30;40m88[0;5;34;40m8[0;5;30;40m88[0;5;37;47m8...%[0;1;37;47m [0;5;37;40mS[0;5;36;40m%[0;5;30;40m888[0;5;34;40mX[0;5;33;40mt[0;5;37;40m@[0;1;37;47mt[0;5;37;47m; .;[0;5;33;40m%[0;5;30;40m8[0;5;34;40m8[0;5;30;40m88[0;5;34;40m8[0;5;30;40m88[0;5;34;40m@[0;1;30;40m@[0;1;30;47m8[0;5;37;47m . . .[0m
|
|
14
|
+
[0;5;37;47m . [0;36;47m8[0;1;37;47m [0;5;37;47m:.. .;;:::;::. :;;::::;:;:. .:;. .;:;::;::::. .;: . .:;: :;:::::::::: . ..:t;:: . :%;;:::::::. . [0m
|
|
15
|
+
[0;5;37;47m . . . . . X% . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . .. . . . [0m
|
|
16
|
+
[0;5;37;47m . .. . . . . . . . . . . . . . . . . . . . . . . . .[0m`
|