@pixlcore/xycli 1.0.0
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/README.md +543 -0
- package/docs/help.md +2587 -0
- package/docs/screenshots.md +219 -0
- package/docs/sync.md +920 -0
- package/docs/tutorial.md +204 -0
- package/index.js +444 -0
- package/lib/alerts.js +735 -0
- package/lib/apikey.js +425 -0
- package/lib/buckets.js +607 -0
- package/lib/categories.js +323 -0
- package/lib/channels.js +290 -0
- package/lib/config.js +86 -0
- package/lib/dashboard.js +237 -0
- package/lib/doc.js +131 -0
- package/lib/events.js +1314 -0
- package/lib/groups.js +1038 -0
- package/lib/jobs.js +1254 -0
- package/lib/log.js +125 -0
- package/lib/marketplace.js +386 -0
- package/lib/monitors.js +412 -0
- package/lib/plugins.js +491 -0
- package/lib/secrets.js +441 -0
- package/lib/servers.js +1502 -0
- package/lib/snapshots.js +498 -0
- package/lib/sync.js +963 -0
- package/lib/system.js +612 -0
- package/lib/tags.js +250 -0
- package/lib/tickets.js +681 -0
- package/lib/transfer.js +447 -0
- package/lib/utils.js +2745 -0
- package/lib/webhook.js +414 -0
- package/logo.webp +0 -0
- package/package.json +38 -0
- package/test/README.md +97 -0
- package/test/api-validation.test.js +170 -0
- package/test/categories.test.js +246 -0
- package/test/channels.test.js +209 -0
- package/test/config.test.js +96 -0
- package/test/doc.test.js +84 -0
- package/test/groups.test.js +211 -0
- package/test/help.test.js +26 -0
- package/test/helpers/common.js +127 -0
- package/test/helpers/pagination-preload.cjs +18 -0
- package/test/log.test.js +103 -0
- package/test/markdown.test.js +61 -0
- package/test/marketplace.test.js +229 -0
- package/test/monitors.test.js +227 -0
- package/test/pagination.test.js +138 -0
- package/test/plugins.test.js +351 -0
- package/test/secrets.test.js +271 -0
- package/test/servers.test.js +303 -0
- package/test/snapshots.test.js +167 -0
- package/test/sync-unit.test.js +446 -0
- package/test/sync.test.js +542 -0
- package/test/system.test.js +94 -0
- package/test/tags.test.js +230 -0
- package/test/tickets.test.js +261 -0
- package/test/toast.test.js +73 -0
- package/test/transfer-apikey.test.js +96 -0
- package/test/transfer-workflow.test.js +122 -0
- package/test/transfer.test.js +314 -0
- package/test/webhook.test.js +332 -0
- package/test.js +49 -0
package/lib/config.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Config Layer
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const Path = require('path');
|
|
5
|
+
const cli = require('pixl-cli');
|
|
6
|
+
const Tools = cli.Tools;
|
|
7
|
+
const chalk = cli.chalk;
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
|
|
11
|
+
loadConfig() {
|
|
12
|
+
// handle loading config at startup
|
|
13
|
+
// optional config files, sync with env, two-way
|
|
14
|
+
var config = {};
|
|
15
|
+
var config_files = this.configFiles = [
|
|
16
|
+
Path.join( '/etc', 'xyops', 'cli.json' ),
|
|
17
|
+
Path.join( process.env.HOME, '.config', 'xyops', 'cli.json' )
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
config_files.forEach( function(file) {
|
|
21
|
+
if (!fs.existsSync(file)) return;
|
|
22
|
+
Tools.mergeHashInto( config, JSON.parse(fs.readFileSync(file, 'utf8')) );
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
for (var ekey in process.env) {
|
|
26
|
+
if (ekey.match(/^XYOPS_(\w+)$/)) {
|
|
27
|
+
var key = RegExp.$1.toLowerCase();
|
|
28
|
+
var value = process.env[ekey];
|
|
29
|
+
if (value === "true") value = true;
|
|
30
|
+
else if (value === "false") value = false;
|
|
31
|
+
else if (String(value).match(/^\-?\d+(\.\d+)?$/)) value = parseFloat(value);
|
|
32
|
+
else if (String(value).match(/^\-?\d+$/)) value = parseInt(value);
|
|
33
|
+
config[key] = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (var key in config) {
|
|
38
|
+
process.env['XYOPS_' + key.toUpperCase()] = '' + config[key];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.config = config;
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async cmd_config() {
|
|
45
|
+
// allow user to view config, and also modify it
|
|
46
|
+
delete this.args.other;
|
|
47
|
+
|
|
48
|
+
if (!Tools.firstKey(this.args)) {
|
|
49
|
+
// just print config and exit
|
|
50
|
+
println( "\n " + this.color('theme').bold( 'CONFIGURATION SOURCES:' ) + "\n" );
|
|
51
|
+
this.configFiles.forEach( function(file) {
|
|
52
|
+
println( bold.green(" • ") + file );
|
|
53
|
+
} );
|
|
54
|
+
println( bold.green(" • ") + "(XYOPS_ prefixed env vars)" );
|
|
55
|
+
|
|
56
|
+
println( "\n " + this.color('theme').bold( 'CURRENT CONFIGURATION:' ) );
|
|
57
|
+
var temp_config = { ...this.config };
|
|
58
|
+
if (temp_config.api_key) temp_config.api_key = this.maskValue(temp_config.api_key);
|
|
59
|
+
this.jsonOutput(temp_config);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Reload only the user's settings before applying changes. The effective
|
|
64
|
+
// configuration also contains system settings and environment overrides,
|
|
65
|
+
// which must not be copied into the user's file when saving preferences.
|
|
66
|
+
var dest_file = this.configFiles[this.configFiles.length - 1];
|
|
67
|
+
var user_config = fs.existsSync(dest_file) ? JSON.parse(fs.readFileSync(dest_file, 'utf8')) : {};
|
|
68
|
+
|
|
69
|
+
// Apply only the supplied arguments, preserving other user settings.
|
|
70
|
+
print("\n");
|
|
71
|
+
for (var key in this.args) {
|
|
72
|
+
var value = (key == 'api_key') ? this.maskValue(this.args[key]) : this.args[key];
|
|
73
|
+
println( " " + bold.cyan("Setting key: ") + bold.green(key) + ": " + JSON.stringify(value) );
|
|
74
|
+
|
|
75
|
+
if (!this.setPath(user_config, key, this.args[key])) {
|
|
76
|
+
this.die("Invalid configuration path: " + key);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Tools.mkdirpSync( Path.dirname(dest_file) );
|
|
81
|
+
println( "\n " + bold.cyan("Writing to: ") + dest_file );
|
|
82
|
+
fs.writeFileSync( dest_file, JSON.stringify(user_config, null, "\t") + "\n", { mode: 0o600 } );
|
|
83
|
+
fs.chmodSync(dest_file, 0o600);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
};
|
package/lib/dashboard.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Dashboard Layer
|
|
2
|
+
|
|
3
|
+
const cli = require('pixl-cli');
|
|
4
|
+
const Tools = cli.Tools;
|
|
5
|
+
const chalk = cli.chalk;
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
|
|
9
|
+
async cmd_dashboard() {
|
|
10
|
+
// show dashboard
|
|
11
|
+
await this.getMultiple();
|
|
12
|
+
|
|
13
|
+
var stats = this.stats;
|
|
14
|
+
var day = stats.currentDay || { transactions: {} };
|
|
15
|
+
var trans = day.transactions || {};
|
|
16
|
+
|
|
17
|
+
// calculate total CPU usage
|
|
18
|
+
var total_cpu = 0;
|
|
19
|
+
|
|
20
|
+
// add CPU for all active jobs (avg)
|
|
21
|
+
for (var job_id in this.activeJobs) {
|
|
22
|
+
var job = this.activeJobs[job_id];
|
|
23
|
+
if (job.cpu && job.cpu.total && job.cpu.count) {
|
|
24
|
+
total_cpu += job.cpu.total / job.cpu.count;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// calculate total memory usage
|
|
29
|
+
var total_mem = 0;
|
|
30
|
+
|
|
31
|
+
// add mem for all jobs (avg)
|
|
32
|
+
for (var job_id in this.activeJobs) {
|
|
33
|
+
var job = this.activeJobs[job_id];
|
|
34
|
+
if (job.mem && job.mem.total && job.mem.count) {
|
|
35
|
+
total_mem += job.mem.total / job.mem.count;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
println( "\n " + this.color('theme').bold("MAIN DASHBOARD") );
|
|
40
|
+
println( "" + dashGrid([
|
|
41
|
+
[ "Conductors", Tools.numKeys(this.masters) ],
|
|
42
|
+
[ "Servers", Tools.numKeys(this.servers) ],
|
|
43
|
+
[ "Current Alerts", Tools.numKeys(this.activeAlerts) ],
|
|
44
|
+
[ "Active Jobs", Tools.numKeys(this.activeJobs) ],
|
|
45
|
+
[ "Jobs Today", Tools.commify(trans.job_complete || 0) ],
|
|
46
|
+
[ "Jobs Failed Today", Tools.commify(trans.job_error || 0) ],
|
|
47
|
+
[ "Job Success Rate", Tools.pct( (trans.job_success || 0) / (trans.job_complete || 1) ) ],
|
|
48
|
+
[ "Avg Job Elapsed", Tools.getTextFromSeconds( Math.round( (trans.job_elapsed || 0) / (trans.job_complete || 1) ), true, true ) ],
|
|
49
|
+
[ "Avg Job Output", Tools.getTextFromBytes( Math.round( (trans.job_log_file_size || 0) / (trans.job_complete || 1) ) ).replace(/bytes/, 'B') ],
|
|
50
|
+
[ "Job CPU Usage", Tools.pct( Math.round(total_cpu || 0), 100 ) ],
|
|
51
|
+
[ "Job Mem Usage", Tools.getTextFromBytes(total_mem, 1).replace(/bytes/, 'B') ],
|
|
52
|
+
[ "Uptime", Tools.getTextFromSeconds( stats.started ? (this.epoch - stats.started) : 0, true, true ) ],
|
|
53
|
+
[ "Scheduler", this.getNiceEnabled(this.state.scheduler.enabled) ],
|
|
54
|
+
[ "Server Time", this.getNiceTime(this.epoch, true) ]
|
|
55
|
+
],
|
|
56
|
+
{
|
|
57
|
+
minCols: 3,
|
|
58
|
+
maxCols: 6,
|
|
59
|
+
gap: 1,
|
|
60
|
+
indent: 1,
|
|
61
|
+
valueStyles: ["bold", "green"]
|
|
62
|
+
}) );
|
|
63
|
+
|
|
64
|
+
// active alerts
|
|
65
|
+
this.printActiveAlerts();
|
|
66
|
+
|
|
67
|
+
// active jobs
|
|
68
|
+
this.printActiveJobs({
|
|
69
|
+
rows: Object.values(this.activeJobs).sort( function(a, b) {
|
|
70
|
+
// keep workflow parent jobs directly above their sub-jobs
|
|
71
|
+
if (b.workflow && (b.workflow.job == a.id)) return -1;
|
|
72
|
+
if (a.workflow && (a.workflow.job == b.id)) return 1;
|
|
73
|
+
return (a.started < b.started) ? 1 : -1;
|
|
74
|
+
} )
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// internal jobs
|
|
78
|
+
this.printInternalJobs();
|
|
79
|
+
|
|
80
|
+
// queue summary
|
|
81
|
+
await this.printQueueSummary();
|
|
82
|
+
|
|
83
|
+
// rate limit summary
|
|
84
|
+
this.printRateSummary();
|
|
85
|
+
|
|
86
|
+
// aliases
|
|
87
|
+
switch (this.args.other[0]) {
|
|
88
|
+
case 'upcoming': this.args.upcoming = true; break;
|
|
89
|
+
// case 'completed': this.args.completed = true; break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// upcoming
|
|
93
|
+
if (this.args.upcoming) {
|
|
94
|
+
this.prepSearchArgs();
|
|
95
|
+
await this.printUpcomingJobs();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// completed jobs
|
|
99
|
+
// if (this.args.completed) {
|
|
100
|
+
// await this.printCompletedJobs();
|
|
101
|
+
// }
|
|
102
|
+
|
|
103
|
+
this.printSuggestedCommands({
|
|
104
|
+
"Upcoming jobs": this.args.upcoming ? "" : "xy upcoming",
|
|
105
|
+
"Completed jobs": "xy jobs",
|
|
106
|
+
"Event list": "xy events",
|
|
107
|
+
"Hide suggestions": "xy config --suggest false"
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async printQueueSummary() {
|
|
112
|
+
// Fetch all visible queued-job groups from the server.
|
|
113
|
+
cli.progress.start();
|
|
114
|
+
var { err, data } = await this.api.getQueueSummary();
|
|
115
|
+
if (err) this.die(err);
|
|
116
|
+
cli.progress.end();
|
|
117
|
+
|
|
118
|
+
this.queues = data.queues || [];
|
|
119
|
+
// if (!this.queues.length) return;
|
|
120
|
+
|
|
121
|
+
// Sort queues alphabetically by their effective queue or capacity ID.
|
|
122
|
+
this.queues.sort( function(a, b) {
|
|
123
|
+
return String(a.id).toLowerCase().localeCompare( String(b.id).toLowerCase() );
|
|
124
|
+
} );
|
|
125
|
+
|
|
126
|
+
var source_types = (this.config.ui?.job_source_types) || [];
|
|
127
|
+
|
|
128
|
+
var rows = this.queues.map( queue => {
|
|
129
|
+
var event_ids = Object.keys(queue.events || {}).sort();
|
|
130
|
+
var category_ids = Object.keys(queue.categories || {}).sort();
|
|
131
|
+
var source_ids = Object.keys(queue.sources || {}).sort();
|
|
132
|
+
var target_ids = Object.keys(queue.targets || {}).sort();
|
|
133
|
+
|
|
134
|
+
var nice_events = event_ids.length ?
|
|
135
|
+
event_ids.map( this.getNiceEvent.bind(this) ).join(', ') :
|
|
136
|
+
gray('(None)');
|
|
137
|
+
|
|
138
|
+
var nice_categories = category_ids.length ?
|
|
139
|
+
category_ids.map( this.getNiceCategory.bind(this) ).join(', ') :
|
|
140
|
+
gray('(None)');
|
|
141
|
+
|
|
142
|
+
var nice_sources = source_ids.length ? source_ids.map( function(source) {
|
|
143
|
+
var source_def = Tools.findObject( source_types, { id: source } );
|
|
144
|
+
return source_def ? source_def.title : Tools.ucfirst(source);
|
|
145
|
+
} ).join(', ') : gray('(None)');
|
|
146
|
+
|
|
147
|
+
var nice_targets = target_ids.length ? this.getNiceTargets(target_ids) : gray('(None)');
|
|
148
|
+
|
|
149
|
+
return [
|
|
150
|
+
this.color('theme').bold(queue.id),
|
|
151
|
+
nice_events,
|
|
152
|
+
nice_categories,
|
|
153
|
+
nice_sources,
|
|
154
|
+
nice_targets,
|
|
155
|
+
bold(Tools.commify(queue.count || 0))
|
|
156
|
+
];
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
this.printBoxTable({
|
|
160
|
+
title: 'Job Queues',
|
|
161
|
+
header: [
|
|
162
|
+
'Queue ID / Cap Key',
|
|
163
|
+
'Events',
|
|
164
|
+
'Categories',
|
|
165
|
+
'Sources',
|
|
166
|
+
'Targets',
|
|
167
|
+
'# Jobs'
|
|
168
|
+
],
|
|
169
|
+
rows: rows
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
printRateSummary() {
|
|
174
|
+
// Render all currently active job rate-limit windows.
|
|
175
|
+
var rates = Object.values(this.jobRateLimits || {});
|
|
176
|
+
// if (!rates.length) return;
|
|
177
|
+
|
|
178
|
+
// Sort pools alphabetically by their queue or shared-capacity ID.
|
|
179
|
+
rates.sort( function(a, b) {
|
|
180
|
+
return String(a.id).toLowerCase().localeCompare( String(b.id).toLowerCase() );
|
|
181
|
+
} );
|
|
182
|
+
|
|
183
|
+
var rows = rates.map( rate => {
|
|
184
|
+
var rate_id = String(rate.id || '');
|
|
185
|
+
var event_id = '';
|
|
186
|
+
var match = null;
|
|
187
|
+
|
|
188
|
+
// Normal event pool IDs begin with the event ID.
|
|
189
|
+
match = rate_id.match(/^(e\w+)/);
|
|
190
|
+
if (match) {
|
|
191
|
+
event_id = match[1];
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
// Workflow node pools contain the parent workflow job ID.
|
|
195
|
+
match = rate_id.match(/^n\w+\-(j\w+)/);
|
|
196
|
+
|
|
197
|
+
if (match) {
|
|
198
|
+
var workflow_job = this.activeJobs[match[1]];
|
|
199
|
+
if (workflow_job) {
|
|
200
|
+
event_id = workflow_job.event;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
var remaining = Math.max( 0, (rate.expires || 0) - this.epoch );
|
|
206
|
+
|
|
207
|
+
return [
|
|
208
|
+
this.color('theme').bold(rate_id),
|
|
209
|
+
event_id ? this.getNiceEvent(event_id) : gray('n/a'),
|
|
210
|
+
Tools.commify(rate.count || 0),
|
|
211
|
+
Tools.commify(rate.max || 0),
|
|
212
|
+
Tools.getTextFromSeconds( rate.window || 0, false, true ),
|
|
213
|
+
this.getTextFromSecondsRound( remaining, false )
|
|
214
|
+
];
|
|
215
|
+
} );
|
|
216
|
+
|
|
217
|
+
this.printBoxTable({
|
|
218
|
+
title: 'Rate Limit Pools',
|
|
219
|
+
header: [
|
|
220
|
+
'Pool ID / Cap Key',
|
|
221
|
+
'Event',
|
|
222
|
+
'Current Count',
|
|
223
|
+
'Max Count',
|
|
224
|
+
'Window Size',
|
|
225
|
+
'Expires In'
|
|
226
|
+
],
|
|
227
|
+
rows: rows
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async cmd_upcoming() {
|
|
232
|
+
// alias for `xy dashboard --upcoming`
|
|
233
|
+
this.args.upcoming = true;
|
|
234
|
+
await this.cmd_dashboard();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
};
|
package/lib/doc.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Documentation Commands
|
|
2
|
+
|
|
3
|
+
const cli = require('pixl-cli');
|
|
4
|
+
const Tools = cli.Tools;
|
|
5
|
+
const chalk = cli.chalk;
|
|
6
|
+
const gray = chalk.gray;
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
|
|
10
|
+
async cmd_doc() {
|
|
11
|
+
// Fetch one Markdown document, optionally narrowing it to one complete
|
|
12
|
+
// chapter before handing it to the shared terminal renderer.
|
|
13
|
+
var selection = this.consumeDocSelection();
|
|
14
|
+
|
|
15
|
+
cli.progress.start({ amount: 1, pct: false, text: gray('→ Loading documentation...') });
|
|
16
|
+
var { err, data } = await this.api.getDoc({ doc: selection.doc });
|
|
17
|
+
cli.progress.end();
|
|
18
|
+
if (err) this.die(err);
|
|
19
|
+
if (!data || (typeof(data.text) != 'string')) this.die('Documentation response did not include Markdown text.');
|
|
20
|
+
|
|
21
|
+
var text = data.text;
|
|
22
|
+
if (selection.chapter) text = this.extractDocChapter(text, selection.doc, selection.chapter);
|
|
23
|
+
text = this.prepareDocMarkdown(text, selection.doc);
|
|
24
|
+
println('\n' + this.markdown(text.trim()).trimEnd());
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
consumeDocSelection() {
|
|
28
|
+
// Accept either doc/chapter or two positional words. Keep both portions
|
|
29
|
+
// deliberately strict because the document name maps to a server-side file.
|
|
30
|
+
var values = this.args.other.slice(0);
|
|
31
|
+
delete this.args.other;
|
|
32
|
+
if (Tools.numKeys(this.args)) this.die('Unsupported documentation option: --' + Tools.firstKey(this.args));
|
|
33
|
+
if (values.length > 2) return this.dieUsage('doc');
|
|
34
|
+
|
|
35
|
+
var doc = values.length ? String(values.shift()) : 'index';
|
|
36
|
+
var chapter = values.length ? String(values.shift()) : '';
|
|
37
|
+
|
|
38
|
+
if (doc.includes('/')) {
|
|
39
|
+
if (chapter) return this.dieUsage('doc');
|
|
40
|
+
var parts = doc.split('/');
|
|
41
|
+
if ((parts.length != 2) || !parts[0] || !parts[1]) return this.dieUsage('doc');
|
|
42
|
+
doc = parts[0];
|
|
43
|
+
chapter = parts[1];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!doc.match(/^\w+$/)) this.die('Invalid documentation name: ' + doc);
|
|
47
|
+
if (doc.toLowerCase() == 'search') this.die('Documentation search is not supported by this command.');
|
|
48
|
+
if (chapter && !chapter.match(/^[a-z0-9_-]+$/i)) this.die('Invalid documentation chapter slug: ' + chapter);
|
|
49
|
+
|
|
50
|
+
return { doc: doc.toLowerCase(), chapter: chapter.toLowerCase() };
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
extractDocChapter(text, doc, chapter) {
|
|
54
|
+
// Include the selected heading and all of its descendants, stopping only
|
|
55
|
+
// when the next heading is at the same or a higher document level.
|
|
56
|
+
var lines = String(text).split(/\r?\n/);
|
|
57
|
+
var start = -1;
|
|
58
|
+
var end = lines.length;
|
|
59
|
+
var level = 0;
|
|
60
|
+
var fence = '';
|
|
61
|
+
var slugs = [];
|
|
62
|
+
|
|
63
|
+
for (var idx = 0; idx < lines.length; idx++) {
|
|
64
|
+
var fence_match = lines[idx].match(/^\s*(`{3,}|~{3,})/);
|
|
65
|
+
if (fence_match) {
|
|
66
|
+
var marker = fence_match[1][0];
|
|
67
|
+
if (!fence) fence = marker;
|
|
68
|
+
else if (fence == marker) fence = '';
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (fence) continue;
|
|
72
|
+
|
|
73
|
+
var heading = lines[idx].match(/^(#{1,6})[ \t]+(.+?)\s*$/);
|
|
74
|
+
if (!heading) continue;
|
|
75
|
+
var title = heading[2].replace(/\s+#+\s*$/, '').trim();
|
|
76
|
+
var slug = this.getDocHeadingSlug(title);
|
|
77
|
+
slugs.push(slug);
|
|
78
|
+
|
|
79
|
+
if (start < 0) {
|
|
80
|
+
if (slug != chapter) continue;
|
|
81
|
+
start = idx;
|
|
82
|
+
level = heading[1].length;
|
|
83
|
+
}
|
|
84
|
+
else if (heading[1].length <= level) {
|
|
85
|
+
end = idx;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (start < 0) {
|
|
91
|
+
var suggestion = this.findClosestString(chapter, slugs);
|
|
92
|
+
var message = 'Could not find documentation chapter: ' + doc + '/' + chapter + '.';
|
|
93
|
+
if (suggestion) message += ' Did you mean "' + doc + '/' + suggestion + '"?';
|
|
94
|
+
this.die(message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return lines.slice(start, end).join('\n').trim();
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
prepareDocMarkdown(text, doc) {
|
|
101
|
+
// Relative documentation links have no useful destination in a terminal.
|
|
102
|
+
// Present them as equivalent CLI commands while preserving web links and
|
|
103
|
+
// leaving example source code completely untouched.
|
|
104
|
+
var fence = '';
|
|
105
|
+
return String(text).split(/\r?\n/).map( function(line) {
|
|
106
|
+
var fence_match = line.match(/^\s*(`{3,}|~{3,})/);
|
|
107
|
+
if (fence_match) {
|
|
108
|
+
var marker = fence_match[1][0];
|
|
109
|
+
if (!fence) fence = marker;
|
|
110
|
+
else if (fence == marker) fence = '';
|
|
111
|
+
return line;
|
|
112
|
+
}
|
|
113
|
+
if (fence) return line;
|
|
114
|
+
|
|
115
|
+
return line.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, function(all, label, target, offset, source) {
|
|
116
|
+
if (source[offset - 1] == '!') return all;
|
|
117
|
+
var match = target.match(/^(?:(\w+)\.md)?(?:#([a-z0-9_-]+))?$/i);
|
|
118
|
+
if (!match || (!match[1] && !match[2])) return all;
|
|
119
|
+
var target_doc = (match[1] || doc).toLowerCase();
|
|
120
|
+
var command = 'xy doc ' + target_doc + (match[2] ? '/' + match[2].toLowerCase() : '');
|
|
121
|
+
return label + ' (`' + command + '`)';
|
|
122
|
+
});
|
|
123
|
+
}).join('\n');
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
getDocHeadingSlug(title) {
|
|
127
|
+
// Match the anchor IDs generated by the xyOps documentation page.
|
|
128
|
+
return String(title).trim().replace(/\W+/g, '-').toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
}; // module.exports
|