]> git.wincent.com - wincent.git/blob - src/main.ts
2522e3bf4cf0f30c88d8a683f5d2ecef592325bf
[wincent.git] / src / main.ts
1 import * as os from 'os';
2 import * as path from 'path';
3
4 import ErrorWithMetadata from './ErrorWithMetadata';
5 import Context from './Fig/Context';
6 import {root} from './Fig';
7 import {debug, log, setLogLevel} from './console';
8 import getOptions from './getOptions';
9 import merge from './merge';
10 import prompt from './prompt';
11 import readAspect from './readAspect';
12 import readProject from './readProject';
13 import regExpFromString from './regExpFromString';
14 import simplify from './simplify';
15 import stringify from './stringify';
16 import test from './test';
17
18 async function main() {
19     if (Context.attributes.uid === 0) {
20         throw new ErrorWithMetadata('Cannot run as root');
21     }
22
23     // Skip first two args (node executable and main.js script).
24     const options = await getOptions(process.argv.slice(2));
25
26     setLogLevel(options.logLevel);
27
28     debug(() => {
29         log.debug('process.argv:\n\n' + stringify(process.argv) + '\n');
30         log.debug('getOptions():\n\n' + stringify(options) + '\n');
31     });
32
33     if (process.cwd() === root) {
34         log.info(`Working from root: ${simplify(root)}`);
35     } else {
36         log.notice(`Changing to root: ${simplify(root)}`);
37         process.chdir(root);
38     }
39
40     log.info('Running tests');
41
42     await test();
43
44     if (options.testsOnly) {
45         return;
46     }
47
48     const project = await readProject(path.join(root, 'project.json'));
49
50     const hostname = os.hostname();
51
52     const profiles = project.profiles ?? {};
53
54     const [profile] =
55         Object.entries(profiles).find(([, {pattern}]) => {
56             if (regExpFromString(pattern).test(hostname)) {
57                 return true;
58             }
59         }) || [];
60
61     log.info(`Profile: ${profile || 'n/a'}`);
62
63     const profileVariables: {[key: string]: JSONValue} = profile
64         ? profiles[profile]!.variables ?? {}
65         : {};
66
67     const platform = Context.attributes.platform;
68
69     log.info(`Platform: ${platform}`);
70
71     const {aspects, variables: platformVariables = {}} = project.platforms[
72         platform
73     ];
74
75     // Register tasks.
76     const candidateTasks = [];
77
78     for (const aspect of aspects) {
79         switch (aspect) {
80             case 'launchd':
81                 require('../aspects/launchd');
82                 break;
83             case 'terminfo':
84                 require('../aspects/terminfo');
85                 break;
86         }
87
88         // Check for an exact match of the starting task if `--start-at-task=` was
89         // supplied.
90         for (const [, name] of Context.tasks.get(aspect)) {
91             if (name === options.startAt.literal) {
92                 options.startAt.found = true;
93             } else if (
94                 !options.startAt.found &&
95                 options.startAt.fuzzy &&
96                 options.startAt.fuzzy.test(name)
97             ) {
98                 candidateTasks.push(name);
99             }
100         }
101     }
102
103     if (!options.startAt.found && candidateTasks.length === 1) {
104         log.notice(`Matching task found: ${candidateTasks[0]}`);
105
106         log();
107
108         const reply = await prompt('Start running at this task? [y/n]: ');
109
110         if (/^\s*y(?:e(?:s)?)?\s*$/i.test(reply)) {
111             options.startAt.found = true;
112             options.startAt.literal = candidateTasks[0];
113         } else {
114             throw new ErrorWithMetadata('User aborted');
115         }
116     } else if (!options.startAt.found && candidateTasks.length > 1) {
117         log.notice(`${candidateTasks.length} tasks found:\n`);
118
119         const width = candidateTasks.length.toString().length;
120
121         while (!options.startAt.found) {
122             candidateTasks.forEach((name, i) => {
123                 log(`${(i + 1).toString().padStart(width)}: ${name}`);
124             });
125
126             log();
127
128             const reply = await prompt('Start at task number: ');
129
130             const choice = parseInt(reply.trim(), 10);
131
132             if (
133                 Number.isNaN(choice) ||
134                 choice < 1 ||
135                 choice > candidateTasks.length
136             ) {
137                 log.warn(
138                     `Invalid choice ${JSON.stringify(
139                         reply
140                     )}; try again or press CTRL-C to abort.`
141                 );
142
143                 log();
144             } else {
145                 options.startAt.found = true;
146                 options.startAt.literal = candidateTasks[choice - 1];
147             }
148         }
149     }
150
151     const baseVariables = merge(profileVariables, platformVariables);
152
153     // Execute tasks.
154     try {
155         for (const aspect of aspects) {
156             const {variables: aspectVariables = {}} = await readAspect(
157                 path.join(root, 'aspects', aspect, 'aspect.json')
158             );
159
160             if (options.focused.size && !options.focused.has(aspect)) {
161                 log.info(`Skipping aspect: ${aspect}`);
162                 continue;
163             }
164
165             const variables = merge(aspectVariables, baseVariables);
166
167             // log.debug(`variables:\n\n${JSON.stringify(variables, null, 2)}\n`);
168
169             for (const [callback, name] of Context.tasks.get(aspect)) {
170                 if (
171                     !options.startAt.found ||
172                     name === options.startAt.literal
173                 ) {
174                     options.startAt.found = false;
175                     log.info(`Task: ${name}`);
176
177                     await Context.withContext({aspect, variables}, async () => {
178                         await callback();
179                     });
180                 }
181             }
182         }
183     } finally {
184         const counts = Object.entries(Context.counts)
185             .map(([name, count]) => {
186                 return `${name}=${count}`;
187             })
188             .join(' ');
189
190         log.info(`Summary: ${counts}`);
191     }
192 }
193
194 main().catch((error) => {
195     if (error instanceof ErrorWithMetadata) {
196         if (error.metadata) {
197             log.error(
198                 `${error.message}\n\n${JSON.stringify(
199                     error.metadata,
200                     null,
201                     2
202                 )}\n`
203             );
204         } else {
205             log.error(error.message);
206         }
207     } else {
208         log.error(error.toString());
209     }
210
211     process.exit(1);
212 });