Smalltalk createPackage: 'AmberCli'! Object subclass: #AmberCli slots: {} package: 'AmberCli'! !AmberCli commentStamp! I am the Amber CLI (CommandLine Interface) tool which runs on Node.js. My responsibility is to start different Amber programs like the FileServer or the Repl. Which program to start is determined by the first commandline parameters passed to the AmberCli executable. Use `help` to get a list of all available options. Any further commandline parameters are passed to the specific program. ## Commands New commands can be added by creating a class side method in the `commands` protocol which takes one parameter. This parameter is an array of all commandline options + values passed on to the program. Any `camelCaseCommand` is transformed into a commandline parameter of the form `camel-case-command` and vice versa.! !AmberCli class methodsFor: 'commandline'! commandLineSwitches "Collect all methodnames from the 'commands' protocol of the class and select the ones with only one parameter. Then remove the ':' at the end of the name. Additionally all uppercase letters are made lowercase and preceded by a '-'. Example: fallbackPage: becomes --fallback-page. Return the Array containing the commandline switches." | switches | switches := ((self class methodsInProtocol: 'commands') collect: [ :each | each selector]). switches := switches select: [ :each | each match: '^[^:]*:$']. switches :=switches collect: [ :each | (each allButLast replace: '([A-Z])' with: '-$1') asLowercase]. ^ switches ! handleArguments: args | selector | selector := self selectorForCommandLineSwitch: (args first). args remove: args first. self perform: selector withArguments: { args } ! selectorForCommandLineSwitch: aSwitch "Add ':' at the end and replace all occurences of a lowercase letter preceded by a '-' with the Uppercase letter. Example: fallback-page becomes fallbackPage:. If no correct selector is found return 'help:'" | command selector | (self commandLineSwitches includes: aSwitch) ifTrue: [ selector := (aSwitch replace: '-[a-z]' with: [ :each | each second asUppercase ]), ':'] ifFalse: [ selector := 'help:' ]. ^ selector ! ! !AmberCli class methodsFor: 'commands'! config: args Configurator new start ! help: args Transcript show: 'Available commands'. self commandLineSwitches do: [ :each | console log: each ] ! init: args Initer new start ! repl: args ^ Repl new createInterface ! serve: args ^ (FileServer createServerWithArguments: args) start ! version: arguments ! ! !AmberCli class methodsFor: 'startup'! main "Main entry point for Amber applications. Parses commandline arguments and starts the according subprogram." | args packageJSON | packageJSON := require value: '../package.json'. Transcript show: 'Welcome to Amber CLI version ', packageJSON version, ' (Amber ', Smalltalk version, ', NodeJS ', process versions node, ').'. args := process argv. "Remove the first args which contain the path to the node executable and the script file." args removeFrom: 1 to: 2. args ifEmpty: [self help: nil] ifNotEmpty: [^self handleArguments: args] ! ! Object subclass: #BaseFileManipulator slots: {#path. #fs} package: 'AmberCli'! !BaseFileManipulator methodsFor: 'initialization'! initialize super initialize. path := require value: 'path'. fs := require value: 'fs' ! ! !BaseFileManipulator methodsFor: 'private'! dirname ! rootDirname ^ path join: self dirname with: '..' ! ! BaseFileManipulator subclass: #Configurator slots: {} package: 'AmberCli'! !Configurator methodsFor: 'action'! start self writeConfigThenDo: [ :err | err ifNotNil: [ process exit: 111 ] ifNil: [ process exit ]] ! writeConfigThenDo: aBlock (require value: '@ambers/sdk') configBuilder writeConfig: process cwd toFile: 'config.js' thenDo: aBlock ! ! !Configurator methodsFor: 'initialization'! initialize super initialize ! ! BaseFileManipulator subclass: #FileServer slots: {#http. #url. #host. #port. #basePath. #util. #username. #password. #fallbackPage} package: 'AmberCli'! !FileServer commentStamp! I am the Amber Smalltalk FileServer. My runtime requirement is a functional Node.js executable. To start a FileServer instance on port `4000` use the following code: FileServer new start A parameterized instance can be created with the following code: FileServer createServerWithArguments: options Here, `options` is an array of commandline style strings each followed by a value e.g. `#('--port', '6000', '--host', '0.0.0.0')`. A list of all available parameters can be printed to the commandline by passing `--help` as parameter. See the `Options` section for further details on how options are mapped to instance methods. After startup FileServer checks if the directory layout required by Amber is present and logs a warning on absence. ## Options Each option is of the form `--some-option-string` which is transformed into a selector of the format `someOptionString:`. The trailing `--` gets removed, each `-[a-z]` gets transformed into the according uppercase letter, and a `:` is appended to create a selector which takes a single argument. Afterwards, the selector gets executed on the `FileServer` instance with the value following in the options array as parameter. ## Adding new commandline parameters Adding new commandline parameters to `FileServer` is as easy as adding a new single parameter method to the `accessing` protocol.! !FileServer methodsFor: 'accessing'! basePath ^ basePath ifNil: [self class defaultBasePath] ! basePath: aString basePath := aString. self validateBasePath. ! fallbackPage ^ fallbackPage ! fallbackPage: aString fallbackPage := aString ! host ^ host ! host: hostname host := hostname ! password: aPassword password := aPassword. ! port ^ port ! port: aNumber port := aNumber ! username: aUsername username := aUsername. ! ! !FileServer methodsFor: 'initialization'! checkDirectoryLayout (fs existsSync: (self withBasePath: 'index.html')) ifFalse: [ console warn: 'Warning: project directory does not contain index.html.'. console warn: ' You can specify the directory containing index.html with --base-path.'. console warn: ' You can also specify a page to be served by default,'. console warn: ' for all paths that do not map to a file, with --fallback-page.']. ! initialize super initialize. http := self require: 'http'. util := self require: 'util'. url := self require: 'url'. host := self class defaultHost. port := self class defaultPort. username := nil. password := nil. fallbackPage := nil. ! ! !FileServer methodsFor: 'private'! base64Decode: aString ! isAuthenticated: aRequest "Basic HTTP Auth: http://stackoverflow.com/a/5957629/293175 and https://gist.github.com/1686663" | header token auth parts| (username isNil and: [password isNil]) ifTrue: [^ true]. "get authentication header" header := (aRequest headers at: 'authorization') ifNil:['']. header ifEmpty: [^ false] ifNotEmpty: [ "get authentication token" token := (header tokenize: ' ') ifNil:['']. "convert back from base64" auth := self base64Decode: (token at: 2). "split token at colon" parts := auth tokenize: ':'. ((username = (parts at: 1)) and: [password = (parts at: 2)]) ifTrue: [^ true] ifFalse: [^ false] ]. ! require: aModuleString "call to the require function" ^require value: aModuleString ! validateBasePath "The basePath must be an existing directory. " fs stat: self basePath then: [ :err :stat | err ifNil: [ stat isDirectory ifFalse: [ console warn: 'Warning: --base-path parameter ' , self basePath , ' is not a directory.' ]] ifNotNil: [ console warn: 'Warning: path at --base-path parameter ' , self basePath , ' does not exist.' ]]. ! withBasePath: aBaseRelativePath "return a file path which is relative to the basePath." ^ path join: self basePath with: aBaseRelativePath ! writeData: data toFileNamed: aFilename console log: aFilename ! ! !FileServer methodsFor: 'request handling'! handleGETRequest: aRequest respondTo: aResponse | uri filename | uri := url parse: aRequest url. filename := path join: self basePath with: uri pathname uriDecoded. fs exists: filename do: [:aBoolean | aBoolean ifFalse: [self respondNotFoundTo: aResponse] ifTrue: [(fs statSync: filename) isDirectory ifTrue: [self respondDirectoryNamed: filename from: uri to: aResponse] ifFalse: [self respondFileNamed: filename to: aResponse]]] ! handleOPTIONSRequest: aRequest respondTo: aResponse aResponse writeHead: 200 options: #{'Access-Control-Allow-Origin' -> '*'. 'Access-Control-Allow-Methods' -> 'GET, PUT, POST, DELETE, OPTIONS'. 'Access-Control-Allow-Headers' -> 'Content-Type, Accept'. 'Content-Length' -> 0. 'Access-Control-Max-Age' -> 10}. aResponse end ! handlePUTRequest: aRequest respondTo: aResponse | file stream | (self isAuthenticated: aRequest) ifFalse: [self respondAuthenticationRequiredTo: aResponse. ^ nil]. file := '.', aRequest url uriDecoded. stream := fs createWriteStream: file. stream on: 'error' do: [:error | console warn: 'Error creating WriteStream for file ', file. console warn: ' Did you forget to create the necessary directory in your project (often /src)?'. console warn: ' The exact error is: ', error asString. self respondNotCreatedTo: aResponse]. stream on: 'close' do: [ self respondCreatedTo: aResponse]. aRequest setEncoding: 'utf8'. aRequest on: 'data' do: [:data | stream write: data]. aRequest on: 'end' do: [ stream writable ifTrue: [stream end]] ! handleRequest: aRequest respondTo: aResponse aRequest method = 'PUT' ifTrue: [self handlePUTRequest: aRequest respondTo: aResponse]. aRequest method = 'GET' ifTrue:[self handleGETRequest: aRequest respondTo: aResponse]. aRequest method = 'OPTIONS' ifTrue:[self handleOPTIONSRequest: aRequest respondTo: aResponse] ! respondAuthenticationRequiredTo: aResponse aResponse writeHead: 401 options: #{'WWW-Authenticate' -> 'Basic realm="Secured Developer Area"'}; write: 'Authentication needed'; end. ! respondCreatedTo: aResponse aResponse writeHead: 201 options: #{'Content-Type' -> 'text/plain'. 'Access-Control-Allow-Origin' -> '*'}; end. ! respondDirectoryNamed: aDirname from: aUrl to: aResponse (aUrl pathname endsWith: '/') ifTrue: [self respondFileNamed: aDirname, 'index.html' to: aResponse] ifFalse: [self respondRedirect: aUrl pathname, '/', (aUrl search ifNil: ['']) to: aResponse] ! respondFileNamed: aFilename to: aResponse | type filename | filename := aFilename. fs readFile: filename do: [:ex :file | ex notNil ifTrue: [ console log: filename, ' does not exist'. self respondNotFoundTo: aResponse] ifFalse: [ type := self class mimeTypeFor: filename. type = 'application/javascript' ifTrue: [ type:=type,';charset=utf-8' ]. aResponse writeHead: 200 options: #{'Content-Type' -> type}; write: file encoding: 'binary'; end]] ! respondInternalErrorTo: aResponse aResponse writeHead: 500 options: #{'Content-Type' -> 'text/plain'}; write: '500 Internal server error'; end ! respondNotCreatedTo: aResponse aResponse writeHead: 400 options: #{'Content-Type' -> 'text/plain'}; write: 'File could not be created. Did you forget to create the src directory on the server?'; end. ! respondNotFoundTo: aResponse self fallbackPage isNil ifFalse: [^self respondFileNamed: self fallbackPage to: aResponse]. aResponse writeHead: 404 options: #{'Content-Type' -> 'text/html'}; write: '

