tendersync 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,13 @@
1
+ == 1.0.2 2009-08-15
2
+
3
+ * Fixed some bugs in posting
4
+
5
+ == 1.0.1 2009-07-17
6
+
7
+ * Indexing working
8
+ * More command line options
9
+ * Tests and Documentation
10
+
11
+ == 1.0.0 2009-06-11
12
+
13
+ * First Release
data/Manifest ADDED
@@ -0,0 +1,27 @@
1
+ bin/tendersync
2
+ config/website.yml.sample
3
+ History.txt
4
+ lib/tendersync/document.rb
5
+ lib/tendersync/runner.rb
6
+ lib/tendersync/session.rb
7
+ lib/tendersync/tendersync.rb
8
+ lib/tendersync.rb
9
+ Manifest
10
+ Rakefile
11
+ README.md
12
+ script/console
13
+ script/destroy
14
+ script/generate
15
+ script/txt2html
16
+ spec/fixtures/passenger_restart_issues
17
+ spec/spec.opts
18
+ spec/spec_helper.rb
19
+ spec/tendersync_document_spec.rb
20
+ spec/tendersync_session_spec.rb
21
+ spec/tendersync_spec.rb
22
+ tasks/rspec.rake
23
+ website/index.html
24
+ website/index.txt
25
+ website/javascripts/rounded_corners_lite.inc.js
26
+ website/stylesheets/screen.css
27
+ website/template.html.erb
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Tendersync
2
+
3
+ Authors: Markus Roberts and Bill Kayser
4
+
5
+ Tendersync allows you to sync documents stored in the
6
+ [ENTP Tender](http://www.tenderapp.com)
7
+ `faqs` section with a local filesystem, allowing you to manage your
8
+ documents with git or subversion.
9
+
10
+ It includes a command for creating an index document for any given
11
+ section.
12
+
13
+ Find out more about Tender by visiting [the Tender site](http://www.tenderapp.com).
14
+
15
+ ## Features
16
+
17
+ * List remote sections and documents under the `/faqs` area
18
+ * Pull single documents or entire tree from Tender site to local
19
+ filesystem
20
+ * Push local changes back to Tender one at a time or en masse
21
+ * Manage document meta-data, like keywords, in headers
22
+ * Push changed versions to the server
23
+
24
+ ## Synopsis
25
+
26
+ Create a working directory where you want to store the tender docs in a hierarchy
27
+ and run tendersync from there.
28
+
29
+ sudo gem install tendersync
30
+ cd $workdir
31
+ tendersync -h
32
+
33
+ ## Using Tendersync
34
+
35
+ To get started, you need to pass in your account information. You
36
+ only need to do this once. A local file `.tendersync` is created with
37
+ the configuration information.
38
+
39
+ This will get you set up:
40
+
41
+ tendersync -u user@me.com -p password --docurl=http://company.tenderapp.com
42
+
43
+ To verify it worked run the `ls` command:
44
+
45
+ tendersync ls
46
+
47
+ Tender documents are organized into sections defined by you. At New
48
+ Relic, we have faqs, docs, and troubleshooting. You can specify
49
+ commands to apply to one or more sections by passing in section names
50
+ with -s:
51
+
52
+ tendersync -s docs -s troubleshooting pull
53
+
54
+ ### Examples
55
+
56
+ Start with:
57
+
58
+ tendersync -h
59
+
60
+ Download all your docs:
61
+
62
+ tendersync pull
63
+
64
+ Download just the faq docs:
65
+
66
+ tendersync pull -s faqs
67
+
68
+ Create a git repository and save all the documents:
69
+
70
+ git init
71
+ git add .
72
+ git commit -m "First version of docs on Tender"
73
+
74
+ Upload docs to the server:
75
+
76
+ tendersync post faqs/sinatra_support
77
+ tendersync post docs/install-*
78
+ tendersync post -s docs
79
+
80
+ Upload everything to the server (regardless of whether the content has
81
+ changed or not):
82
+
83
+ tendersync post
84
+
85
+ ### Using the `index` Command
86
+
87
+ You can generate a table of contents for any section with the index
88
+ command. By default index will generate a single file named
89
+ `SECTION_table_of_contents`.
90
+
91
+ In this file will be a list of all the files in the given section with
92
+ links to those files. Under each file link will be a bullet list of
93
+ the topmost sections in the document. If these sections are preceeded
94
+ by anchor links (A elements with the name attribute) then the bullets
95
+ will have links to those sections.
96
+
97
+ It will look something like this:
98
+
99
+ <pre>
100
+ ## Installation and configuration
101
+ ### [Agent Installation (Ruby)](agent-installation)
102
+ * [Installing the Plug-in](agent-installation#Installing_the_Plug-in)
103
+ * [Installing the Gem](agent-installation#Installing_the_Gem)
104
+ </pre>
105
+
106
+ #### Customizing the amount of detail in the Index
107
+
108
+ You can show sections deeper than one level in a particular document
109
+ using the `-d` option. The default is 1.
110
+
111
+ tendersync index -d 2
112
+
113
+ #### Definiting TOC groups
114
+
115
+ If you want to divide the table of contents into groups of related
116
+ documents, you can pass in a title for a group and a regular expression
117
+ to match against document titles that belong in that group. These group
118
+ definitions will be saved so you only need to enter them once.
119
+
120
+ Enter a group using the `-g` option passing in a title and regular
121
+ expression separated by a semi-colon.
122
+
123
+ tendersync index -g "Page Details;/page/i"
124
+
125
+ You can add multiple groups with additional -g options:
126
+
127
+ tendersync index -g "Page Details;/page/i" -g "Installation Info;/installation/i"
128
+
129
+ If you want to remove a group definition, you need to remove it
130
+ manually from the `.tendersync` file.
131
+
132
+ ## THANKS
133
+
134
+ All due regards, credit, thanks, etc., to the ENTP team for a great tool.
135
+
136
+ ## LICENSE
137
+
138
+ Copyright (c) 2009 New Relic, Inc.
139
+
140
+ Permission is hereby granted, free of charge, to any person obtaining
141
+ a copy of this software and associated documentation files (the
142
+ 'Software'), to deal in the Software without restriction, including
143
+ without limitation the rights to use, copy, modify, merge, publish,
144
+ distribute, sublicense, and/or sell copies of the Software, and to
145
+ permit persons to whom the Software is furnished to do so, subject to
146
+ the following conditions:
147
+
148
+ The above copyright notice and this permission notice shall be
149
+ included in all copies or substantial portions of the Software.
150
+
151
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
152
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
153
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
154
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
155
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
156
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
157
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'echoe'
3
+ %w[rake rake/clean fileutils newgem rubigen].each { |f| require f }
4
+ require File.dirname(__FILE__) + '/lib/tendersync'
5
+
6
+ GEM_NAME = "tendersync"
7
+ GEM_VERSION = Tendersync::VERSION
8
+ AUTHOR = "Bill Kayser"
9
+ EMAIL = "bkayser@newrelic.com"
10
+ HOMEPAGE = "http://www.github.com/newrelic/tendersync"
11
+ SUMMARY = "Utility for syncing and indexing files from ENTP's Tender site."
12
+ DESCRIPTION = <<-EOF
13
+ Tendersync is a utility for syncing files from ENTP's Tender site for managing customer facing documentation. It can be used to pull and push documents to a local repository as well as create indexes for each documentation section.
14
+ EOF
15
+
16
+ # Generate all the Rake tasks
17
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
18
+ Echoe.new(GEM_NAME, Tendersync::VERSION) do |p|
19
+ p.author = AUTHOR
20
+ p.email = EMAIL
21
+ p.summary = SUMMARY
22
+ p.url = HOMEPAGE
23
+ p.project = 'newrelic'
24
+ p.description = DESCRIPTION
25
+ p.version = Tendersync::VERSION
26
+ p.need_tar_gz = false
27
+ p.need_gem = true
28
+ p.bin_files = 'bin/tendersync'
29
+ p.runtime_dependencies = [
30
+ ['mechanize','>= 0.9.3'],
31
+ ]
32
+ p.development_dependencies = [
33
+ ['newgem', ">= #{::Newgem::VERSION}"]
34
+ ]
35
+ p.ignore_pattern = %w[docs/** general/** troubleshooting/**]
36
+ p.clean_pattern |= %w[**/.DS_Store tmp *.log]
37
+ end
38
+
39
+ Dir['tasks/**/*.rake'].each { |t| load t }
40
+
data/bin/tendersync ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2009-6-11.
4
+ # Copyright (c) 2009. All rights reserved.
5
+
6
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/tendersync")
7
+
8
+ require "tendersync/runner"
9
+
10
+ begin
11
+ Tendersync::Runner.new(ARGV.dup).run
12
+ rescue Tendersync::Runner::Error => e
13
+ puts e.message
14
+ end
@@ -0,0 +1,2 @@
1
+ host: unknown@rubyforge.org
2
+ remote_dir: /var/www/gforge-projects/tendersync
@@ -0,0 +1,218 @@
1
+ require 'fileutils'
2
+ require 'set'
3
+ require 'yaml'
4
+
5
+ class Tendersync::Document
6
+ Properties = [:section, :document_id, :title, :permalink, :keywords, :body]
7
+ attr_accessor *Properties
8
+
9
+ NUM_DASHES = 28 # the number of dashes in keyword fields
10
+
11
+ class TOCEntry
12
+ attr_reader :name, :link, :level
13
+ attr_accessor :parent
14
+ def initialize name, link=nil, level=nil
15
+ @name = name
16
+ @link = link if link
17
+ @level = level if level
18
+ end
19
+ def children
20
+ @children ||= []
21
+ end
22
+ # Write this element and all children, recursively as bullet lists with links
23
+ def write_entries(io, depth=1, indent = 0, doc_link = parent.link)
24
+ io.write " " * 4 * indent # indentation
25
+ io.write "* " # bullet
26
+ if link
27
+ io.puts "[#{name}](#{doc_link}##{self.link})"
28
+ else
29
+ io.puts name
30
+ end
31
+ children.each { | child | child.write_entries(io, depth-1, indent+1, doc_link)} unless depth == 1
32
+ end
33
+ def add child
34
+ if !parent || child.level > self.level
35
+ children << child
36
+ child.parent = self
37
+ else
38
+ parent.add(child)
39
+ end
40
+ child
41
+ end
42
+ end
43
+
44
+ class Group < TOCEntry
45
+ attr_reader :title_regex
46
+ def initialize(name, title_regex=//)
47
+ super(name, nil, nil)
48
+ @title_regex = title_regex
49
+ end
50
+ Default = Group.new('Other')
51
+
52
+ end
53
+
54
+ def initialize(values={})
55
+ values.each do | prop, value |
56
+ self.send "#{prop}=", value
57
+ end
58
+ end
59
+ #
60
+ # Documents can be read from / written to a file
61
+ #
62
+ def to_s
63
+ io = StringIO.new
64
+ Properties.each do |field|
65
+ next unless value = self.send(field)
66
+ io.write "-" * NUM_DASHES
67
+ io.write " #{field} "
68
+ io.write "-" * NUM_DASHES
69
+ io.puts
70
+ io.puts value
71
+ end
72
+ io.string
73
+ end
74
+
75
+ def save
76
+ FileUtils.mkdir_p section
77
+ File.open("#{section}/#{permalink}",'w') { |f| f.print self }
78
+ self
79
+ end
80
+
81
+ def self.load(section, io)
82
+ values = { :section => section }
83
+ key = data = nil
84
+ while line = io.gets
85
+ line.chomp!
86
+ if line =~ /^----+ (.+) -----+$/
87
+ values[key] = data.join("\n") if data
88
+ key = $1.intern
89
+ data = []
90
+ else
91
+ raise "keyword line not recognized: #{line}" unless data
92
+ data << line
93
+ end
94
+ end
95
+ values[key] = data.join("\n") if key
96
+ new values
97
+ end
98
+
99
+ def self.read_from_file(file_name)
100
+ section = file_name.split('/')[-2]
101
+ if !File.exists? file_name
102
+ raise Tendersync::Runner::Error, "Cannot read #{file_name}"
103
+ end
104
+ File.open(file_name) { |f| self.load(section, f) }
105
+ end
106
+
107
+ #
108
+ # Can be scraped from a form
109
+ #
110
+ def self.from_form(section,form)
111
+ values = {
112
+ :document_id => form.action[%r{/faqs/(\d+)/edit},1],
113
+ :section => section
114
+ }
115
+ form.fields.each { |tf|
116
+ if field_name = tf.name[/faq\[(.*)\]/,1]
117
+ value = tf.value.map { |line| line.chomp }.join("\n")
118
+ values[field_name.intern] = value
119
+ end
120
+ }
121
+ new(values)
122
+ end
123
+ def to_form(form)
124
+ form.fields.each { |tf|
125
+ if field_name = tf.name[/faq\[(.*)\]/,1] and self.send(field_name.intern)
126
+ lines = []
127
+ self.send(field_name.intern).each_line {|line| lines << line.chomp }
128
+ tf.value = lines.join("\r\n")
129
+ end
130
+ }
131
+ end
132
+
133
+ def self.each(section)
134
+ Dir.glob("#{section}/*").each { |f| yield Tendersync::Document.read_from_file(f) }
135
+ end
136
+
137
+ def self.index_for(section_id, section_name)
138
+ new(:section => section_id,
139
+ :title => "#{section_name} Table of Contents",
140
+ :permalink => "#{section_id}-table-of-contents",
141
+ :keywords => "toc index")
142
+ end
143
+
144
+ # Update this document body with an index of all the documents in
145
+ # this section. Underneath the TOC entry for a document will be sub
146
+ # entries for each named A element.
147
+ #
148
+ # group_map is an associative array of /regex/ to "Title String" of
149
+ # a group of documents. It is used to divide up documents into
150
+ # groups within the table of contents. A document is placed in a
151
+ # TOC group based on the first regex it matches in the group map.
152
+ #
153
+ # If group_map is empty then headings will be sorted alphabetically
154
+ # and not grouped.
155
+ #
156
+ # depth s the number of nested levels to descend into a document.
157
+ def refresh_index(groups=[], depth=2)
158
+ generate_index(create_toc(groups), depth)
159
+ end
160
+
161
+ private
162
+
163
+ def create_toc(groups)
164
+ groups += groups + [Group::Default] # array of groups
165
+ link_root = {}
166
+ self.class.each(section) do |document|
167
+ next if document.permalink =~ /-table-of-contents$/
168
+ puts "indexing #{document.permalink}..."
169
+ title = document.title
170
+ group = groups.detect { | g | title =~ g.title_regex }
171
+ doc_entry = TOCEntry.new title, document.permalink, 0
172
+ group.add doc_entry
173
+ last = doc_entry
174
+ link = nil
175
+ document.body.scan(%r{<a name=(.*?)>|^(#+)\s*(.*?)\s*$}i) do
176
+ name = $1
177
+ heading_level = $2 && $2.length
178
+ text = $3
179
+ if name
180
+ # Record the link name for the next header
181
+ link = eval(name)
182
+ elsif heading_level == 1
183
+ last = last.add(TOCEntry.new(text, link, heading_level))
184
+ elsif heading_level >= 2 # level 2
185
+ last = last.add(TOCEntry.new(text, link, heading_level))
186
+ link = nil
187
+ end
188
+ end
189
+ end
190
+ groups
191
+ end
192
+
193
+ def generate_index(groups, depth)
194
+ groups.reject! { | group | group.children.empty? }
195
+ # Now go through each group
196
+ io = StringIO.new
197
+ io.puts
198
+ groups.each do | group |
199
+ # Show the group heading unless there is only one group
200
+ io.puts "## #{group.name}" unless groups.size == 1
201
+ group.children.each do | doc_entry |
202
+ doc_link = doc_entry.link
203
+ io.puts "### [#{doc_entry.name}](#{doc_link})"
204
+ doc_entry.children.each do | doc_section |
205
+ doc_section.write_entries(io, depth)
206
+ end
207
+ io.puts
208
+ end
209
+ end
210
+ if $dry_run
211
+ puts io.string
212
+ else
213
+ self.body = io.string
214
+ end
215
+ end
216
+
217
+ end
218
+
@@ -0,0 +1,211 @@
1
+ require 'rubygems'
2
+ require 'optparse'
3
+ require 'tendersync/session'
4
+ require 'tendersync/document'
5
+ require 'mechanize'
6
+ require 'yaml'
7
+
8
+ class Tendersync::Runner
9
+ class Error < StandardError; end
10
+
11
+ def initialize argv
12
+ @dry_run = false
13
+ @sections = []
14
+ @groups = []
15
+ settings['groups'] ||= []
16
+ @parser = OptionParser.new do |op|
17
+ op.banner += " command\n"
18
+ op.on('-n', "dry run" ) { @dry_run = true }
19
+ op.on('-s', '--section', '=SECTION', String, "section, specify multiple separately" ) { |s| @sections << s }
20
+ op.on('-u', '--username','=EMAIL', String, "* login e-mail" ) {|str| settings['username'] = str }
21
+ op.on('-p', '--password','=PASS', String, "* password" ) {|str| settings['password'] = str }
22
+ op.on( '--docurl', '=URL', String, "* tender site URL" ) { |dir| settings['docurl'] = dir }
23
+ op.separator ""
24
+ op.separator "Indexing Options:"
25
+ op.on('-g', '--group', '=TITLE;regex', String, "*map of regex to group title for TOC groups") do | g |
26
+ pair = g.split(';')
27
+ settings['groups'] << [pair.first, pair.last]
28
+ end
29
+ op.on('-d', '--depth', '=DEPTH', String, "*Number of levels to descend into a document being indexed") { | g | settings['depth'] = g.to_i }
30
+
31
+
32
+ %Q{
33
+ * saved in .tendersync file for subsequent default
34
+
35
+ Commands:
36
+
37
+ pull [URL, URL...] -- download documents from tender; specify sections with -s, a page URL, or
38
+ nothing to download all documents
39
+ index -- create a master index of each section, writing to section/file; specify
40
+ the sections with -s options; you can organize the TOC into groups by
41
+ mapping document titles to groups via a regular expression with -g options
42
+ ls -- list files in specified session
43
+ post PATTERN -- post the matching documents to tender; use /regexp/ or glob
44
+ irb -- drops you into IRB with a tender session & related classes (for hacking/
45
+ one-time tasks). Programmers only.
46
+ create PERMALINK -- create a new tender document with the specified permalink in the section
47
+ specified by --section=... (must be only one.)
48
+
49
+ }.split(/\n/).each {|line| op.separator line.chomp }
50
+ end
51
+
52
+ begin
53
+ @command,*@args = *@parser.parse(argv)
54
+ rescue OptionParser::InvalidOption => e
55
+ raise Error, e.message
56
+ end
57
+
58
+ @username = settings['username']
59
+ @password = settings['password']
60
+ @dochome = settings['docurl'] && settings['docurl'] =~ /^(http.*?)\/?$/ && $1
61
+ @root = settings['root']
62
+
63
+ case
64
+ when ! @username
65
+ raise Error, "Please enter a username and password. You only need to do this once."
66
+ when ! @password
67
+ raise Error, "Please enter a password. You only need to do this once."
68
+ when ! @dochome
69
+ raise Error, "Please enter a --docurl indicating the home page URL of your Tender docs.\n" +
70
+ "You only need to do this once."
71
+ else
72
+ settings.save!
73
+ end
74
+ end
75
+
76
+ def run
77
+ $session = Tendersync::Session.new @dochome, @username, @password
78
+ $dry_run = @dry_run
79
+ case @command || 'help'
80
+ when 'help'
81
+ raise Error, @parser.to_s
82
+ when *%w[pull post create irb ls index]
83
+ send @command
84
+ else
85
+ raise Error, "Unknown command: #{@command}\n\n#{@parser}"
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def ls
92
+ $session.ls *sections
93
+ end
94
+
95
+ def pull
96
+ if @args.size > 0
97
+ @args.each do |url|
98
+ section = url =~ /\/faqs\/([^\/]*)\// && $1
99
+ raise Error, "Invalid URI for document: #{url}" if section.nil?
100
+ doc = Document.from_form(section, $session.edit_page_for(url).form_with(:action => /edit/))
101
+ puts " #{doc.permalink}"
102
+ doc.save unless $dry_run
103
+ end
104
+ else
105
+ sections.each do |section|
106
+ puts "pulling #{section} docs ..."
107
+ $session.pull_from_tender(section)
108
+ end
109
+ end
110
+ end
111
+
112
+ def post
113
+ documents = @args.collect { |doc_name|
114
+ matches = if doc_name =~ %r{/}
115
+ Dir.glob(doc_name)
116
+ else
117
+ Dir.glob("#{@root}/{#{sections.join(',')}}/#{doc_name}*")
118
+ end
119
+ if matches.empty?
120
+ puts "No documents match #{doc_name}"
121
+ else
122
+ matches.collect { |match| Tendersync::Document.read_from_file(match) }
123
+ end
124
+ }.flatten.compact
125
+ documents.each { |document|
126
+ if @dry_run
127
+ puts "would post #{document.section}/#{document.permalink} to tender."
128
+ else
129
+ $session.post(document)
130
+ end
131
+ }
132
+ end
133
+ alias push post
134
+
135
+ def create
136
+ raise Error, "You must specify exactly one section to put the document in." if sections.length != 1
137
+ raise Error, "You must specify exactly one document permalink." if @args.length != 1
138
+ section,permalink = sections.first,@args.first
139
+ filename = "#{@root}/#{section}/#{permalink}"
140
+ text = File.read(filename) rescue ""
141
+ text = "Put Text Here" if text.strip.empty?
142
+ if $dry_run
143
+ puts "would create document #{permalink}\nin #{section} as #{filename}"
144
+ puts "\ntext:\n------------\n#{text}"
145
+ else
146
+ document = $session.create_document(section,permalink,text)
147
+ document.save
148
+ end
149
+ end
150
+
151
+ def irb
152
+ puts <<-EOF
153
+
154
+ Use $session to access the Tendersync::Session instance.
155
+ Use Tendersync::Document to manipulate documents local and remote.
156
+
157
+ Examples of crazy stuff you could try:
158
+
159
+ puts $session.all_sections.inspect
160
+
161
+ $session.pull_from_tender('troubleshooting')
162
+
163
+ $session.post(Tendersync::Document.index('docs').save)
164
+
165
+ Tendersync::Document.each { |d| puts d.body.split(/\W/).join("\\n") }
166
+
167
+ doc = Tendersync::Document.read_from_file("./docs/agent-api")
168
+ doc.body.gsub! /api/,"API"
169
+ doc.save
170
+
171
+ EOF
172
+ ARGV.clear
173
+ require 'irb'
174
+ require 'irb/completion'
175
+ $sections = sections
176
+ IRB.start
177
+ end
178
+ def index
179
+ groups = settings['groups'].map do |title,regex|
180
+ regex = eval(regex) if regex =~ %r{^/.*/[a-z]*$}
181
+ Tendersync::Document::Group.new title, Regexp.new(regex)
182
+ end
183
+ section_details = $session.all_sections
184
+ sections.each do |section|
185
+ doc = Tendersync::Document.index_for section, section_details[section]
186
+ puts "indexing #{section}: #{doc.section}/#{doc.permalink}"
187
+ doc.refresh_index groups, settings['depth'] || 2
188
+ doc.save
189
+ end
190
+ end
191
+ def sections
192
+ @sections = $session.all_sections.keys if @sections.empty?
193
+ @sections
194
+ end
195
+ def settings
196
+ case
197
+ when @settings
198
+ return @settings
199
+ when File.exists?(".tendersync")
200
+ File.open(".tendersync", "r") { |f| @settings = YAML.load(f) }
201
+ else
202
+ @settings = {}
203
+ end
204
+ def @settings.save!
205
+ File.open(".tendersync","w") do |f|
206
+ f.write(self.to_yaml)
207
+ end
208
+ end
209
+ @settings
210
+ end
211
+ end