404 Not found

'; write: '

Did you forget to put an index.html file into the directory which is served by "bin/amber serve"? To solve this you can:

'; end ! respondOKTo: aResponse aResponse writeHead: 200 options: #{'Content-Type' -> 'text/plain'. 'Access-Control-Allow-Origin' -> '*'}; end. ! respondRedirect: aString to: aResponse aResponse writeHead: 303 options: #{'Location' -> aString}; end. ! ! !FileServer methodsFor: 'starting'! start "Checks if required directory layout is present (issue warning if not). Afterwards start the server." self checkDirectoryLayout. (http createServer: [:request :response | self handleRequest: request respondTo: response]) on: 'error' do: [:error | console log: 'Error starting server: ', error asString]; on: 'listening' do: [console log: 'Starting file server on http://', self host, ':', self port asString]; listen: self port host: self host. ! startOn: aPort self port: aPort. self start ! ! FileServer class slots: {#mimeTypes}! !FileServer class methodsFor: 'accessing'! commandLineSwitches "Collect all methodnames from the 'accessing' protocol and select the ones with only one parameter. Then remove the ':' at the end of the name and add a '--' at the beginning. Additionally all uppercase letters are made lowercase and preceded by a '-'. Example: fallbackPage: becomes --fallback-page. Return the Array containing the commandline switches." | switches | switches := ((self methodsInProtocol: 'accessing') collect: [ :each | each selector]). switches := switches select: [ :each | each match: '^[^:]*:$']. switches :=switches collect: [ :each | (each allButLast replace: '([A-Z])' with: '-$1') asLowercase replace: '^([a-z])' with: '--$1' ]. ^ switches ! defaultBasePath ^ './' ! defaultHost ^ '127.0.0.1' ! defaultMimeTypes ^ #{ '%' -> 'application/x-trash'. '323' -> 'text/h323'. 'abw' -> 'application/x-abiword'. 'ai' -> 'application/postscript'. 'aif' -> 'audio/x-aiff'. 'aifc' -> 'audio/x-aiff'. 'aiff' -> 'audio/x-aiff'. 'alc' -> 'chemical/x-alchemy'. 'art' -> 'image/x-jg'. 'asc' -> 'text/plain'. 'asf' -> 'video/x-ms-asf'. 'asn' -> 'chemical/x-ncbi-asn1-spec'. 'aso' -> 'chemical/x-ncbi-asn1-binary'. 'asx' -> 'video/x-ms-asf'. 'au' -> 'audio/basic'. 'avi' -> 'video/x-msvideo'. 'b' -> 'chemical/x-molconn-Z'. 'bak' -> 'application/x-trash'. 'bat' -> 'application/x-msdos-program'. 'bcpio' -> 'application/x-bcpio'. 'bib' -> 'text/x-bibtex'. 'bin' -> 'application/octet-stream'. 'bmp' -> 'image/x-ms-bmp'. 'book' -> 'application/x-maker'. 'bsd' -> 'chemical/x-crossfire'. 'c' -> 'text/x-csrc'. 'c++' -> 'text/x-c++src'. 'c3d' -> 'chemical/x-chem3d'. 'cac' -> 'chemical/x-cache'. 'cache' -> 'chemical/x-cache'. 'cascii' -> 'chemical/x-cactvs-binary'. 'cat' -> 'application/vnd.ms-pki.seccat'. 'cbin' -> 'chemical/x-cactvs-binary'. 'cc' -> 'text/x-c++src'. 'cdf' -> 'application/x-cdf'. 'cdr' -> 'image/x-coreldraw'. 'cdt' -> 'image/x-coreldrawtemplate'. 'cdx' -> 'chemical/x-cdx'. 'cdy' -> 'application/vnd.cinderella'. 'cef' -> 'chemical/x-cxf'. 'cer' -> 'chemical/x-cerius'. 'chm' -> 'chemical/x-chemdraw'. 'chrt' -> 'application/x-kchart'. 'cif' -> 'chemical/x-cif'. 'class' -> 'application/java-vm'. 'cls' -> 'text/x-tex'. 'cmdf' -> 'chemical/x-cmdf'. 'cml' -> 'chemical/x-cml'. 'cod' -> 'application/vnd.rim.cod'. 'com' -> 'application/x-msdos-program'. 'cpa' -> 'chemical/x-compass'. 'cpio' -> 'application/x-cpio'. 'cpp' -> 'text/x-c++src'. 'cpt' -> 'image/x-corelphotopaint'. 'crl' -> 'application/x-pkcs7-crl'. 'crt' -> 'application/x-x509-ca-cert'. 'csf' -> 'chemical/x-cache-csf'. 'csh' -> 'text/x-csh'. 'csm' -> 'chemical/x-csml'. 'csml' -> 'chemical/x-csml'. 'css' -> 'text/css'. 'csv' -> 'text/comma-separated-values'. 'ctab' -> 'chemical/x-cactvs-binary'. 'ctx' -> 'chemical/x-ctx'. 'cu' -> 'application/cu-seeme'. 'cub' -> 'chemical/x-gaussian-cube'. 'cxf' -> 'chemical/x-cxf'. 'cxx' -> 'text/x-c++src'. 'dat' -> 'chemical/x-mopac-input'. 'dcr' -> 'application/x-director'. 'deb' -> 'application/x-debian-package'. 'dif' -> 'video/dv'. 'diff' -> 'text/plain'. 'dir' -> 'application/x-director'. 'djv' -> 'image/vnd.djvu'. 'djvu' -> 'image/vnd.djvu'. 'dl' -> 'video/dl'. 'dll' -> 'application/x-msdos-program'. 'dmg' -> 'application/x-apple-diskimage'. 'dms' -> 'application/x-dms'. 'doc' -> 'application/msword'. 'dot' -> 'application/msword'. 'dv' -> 'video/dv'. 'dvi' -> 'application/x-dvi'. 'dx' -> 'chemical/x-jcamp-dx'. 'dxr' -> 'application/x-director'. 'emb' -> 'chemical/x-embl-dl-nucleotide'. 'embl' -> 'chemical/x-embl-dl-nucleotide'. 'ent' -> 'chemical/x-pdb'. 'eps' -> 'application/postscript'. 'etx' -> 'text/x-setext'. 'exe' -> 'application/x-msdos-program'. 'ez' -> 'application/andrew-inset'. 'fb' -> 'application/x-maker'. 'fbdoc' -> 'application/x-maker'. 'fch' -> 'chemical/x-gaussian-checkpoint'. 'fchk' -> 'chemical/x-gaussian-checkpoint'. 'fig' -> 'application/x-xfig'. 'flac' -> 'application/x-flac'. 'fli' -> 'video/fli'. 'fm' -> 'application/x-maker'. 'frame' -> 'application/x-maker'. 'frm' -> 'application/x-maker'. 'gal' -> 'chemical/x-gaussian-log'. 'gam' -> 'chemical/x-gamess-input'. 'gamin' -> 'chemical/x-gamess-input'. 'gau' -> 'chemical/x-gaussian-input'. 'gcd' -> 'text/x-pcs-gcd'. 'gcf' -> 'application/x-graphing-calculator'. 'gcg' -> 'chemical/x-gcg8-sequence'. 'gen' -> 'chemical/x-genbank'. 'gf' -> 'application/x-tex-gf'. 'gif' -> 'image/gif'. 'gjc' -> 'chemical/x-gaussian-input'. 'gjf' -> 'chemical/x-gaussian-input'. 'gl' -> 'video/gl'. 'gnumeric' -> 'application/x-gnumeric'. 'gpt' -> 'chemical/x-mopac-graph'. 'gsf' -> 'application/x-font'. 'gsm' -> 'audio/x-gsm'. 'gtar' -> 'application/x-gtar'. 'h' -> 'text/x-chdr'. 'h++' -> 'text/x-c++hdr'. 'hdf' -> 'application/x-hdf'. 'hh' -> 'text/x-c++hdr'. 'hin' -> 'chemical/x-hin'. 'hpp' -> 'text/x-c++hdr'. 'hqx' -> 'application/mac-binhex40'. 'hs' -> 'text/x-haskell'. 'hta' -> 'application/hta'. 'htc' -> 'text/x-component'. 'htm' -> 'text/html'. 'html' -> 'text/html'. 'hxx' -> 'text/x-c++hdr'. 'ica' -> 'application/x-ica'. 'ice' -> 'x-conference/x-cooltalk'. 'ico' -> 'image/x-icon'. 'ics' -> 'text/calendar'. 'icz' -> 'text/calendar'. 'ief' -> 'image/ief'. 'iges' -> 'model/iges'. 'igs' -> 'model/iges'. 'iii' -> 'application/x-iphone'. 'inp' -> 'chemical/x-gamess-input'. 'ins' -> 'application/x-internet-signup'. 'iso' -> 'application/x-iso9660-image'. 'isp' -> 'application/x-internet-signup'. 'ist' -> 'chemical/x-isostar'. 'istr' -> 'chemical/x-isostar'. 'jad' -> 'text/vnd.sun.j2me.app-descriptor'. 'jar' -> 'application/java-archive'. 'java' -> 'text/x-java'. 'jdx' -> 'chemical/x-jcamp-dx'. 'jmz' -> 'application/x-jmol'. 'jng' -> 'image/x-jng'. 'jnlp' -> 'application/x-java-jnlp-file'. 'jpe' -> 'image/jpeg'. 'jpeg' -> 'image/jpeg'. 'jpg' -> 'image/jpeg'. 'js' -> 'application/javascript'. 'kar' -> 'audio/midi'. 'key' -> 'application/pgp-keys'. 'kil' -> 'application/x-killustrator'. 'kin' -> 'chemical/x-kinemage'. 'kpr' -> 'application/x-kpresenter'. 'kpt' -> 'application/x-kpresenter'. 'ksp' -> 'application/x-kspread'. 'kwd' -> 'application/x-kword'. 'kwt' -> 'application/x-kword'. 'latex' -> 'application/x-latex'. 'lha' -> 'application/x-lha'. 'lhs' -> 'text/x-literate-haskell'. 'lsf' -> 'video/x-la-asf'. 'lsx' -> 'video/x-la-asf'. 'ltx' -> 'text/x-tex'. 'lzh' -> 'application/x-lzh'. 'lzx' -> 'application/x-lzx'. 'm3u' -> 'audio/x-mpegurl'. 'm4a' -> 'audio/mpeg'. 'maker' -> 'application/x-maker'. 'man' -> 'application/x-troff-man'. 'mcif' -> 'chemical/x-mmcif'. 'mcm' -> 'chemical/x-macmolecule'. 'mdb' -> 'application/msaccess'. 'me' -> 'application/x-troff-me'. 'mesh' -> 'model/mesh'. 'mid' -> 'audio/midi'. 'midi' -> 'audio/midi'. 'mif' -> 'application/x-mif'. 'mm' -> 'application/x-freemind'. 'mmd' -> 'chemical/x-macromodel-input'. 'mmf' -> 'application/vnd.smaf'. 'mml' -> 'text/mathml'. 'mmod' -> 'chemical/x-macromodel-input'. 'mng' -> 'video/x-mng'. 'moc' -> 'text/x-moc'. 'mol' -> 'chemical/x-mdl-molfile'. 'mol2' -> 'chemical/x-mol2'. 'moo' -> 'chemical/x-mopac-out'. 'mop' -> 'chemical/x-mopac-input'. 'mopcrt' -> 'chemical/x-mopac-input'. 'mov' -> 'video/quicktime'. 'movie' -> 'video/x-sgi-movie'. 'mp2' -> 'audio/mpeg'. 'mp3' -> 'audio/mpeg'. 'mp4' -> 'video/mp4'. 'mpc' -> 'chemical/x-mopac-input'. 'mpe' -> 'video/mpeg'. 'mpeg' -> 'video/mpeg'. 'mpega' -> 'audio/mpeg'. 'mpg' -> 'video/mpeg'. 'mpga' -> 'audio/mpeg'. 'ms' -> 'application/x-troff-ms'. 'msh' -> 'model/mesh'. 'msi' -> 'application/x-msi'. 'mvb' -> 'chemical/x-mopac-vib'. 'mxu' -> 'video/vnd.mpegurl'. 'nb' -> 'application/mathematica'. 'nc' -> 'application/x-netcdf'. 'nwc' -> 'application/x-nwc'. 'o' -> 'application/x-object'. 'oda' -> 'application/oda'. 'odb' -> 'application/vnd.oasis.opendocument.database'. 'odc' -> 'application/vnd.oasis.opendocument.chart'. 'odf' -> 'application/vnd.oasis.opendocument.formula'. 'odg' -> 'application/vnd.oasis.opendocument.graphics'. 'odi' -> 'application/vnd.oasis.opendocument.image'. 'odm' -> 'application/vnd.oasis.opendocument.text-master'. 'odp' -> 'application/vnd.oasis.opendocument.presentation'. 'ods' -> 'application/vnd.oasis.opendocument.spreadsheet'. 'odt' -> 'application/vnd.oasis.opendocument.text'. 'ogg' -> 'application/ogg'. 'old' -> 'application/x-trash'. 'oth' -> 'application/vnd.oasis.opendocument.text-web'. 'oza' -> 'application/x-oz-application'. 'p' -> 'text/x-pascal'. 'p7r' -> 'application/x-pkcs7-certreqresp'. 'pac' -> 'application/x-ns-proxy-autoconfig'. 'pas' -> 'text/x-pascal'. 'pat' -> 'image/x-coreldrawpattern'. 'pbm' -> 'image/x-portable-bitmap'. 'pcf' -> 'application/x-font'. 'pcf.Z' -> 'application/x-font'. 'pcx' -> 'image/pcx'. 'pdb' -> 'chemical/x-pdb'. 'pdf' -> 'application/pdf'. 'pfa' -> 'application/x-font'. 'pfb' -> 'application/x-font'. 'pgm' -> 'image/x-portable-graymap'. 'pgn' -> 'application/x-chess-pgn'. 'pgp' -> 'application/pgp-signature'. 'pk' -> 'application/x-tex-pk'. 'pl' -> 'text/x-perl'. 'pls' -> 'audio/x-scpls'. 'pm' -> 'text/x-perl'. 'png' -> 'image/png'. 'pnm' -> 'image/x-portable-anymap'. 'pot' -> 'text/plain'. 'ppm' -> 'image/x-portable-pixmap'. 'pps' -> 'application/vnd.ms-powerpoint'. 'ppt' -> 'application/vnd.ms-powerpoint'. 'prf' -> 'application/pics-rules'. 'prt' -> 'chemical/x-ncbi-asn1-ascii'. 'ps' -> 'application/postscript'. 'psd' -> 'image/x-photoshop'. 'psp' -> 'text/x-psp'. 'py' -> 'text/x-python'. 'pyc' -> 'application/x-python-code'. 'pyo' -> 'application/x-python-code'. 'qt' -> 'video/quicktime'. 'qtl' -> 'application/x-quicktimeplayer'. 'ra' -> 'audio/x-realaudio'. 'ram' -> 'audio/x-pn-realaudio'. 'rar' -> 'application/rar'. 'ras' -> 'image/x-cmu-raster'. 'rd' -> 'chemical/x-mdl-rdfile'. 'rdf' -> 'application/rdf+xml'. 'rgb' -> 'image/x-rgb'. 'rm' -> 'audio/x-pn-realaudio'. 'roff' -> 'application/x-troff'. 'ros' -> 'chemical/x-rosdal'. 'rpm' -> 'application/x-redhat-package-manager'. 'rss' -> 'application/rss+xml'. 'rtf' -> 'text/rtf'. 'rtx' -> 'text/richtext'. 'rxn' -> 'chemical/x-mdl-rxnfile'. 'sct' -> 'text/scriptlet'. 'sd' -> 'chemical/x-mdl-sdfile'. 'sd2' -> 'audio/x-sd2'. 'sda' -> 'application/vnd.stardivision.draw'. 'sdc' -> 'application/vnd.stardivision.calc'. 'sdd' -> 'application/vnd.stardivision.impress'. 'sdf' -> 'chemical/x-mdl-sdfile'. 'sdp' -> 'application/vnd.stardivision.impress'. 'sdw' -> 'application/vnd.stardivision.writer'. 'ser' -> 'application/java-serialized-object'. 'sgf' -> 'application/x-go-sgf'. 'sgl' -> 'application/vnd.stardivision.writer-global'. 'sh' -> 'text/x-sh'. 'shar' -> 'application/x-shar'. 'shtml' -> 'text/html'. 'sid' -> 'audio/prs.sid'. 'sik' -> 'application/x-trash'. 'silo' -> 'model/mesh'. 'sis' -> 'application/vnd.symbian.install'. 'sit' -> 'application/x-stuffit'. 'skd' -> 'application/x-koan'. 'skm' -> 'application/x-koan'. 'skp' -> 'application/x-koan'. 'skt' -> 'application/x-koan'. 'smf' -> 'application/vnd.stardivision.math'. 'smi' -> 'application/smil'. 'smil' -> 'application/smil'. 'snd' -> 'audio/basic'. 'spc' -> 'chemical/x-galactic-spc'. 'spl' -> 'application/x-futuresplash'. 'src' -> 'application/x-wais-source'. 'stc' -> 'application/vnd.sun.xml.calc.template'. 'std' -> 'application/vnd.sun.xml.draw.template'. 'sti' -> 'application/vnd.sun.xml.impress.template'. 'stl' -> 'application/vnd.ms-pki.stl'. 'stw' -> 'application/vnd.sun.xml.writer.template'. 'sty' -> 'text/x-tex'. 'sv4cpio' -> 'application/x-sv4cpio'. 'sv4crc' -> 'application/x-sv4crc'. 'svg' -> 'image/svg+xml'. 'svgz' -> 'image/svg+xml'. 'sw' -> 'chemical/x-swissprot'. 'swf' -> 'application/x-shockwave-flash'. 'swfl' -> 'application/x-shockwave-flash'. 'sxc' -> 'application/vnd.sun.xml.calc'. 'sxd' -> 'application/vnd.sun.xml.draw'. 'sxg' -> 'application/vnd.sun.xml.writer.global'. 'sxi' -> 'application/vnd.sun.xml.impress'. 'sxm' -> 'application/vnd.sun.xml.math'. 'sxw' -> 'application/vnd.sun.xml.writer'. 't' -> 'application/x-troff'. 'tar' -> 'application/x-tar'. 'taz' -> 'application/x-gtar'. 'tcl' -> 'text/x-tcl'. 'tex' -> 'text/x-tex'. 'texi' -> 'application/x-texinfo'. 'texinfo' -> 'application/x-texinfo'. 'text' -> 'text/plain'. 'tgf' -> 'chemical/x-mdl-tgf'. 'tgz' -> 'application/x-gtar'. 'tif' -> 'image/tiff'. 'tiff' -> 'image/tiff'. 'tk' -> 'text/x-tcl'. 'tm' -> 'text/texmacs'. 'torrent' -> 'application/x-bittorrent'. 'tr' -> 'application/x-troff'. 'ts' -> 'text/texmacs'. 'tsp' -> 'application/dsptype'. 'tsv' -> 'text/tab-separated-values'. 'txt' -> 'text/plain'. 'udeb' -> 'application/x-debian-package'. 'uls' -> 'text/iuls'. 'ustar' -> 'application/x-ustar'. 'val' -> 'chemical/x-ncbi-asn1-binary'. 'vcd' -> 'application/x-cdlink'. 'vcf' -> 'text/x-vcard'. 'vcs' -> 'text/x-vcalendar'. 'vmd' -> 'chemical/x-vmd'. 'vms' -> 'chemical/x-vamas-iso14976'. 'vor' -> 'application/vnd.stardivision.writer'. 'vrm' -> 'x-world/x-vrml'. 'vrml' -> 'x-world/x-vrml'. 'vsd' -> 'application/vnd.visio'. 'wad' -> 'application/x-doom'. 'wav' -> 'audio/x-wav'. 'wax' -> 'audio/x-ms-wax'. 'wbmp' -> 'image/vnd.wap.wbmp'. 'wbxml' -> 'application/vnd.wap.wbxml'. 'wk' -> 'application/x-123'. 'wm' -> 'video/x-ms-wm'. 'wma' -> 'audio/x-ms-wma'. 'wmd' -> 'application/x-ms-wmd'. 'wml' -> 'text/vnd.wap.wml'. 'wmlc' -> 'application/vnd.wap.wmlc'. 'wmls' -> 'text/vnd.wap.wmlscript'. 'wmlsc' -> 'application/vnd.wap.wmlscriptc'. 'wmv' -> 'video/x-ms-wmv'. 'wmx' -> 'video/x-ms-wmx'. 'wmz' -> 'application/x-ms-wmz'. 'wp5' -> 'application/wordperfect5.1'. 'wpd' -> 'application/wordperfect'. 'wrl' -> 'x-world/x-vrml'. 'wsc' -> 'text/scriptlet'. 'wvx' -> 'video/x-ms-wvx'. 'wz' -> 'application/x-wingz'. 'xbm' -> 'image/x-xbitmap'. 'xcf' -> 'application/x-xcf'. 'xht' -> 'application/xhtml+xml'. 'xhtml' -> 'application/xhtml+xml'. 'xlb' -> 'application/vnd.ms-excel'. 'xls' -> 'application/vnd.ms-excel'. 'xlt' -> 'application/vnd.ms-excel'. 'xml' -> 'application/xml'. 'xpi' -> 'application/x-xpinstall'. 'xpm' -> 'image/x-xpixmap'. 'xsl' -> 'application/xml'. 'xtel' -> 'chemical/x-xtel'. 'xul' -> 'application/vnd.mozilla.xul+xml'. 'xwd' -> 'image/x-xwindowdump'. 'xyz' -> 'chemical/x-xyz'. 'zip' -> 'application/zip'. 'zmt' -> 'chemical/x-mopac-input'. '~' -> 'application/x-trash' } ! defaultPort ^ 4000 ! mimeTypeFor: aString ^ self mimeTypes at: (aString replace: '.*[\.]' with: '') ifAbsent: ['text/plain'] ! mimeTypes ^ mimeTypes ifNil: [mimeTypes := self defaultMimeTypes] ! printHelp console log: 'Available commandline options are:'. console log: '--help'. self commandLineSwitches do: [ :each | console log: each, ' '] ! selectorForCommandLineSwitch: aSwitch "Remove the trailing '--', add ':' at the end and replace all occurences of a lowercase letter preceded by a '-' with the Uppercase letter. Example: --fallback-page becomes fallbackPage:" ^ ((aSwitch replace: '^--' with: '') replace: '-[a-z]' with: [ :each | each second asUppercase ]), ':' ! ! !FileServer class methodsFor: 'initialization'! createServerWithArguments: options "If options are empty return a default FileServer instance. If options are given loop through them and set the passed in values on the FileServer instance. Commanline options map directly to methods in the 'accessing' protocol taking one parameter. Adding a method to this protocol makes it directly settable through command line options. " | server popFront front optionName optionValue switches | switches := self commandLineSwitches. server := self new. options ifEmpty: [^server]. (options size even) ifFalse: [ console log: 'Using default parameters.'. console log: 'Wrong commandline options or not enough arguments for: ' , (' ' join: options). console log: 'Use any of the following ones: ', (',' join: switches). ^server]. popFront := [:args | front := args first. args remove: front. front]. [options notEmpty] whileTrue: [ optionName := popFront value: options. optionValue := popFront value: options. (switches includes: optionName) ifTrue: [ optionName := self selectorForCommandLineSwitch: optionName. server perform: optionName withArguments: { optionValue } ] ifFalse: [ console log: optionName, ' is not a valid commandline option'. console log: 'Use any of the following ones: ', (',' join: switches) ]]. ^ server. ! main "Main entry point for Amber applications. Creates and starts a FileServer instance." | fileServer args | args := process argv. "Remove the first args which contain the path to the node executable and the script file." args removeFrom: 1 to: 3. args detect: [ :each | (each = '--help') ifTrue: [FileServer printHelp]] ifNone: [ fileServer := FileServer createServerWithArguments: args. ^ fileServer start] ! ! BaseFileManipulator subclass: #Initer slots: {#childProcess. #nmPath} package: 'AmberCli'! !Initer methodsFor: 'action'! finishMessage console log: (#( ' ' 'The project should now be set up.' ' ' ' ' ) join: String lf). [] valueWithTimeout: 600 ! gruntInitThenDo: aBlock | child sanitizedTemplatePath | sanitizedTemplatePath := ((path join: nmPath with: '@ambers/grunt-init-amber-project') replace: '\\' with: '\\') replace: ':' with: '\:'. child := childProcess fork: (self npmScriptForModule: 'grunt-init' named: 'grunt-init') args: {sanitizedTemplatePath}. child on: 'error' do: aBlock; on: 'close' do: [ :code | aBlock value: (code = 0 ifTrue: [ nil ] ifFalse: [ code ]) ] ! initProjectThenDo: aBlock | child | child := childProcess exec: 'npm run init' thenDo: aBlock. child stdout pipe: process stdout options: #{ 'end' -> false } ! start self gruntInitThenDo: [ :error | error ifNotNil: [ console log: 'grunt-init exec error:'; log: error. process exit: 101 ] ifNil: [ self initProjectThenDo: [ :error2 | error2 ifNotNil: [ console log: 'npm run init exec error:'; log: error2. process exit: 105 ] ifNil: [ self finishMessage. process exit ]]]] ! ! !Initer methodsFor: 'initialization'! initialize super initialize. childProcess := require value: 'child_process'. nmPath := path join: self rootDirname with: 'node_modules' ! ! !Initer methodsFor: 'npm'! npmScriptForModule: aString named: anotherString | modulePath packageJson binSection scriptPath | modulePath := path dirname: ( require provided resolve: aString, '/package.json'). packageJson := Smalltalk readJSObject: ( require value: aString, '/package.json'). binSection := packageJson at: 'bin'. scriptPath := binSection isString ifTrue: [ binSection ] ifFalse: [ binSection at: anotherString ]. ^ path join: modulePath with: scriptPath ! ! Object subclass: #Repl slots: {#readline. #interface. #util. #session. #resultCount. #commands} package: 'AmberCli'! !Repl commentStamp! I am a class representing a REPL (Read Evaluate Print Loop) and provide a command line interface to Amber Smalltalk. On the prompt you can type Amber statements which will be evaluated after pressing . The evaluation is comparable with executing a 'DoIt' in a workspace. My runtime requirement is a functional Node.js executable with working Readline support.! !Repl methodsFor: 'accessing'! commands ^ commands ! prompt ^ 'amber >> ' ! ! !Repl methodsFor: 'actions'! clearScreen | esc cls | esc := String fromCharCode: 27. cls := esc, '[2J', esc, '[0;0f'. process stdout write: cls. interface prompt ! close process stdin destroy ! createInterface interface := readline createInterface: process stdin stdout: process stdout. interface on: 'line' do: [:buffer | self processLine: buffer]. interface on: 'close' do: [self close]. self printWelcome; setupHotkeys; setPrompt. interface prompt ! eval: buffer ^ self eval: buffer on: DoIt new. ! eval: buffer on: anObject | result | buffer ifNotEmpty: [ [result := Compiler new evaluateExpression: buffer on: anObject] tryIfTrue: [ :e | (Smalltalk isError: e) not or: [ e context isNil ] ] catch: [ :e | process stdout write: e jsStack ]]. ^ result ! printWelcome Transcript show: 'Type :q to exit.'; cr. ! setPrompt interface setPrompt: self prompt ! ! !Repl methodsFor: 'initialization'! initialize super initialize. session := DoIt new. readline := require value: 'readline'. util := require value: 'util'. self setupCommands ! setupCommands commands := Dictionary from: { {':q'} -> [process exit]. {''} -> [interface prompt]} ! setupHotkeys process stdin on: 'keypress' do: [:s :key | key ifNotNil: [self onKeyPress: key]]. ! ! !Repl methodsFor: 'private'! addVariableNamed: aString to: anObject | newClass newObject | newClass := self subclass: anObject class withVariable: aString. self encapsulateVariable: aString withValue: anObject in: newClass. newObject := newClass new. self setPreviousVariablesFor: newObject from: anObject. ^ newObject ! assignNewVariable: buffer do: aBlock "Assigns a new variable and calls the given block with the variable's name and value if buffer contains an assignment expression. If it doesn't the block is called with nil for both arguments." ^ self parseAssignment: buffer do: [ :name :expr || varName value | varName := name ifNil: [self nextResultName]. session := self addVariableNamed: varName to: session. [ value := self eval: varName, ' := ', (expr ifNil: [buffer]) on: session ] on: Error do: [ :e | ConsoleErrorHandler new logError: e. value := nil]. aBlock value: varName value: value] ! encapsulateVariable: aString withValue: anObject in: aClass "Add getter and setter for given variable to session." | compiler | compiler := Compiler new. compiler install: aString, ': anObject ^ ', aString, ' := anObject' forClass: aClass protocol: 'session'. compiler install: aString, ' ^ ', aString forClass: aClass protocol: 'session'. ! executeCommand: aString "Tries to process the given string as a command. Returns true if it was a command, false if not." self commands keysAndValuesDo: [:names :cmd | (names includes: aString) ifTrue: [ cmd value. ^ true]]. ^ false ! instanceVariableNamesFor: aClass "Yields all instance variable names for the given class, including inherited ones." ^ aClass superclass ifNotNil: [ aClass instanceVariableNames copyWithAll: (self instanceVariableNamesFor: aClass superclass)] ifNil: [ aClass instanceVariableNames] ! isIdentifier: aString ^ aString match: '^[a-z_]\w*$' asRegexp ! isVariableDefined: aString ^ (self instanceVariableNamesFor: session class) includes: aString ! nextResultName resultCount := resultCount ifNotNil: [resultCount + 1] ifNil: [1]. ^ 'res', resultCount asString ! onKeyPress: key (key ctrl and: [key name = 'l']) ifTrue: [self clearScreen] ! parseAssignment: aString do: aBlock "Assigns a new variable if the given string is an assignment expression. Calls the given block with name and value. If the string is not one no variable will be assigned and the block will be called with nil for both arguments." | assignment | assignment := (aString tokenize: ':=') collect: [:s | s trimBoth]. ^ (assignment size = 2 and: [self isIdentifier: assignment first]) ifTrue: [ aBlock value: assignment first value: assignment last ] ifFalse: [ aBlock value: nil value: nil ] ! presentResultNamed: varName withValue: value Transcript show: varName, ': ', value class name, ' = ', value asString; cr. interface prompt ! processLine: buffer "Processes lines entered through the readline interface." | show | show := [:varName :value | self presentResultNamed: varName withValue: value]. (self executeCommand: buffer) ifFalse: [ (self isVariableDefined: buffer) ifTrue: [show value: buffer value: (session perform: buffer)] ifFalse: [self assignNewVariable: buffer do: show]] ! setPreviousVariablesFor: newObject from: oldObject (self instanceVariableNamesFor: oldObject class) do: [:each | newObject perform: each, ':' withArguments: {oldObject perform: each}]. ! subclass: aClass withVariable: varName "Create subclass with new variable." ^ ClassBuilder new addSubclassOf: aClass named: (self subclassNameFor: aClass) asSymbol slots: {varName} package: 'Compiler-Core' ! subclassNameFor: aClass ^ (aClass name matchesOf: '\d+$') ifNotNil: [ | counter | counter := (aClass name matchesOf: '\d+$') first asNumber + 1. aClass name replaceRegexp: '\d+$' asRegexp with: counter asString] ifNil: [ aClass name, '2']. ! ! !Repl class methodsFor: 'initialization'! main self new createInterface ! !