@axinom/mosaic-cli 0.14.2-rc.8 → 0.15.0-rc.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/package.json +6 -5
- package/src/cli/README.md +60 -0
- package/src/cli/index.ts +47 -0
- package/src/commands/apply-templates/apply-templates.spec.ts +623 -0
- package/src/commands/apply-templates/apply-templates.ts +494 -0
- package/src/commands/apply-templates/bitwarden-vault.ts +130 -0
- package/src/commands/apply-templates/index.ts +1 -0
- package/src/commands/create-extension-config/create-extension-config.ts +92 -0
- package/src/commands/create-extension-config/index.ts +23 -0
- package/src/commands/get-access-token/get-access-token-options.ts +9 -0
- package/src/commands/get-access-token/get-dev-access-token.ts +32 -0
- package/src/commands/get-access-token/index.ts +66 -0
- package/src/commands/graphql-diff.ts +143 -0
- package/src/commands/msg-codegen/codegen.ts +891 -0
- package/src/commands/msg-codegen/index.ts +48 -0
- package/src/commands/msg-codegen/lint.ts +84 -0
- package/src/commands/msg-codegen/message-codegen-options.ts +7 -0
- package/src/commands/msg-diff/asyncapi-override.ts +31 -0
- package/src/commands/msg-diff/git-checkout-tmp.ts +73 -0
- package/src/commands/msg-diff/index.ts +53 -0
- package/src/commands/msg-diff/message-diff-options.ts +7 -0
- package/src/commands/msg-diff/msg-diff.spec.ts +412 -0
- package/src/commands/msg-diff/msg-diff.ts +364 -0
- package/src/commands/msg-diff/test-resources/0/1-asyncapi.yml +38 -0
- package/src/commands/msg-diff/test-resources/0/2-asyncapi.yml +36 -0
- package/src/commands/msg-diff/test-resources/0/command.json +74 -0
- package/src/commands/msg-diff/test-resources/0/event.json +25 -0
- package/src/commands/msg-diff/test-resources/1/1-asyncapi.yml +25 -0
- package/src/commands/msg-diff/test-resources/1/moved-event.json +25 -0
- package/src/commands/msg-diff/test-resources/common.json +20 -0
- package/src/commands/pg-dump/README.md +21 -0
- package/src/commands/pg-dump/generate.ts +146 -0
- package/src/commands/pg-dump/index.ts +39 -0
- package/src/commands/pg-dump/pg-dump-options.ts +6 -0
- package/src/commands/publish-schema-to-db/README.md +130 -0
- package/src/commands/publish-schema-to-db/abstractions/base-smart-tags.ts +6 -0
- package/src/commands/publish-schema-to-db/abstractions/index.ts +5 -0
- package/src/commands/publish-schema-to-db/abstractions/pg-column.ts +31 -0
- package/src/commands/publish-schema-to-db/abstractions/pg-fk-column.ts +6 -0
- package/src/commands/publish-schema-to-db/abstractions/pg-table.ts +55 -0
- package/src/commands/publish-schema-to-db/abstractions/pg-type.ts +8 -0
- package/src/commands/publish-schema-to-db/content-entity-model.ts +93 -0
- package/src/commands/publish-schema-to-db/generate.ts +82 -0
- package/src/commands/publish-schema-to-db/index.ts +49 -0
- package/src/commands/publish-schema-to-db/jest.config.js +9 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/fk-column.spec.ts +42 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/fk-column.ts +41 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/index.ts +4 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/pk-column.spec.ts +47 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/pk-column.ts +34 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/primitive-column.spec.ts +65 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/primitive-column.ts +62 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/virtual-fk-column.spec.ts +24 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/virtual-fk-column.ts +34 -0
- package/src/commands/publish-schema-to-db/pg-models/json-schema-parse-utils.spec.ts +182 -0
- package/src/commands/publish-schema-to-db/pg-models/json-schema-parse-utils.ts +166 -0
- package/src/commands/publish-schema-to-db/pg-models/pg-sql-gen-utils.spec.ts +19 -0
- package/src/commands/publish-schema-to-db/pg-models/pg-sql-gen-utils.ts +237 -0
- package/src/commands/publish-schema-to-db/pg-models/pgl-utils.spec.ts +19 -0
- package/src/commands/publish-schema-to-db/pg-models/pgl-utils.ts +115 -0
- package/src/commands/publish-schema-to-db/pg-models/tables/content-entity-table.ts +104 -0
- package/src/commands/publish-schema-to-db/pg-models/tables/index.ts +3 -0
- package/src/commands/publish-schema-to-db/pg-models/tables/object-property-table.ts +113 -0
- package/src/commands/publish-schema-to-db/pg-models/tables/relations-table.ts +115 -0
- package/src/commands/publish-schema-to-db/postprocessors/collection-postprocessor.ts +33 -0
- package/src/commands/publish-schema-to-db/postprocessors/content-entity-model-postprocessor.ts +13 -0
- package/src/commands/publish-schema-to-db/postprocessors/episode-postprocessor.ts +37 -0
- package/src/commands/publish-schema-to-db/postprocessors/index.ts +6 -0
- package/src/commands/publish-schema-to-db/postprocessors/movie-postprocessor.ts +30 -0
- package/src/commands/publish-schema-to-db/postprocessors/postprocessing-utils.ts +21 -0
- package/src/commands/publish-schema-to-db/postprocessors/season-postprocessor.ts +37 -0
- package/src/commands/publish-schema-to-db/postprocessors/tvshow-postprocessor.ts +30 -0
- package/src/commands/publish-schema-to-db/publish-schema-to-db-options.ts +15 -0
- package/src/commands/publish-schema-to-db/types/sql-formatter.d.ts +10 -0
- package/src/exports.ts +2 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import { Chalk, cyanBright, gray, green, red, white, yellow } from 'chalk';
|
|
4
|
+
import * as Diff from 'diff';
|
|
5
|
+
import * as envfile from 'envfile';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import * as glob from 'glob';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as readline from 'readline';
|
|
11
|
+
import * as yargs from 'yargs';
|
|
12
|
+
import { CommandModule } from 'yargs';
|
|
13
|
+
import { BitwardenVault, Vault } from './bitwarden-vault';
|
|
14
|
+
|
|
15
|
+
export interface ApplyTemplatesOptions {
|
|
16
|
+
only: string;
|
|
17
|
+
yes: boolean;
|
|
18
|
+
no: boolean;
|
|
19
|
+
verbose: boolean;
|
|
20
|
+
replace: boolean;
|
|
21
|
+
bitwardenUrl: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DiffResult {
|
|
25
|
+
result: 'created' | 'same' | 'different' | 'differentLayout';
|
|
26
|
+
diff: Diff.Change[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const applyTemplates: CommandModule<unknown, ApplyTemplatesOptions> = {
|
|
30
|
+
command: 'apply-templates',
|
|
31
|
+
describe:
|
|
32
|
+
'Creates a local copy of each *.template file. ' +
|
|
33
|
+
'A diff and confirmation prompt will be displayed before any changes are made.\n' +
|
|
34
|
+
'\n' +
|
|
35
|
+
'Env (.env) files have special treatment... the value for each key defined in the target file will be kept in these cases:\n' +
|
|
36
|
+
' a. A value for the key is not defined, or is blank, in the template\n' +
|
|
37
|
+
' b. The value in the template looks like <a_{placeholder}>\n' +
|
|
38
|
+
' c. The line from the template is included in the .env file and commented out\n' +
|
|
39
|
+
'\n' +
|
|
40
|
+
'When a .env file is created from a template, an extra line will be prefixed in the form "#@TEMPLATE = {template-file}". ' +
|
|
41
|
+
'This directive will then be used to match the output file to the original template even after renaming the output file. ' +
|
|
42
|
+
'This simplifies management of .env files when multiple template files are provided for alternative environments.',
|
|
43
|
+
builder: (yargs) =>
|
|
44
|
+
yargs
|
|
45
|
+
.option('only', {
|
|
46
|
+
alias: 'o',
|
|
47
|
+
describe:
|
|
48
|
+
'A glob where files should be searched within.\n(Note: the script will attach "/*.template" to the given value)',
|
|
49
|
+
string: true,
|
|
50
|
+
default: '**',
|
|
51
|
+
})
|
|
52
|
+
.option('yes', {
|
|
53
|
+
alias: ['y'],
|
|
54
|
+
describe:
|
|
55
|
+
'The script will overwrite files without prompting for user confirmation.',
|
|
56
|
+
default: false,
|
|
57
|
+
boolean: true,
|
|
58
|
+
})
|
|
59
|
+
.option('no', {
|
|
60
|
+
alias: 'n',
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
describe: 'The script will not overwrite any files.',
|
|
63
|
+
default: false,
|
|
64
|
+
boolean: true,
|
|
65
|
+
})
|
|
66
|
+
.option('verbose', {
|
|
67
|
+
alias: 'v',
|
|
68
|
+
type: 'boolean',
|
|
69
|
+
describe: 'List all files and show all diff lines.',
|
|
70
|
+
default: false,
|
|
71
|
+
boolean: true,
|
|
72
|
+
})
|
|
73
|
+
.option('replace', {
|
|
74
|
+
type: 'boolean',
|
|
75
|
+
describe:
|
|
76
|
+
'Replace all files reverting to template values. WARNING: this is destructive!',
|
|
77
|
+
default: false,
|
|
78
|
+
boolean: true,
|
|
79
|
+
})
|
|
80
|
+
.option('bitwardenUrl', {
|
|
81
|
+
type: 'string',
|
|
82
|
+
describe:
|
|
83
|
+
'URL of a Bitwarden server to use as a source to resolve placeholders in the template. ' +
|
|
84
|
+
'Bitwarden CLI must be installed as a dev dependency, and the vault unlocked (see: https://bitwarden.com/help/cli/). ' +
|
|
85
|
+
"Placeholders must be in the form: <BITWARDEN_{itemName::fieldName}> where fieldname can be 'username', 'password', or the name of a custom field.",
|
|
86
|
+
default: undefined,
|
|
87
|
+
hidden: true,
|
|
88
|
+
}),
|
|
89
|
+
handler: compareTemplates,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Represents a mapping between a config template and target file.
|
|
94
|
+
*/
|
|
95
|
+
export class ConfigTransform {
|
|
96
|
+
constructor(
|
|
97
|
+
public readonly template: string,
|
|
98
|
+
public readonly target: string,
|
|
99
|
+
) {
|
|
100
|
+
this.targetExists = existsSync(this.target);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
readonly targetExists: boolean;
|
|
104
|
+
|
|
105
|
+
compare(_: boolean, __: boolean): DiffResult {
|
|
106
|
+
const templateContent = readFileSync(this.template).toString();
|
|
107
|
+
const targetContent = readFileSync(this.target).toString();
|
|
108
|
+
const diff = Diff.diffLines(targetContent, templateContent);
|
|
109
|
+
const changes = diff.filter((d) => d.added || d.removed);
|
|
110
|
+
return {
|
|
111
|
+
result: changes.length ? 'different' : 'same',
|
|
112
|
+
diff,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
writeTarget(): void {
|
|
117
|
+
copyFileSync(this.template, this.target);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Represents a mapping between a .env config template and target file.
|
|
123
|
+
*/
|
|
124
|
+
export class EnvConfigTransform extends ConfigTransform {
|
|
125
|
+
constructor(template: string, target: string, bitwardenVault?: Vault) {
|
|
126
|
+
super(template, target);
|
|
127
|
+
this.bitwardenVault = bitwardenVault;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
readonly bitwardenVault?: Vault;
|
|
131
|
+
private newContent?: string;
|
|
132
|
+
|
|
133
|
+
private readTemplate(): string {
|
|
134
|
+
const content = readFileSync(this.template).toString();
|
|
135
|
+
return this.bitwardenVault === undefined
|
|
136
|
+
? content
|
|
137
|
+
: content.replace(
|
|
138
|
+
/<BITWARDEN_\{([^}]*)\}[^>]*>/g,
|
|
139
|
+
(match: string, identifier: string) => {
|
|
140
|
+
if (!identifier.includes('::')) {
|
|
141
|
+
console.warn(
|
|
142
|
+
yellow(
|
|
143
|
+
`WARNING: Bitwarden placeholder in unsupported format: ${match} in ${this.template}.`,
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
return match;
|
|
147
|
+
}
|
|
148
|
+
const [name, fieldName] = identifier.split('::', 2);
|
|
149
|
+
const value = this.bitwardenVault?.getValue(name, fieldName);
|
|
150
|
+
if (value === undefined && !this.bitwardenVault?.error) {
|
|
151
|
+
console.warn(
|
|
152
|
+
yellow(
|
|
153
|
+
`WARNING: A value was not found in Bitwarden for placeholder ${match} in ${this.template}.`,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return value ?? match;
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private mergeContent(templateContent: string, targetContent: string): string {
|
|
163
|
+
const vars = envfile.parse(templateContent);
|
|
164
|
+
const targetVars = envfile.parse(targetContent);
|
|
165
|
+
let mergedContent = templateContent;
|
|
166
|
+
let additionalContent = '';
|
|
167
|
+
for (const [k, v] of Object.entries(targetVars)) {
|
|
168
|
+
// if key is not in template, then append target value to merged content
|
|
169
|
+
if (!(k in vars)) {
|
|
170
|
+
additionalContent += `\n${envfile.stringify({ [k]: v })}`;
|
|
171
|
+
} else if (vars[k] !== v) {
|
|
172
|
+
const commentRegex = new RegExp(`(?<=(\n|^))#${k}=`);
|
|
173
|
+
const commented = targetContent.match(commentRegex);
|
|
174
|
+
// use target value if template value looks like a placeholder
|
|
175
|
+
// use target value if key is also included as a comment
|
|
176
|
+
if (isPlaceholder(vars[k]) || vars[k] === '' || commented) {
|
|
177
|
+
// keep commented template value (updated with current template value)
|
|
178
|
+
const commentPrefix =
|
|
179
|
+
commented && !mergedContent.match(commentRegex)
|
|
180
|
+
? '#' + envfile.stringify({ [k]: vars[k] })
|
|
181
|
+
: '';
|
|
182
|
+
mergedContent = mergedContent.replace(
|
|
183
|
+
new RegExp(`(?<=(\n|^))${k}=.*?(\r?\n|$)`),
|
|
184
|
+
commentPrefix + envfile.stringify({ [k]: vars[k], [k]: v }),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (additionalContent !== '') {
|
|
190
|
+
mergedContent += `\n\n# Additional keys (not in template)${additionalContent}`;
|
|
191
|
+
}
|
|
192
|
+
return mergedContent;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
compare(replace: boolean, verbose: boolean): DiffResult {
|
|
196
|
+
const templateContent = this.readTemplate();
|
|
197
|
+
const targetContent = readFileSync(this.target).toString();
|
|
198
|
+
this.newContent = replace
|
|
199
|
+
? templateContent
|
|
200
|
+
: this.mergeContent(templateContent, targetContent);
|
|
201
|
+
|
|
202
|
+
// set the #@TARGET prefix line
|
|
203
|
+
this.newContent = setDefinedTemplate(
|
|
204
|
+
this.newContent,
|
|
205
|
+
path.basename(this.template),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const changed = this.newContent !== targetContent;
|
|
209
|
+
let diff = [] as Diff.Change[];
|
|
210
|
+
if (changed) {
|
|
211
|
+
// diff of sorted variable def lines
|
|
212
|
+
diff = Diff.diffLines(
|
|
213
|
+
envfile.stringify(orderByKey(envfile.parse(targetContent))),
|
|
214
|
+
envfile.stringify(orderByKey(envfile.parse(this.newContent))),
|
|
215
|
+
).filter((d) => d.added || d.removed);
|
|
216
|
+
}
|
|
217
|
+
const result = !changed
|
|
218
|
+
? 'same'
|
|
219
|
+
: diff.length > 0
|
|
220
|
+
? 'different'
|
|
221
|
+
: 'differentLayout';
|
|
222
|
+
if (result !== 'same' && verbose) {
|
|
223
|
+
// full diff of all lines
|
|
224
|
+
diff = Diff.diffLines(targetContent, this.newContent);
|
|
225
|
+
}
|
|
226
|
+
return { result, diff };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
writeTarget(): void {
|
|
230
|
+
writeFileSync(
|
|
231
|
+
this.target,
|
|
232
|
+
this.newContent ??
|
|
233
|
+
// set defined template on newly created files:
|
|
234
|
+
setDefinedTemplate(this.readTemplate(), this.template),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/*
|
|
240
|
+
* Use glob to iterate matching files.
|
|
241
|
+
*/
|
|
242
|
+
function compareTemplates(options: ApplyTemplatesOptions): void {
|
|
243
|
+
interface Results {
|
|
244
|
+
created: ConfigTransform[];
|
|
245
|
+
same: ConfigTransform[];
|
|
246
|
+
different: ConfigTransform[];
|
|
247
|
+
differentLayout: ConfigTransform[];
|
|
248
|
+
}
|
|
249
|
+
const results: Results = {
|
|
250
|
+
created: [],
|
|
251
|
+
same: [],
|
|
252
|
+
different: [],
|
|
253
|
+
differentLayout: [],
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const bitwardenVault =
|
|
257
|
+
options.bitwardenUrl !== undefined
|
|
258
|
+
? new BitwardenVault(options.bitwardenUrl)
|
|
259
|
+
: undefined;
|
|
260
|
+
|
|
261
|
+
const templatesPath = `${options.only}/*.template`;
|
|
262
|
+
glob(
|
|
263
|
+
templatesPath,
|
|
264
|
+
{ dot: true, ignore: '**/node_modules/**' },
|
|
265
|
+
(err, templates) => {
|
|
266
|
+
if (err) {
|
|
267
|
+
console.log(err);
|
|
268
|
+
process.exit(-1);
|
|
269
|
+
}
|
|
270
|
+
for (const template of templates) {
|
|
271
|
+
const isEnv = isEnvFile(path.basename(template));
|
|
272
|
+
const transforms = isEnv
|
|
273
|
+
? getEnvConfigTransforms(template).map(
|
|
274
|
+
(target) =>
|
|
275
|
+
new EnvConfigTransform(template, target, bitwardenVault),
|
|
276
|
+
)
|
|
277
|
+
: [new ConfigTransform(template, defaultTargetFor(template))];
|
|
278
|
+
for (const transform of transforms) {
|
|
279
|
+
const resultKey = transformComparison(
|
|
280
|
+
transform,
|
|
281
|
+
options.replace,
|
|
282
|
+
options.verbose,
|
|
283
|
+
);
|
|
284
|
+
results[resultKey].push(transform);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const total =
|
|
288
|
+
results.created.length +
|
|
289
|
+
results.same.length +
|
|
290
|
+
results.different.length +
|
|
291
|
+
results.differentLayout.length;
|
|
292
|
+
console.log(
|
|
293
|
+
`Found ${total} template file${total !== 1 ? 's' : ''}. ${
|
|
294
|
+
results.created.length
|
|
295
|
+
} target file${
|
|
296
|
+
results.created.length !== 1 ? 's were' : ' was'
|
|
297
|
+
} created. ${
|
|
298
|
+
results.different.length + results.differentLayout.length
|
|
299
|
+
} target file${
|
|
300
|
+
results.different.length + results.differentLayout.length !== 1
|
|
301
|
+
? 's have'
|
|
302
|
+
: ' has'
|
|
303
|
+
} changes`,
|
|
304
|
+
);
|
|
305
|
+
if (bitwardenVault?.error !== undefined) {
|
|
306
|
+
console.warn(
|
|
307
|
+
yellow(
|
|
308
|
+
`WARNING: Bitwarden placeholders were not resolved. ${bitwardenVault.error}`,
|
|
309
|
+
),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (results.different.length + results.differentLayout.length !== 0) {
|
|
313
|
+
promptChanges();
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
function promptChanges(): void {
|
|
319
|
+
if (options.yes) {
|
|
320
|
+
writeOutputFiles();
|
|
321
|
+
return;
|
|
322
|
+
} else if (options.no) {
|
|
323
|
+
console.log('Changes not applied.');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const rl = readline.createInterface({
|
|
327
|
+
input: process.stdin,
|
|
328
|
+
output: process.stdout,
|
|
329
|
+
});
|
|
330
|
+
console.log(
|
|
331
|
+
yellow(
|
|
332
|
+
"NOTE: To prevent changes to .env files from being overwritten, don't remove or replace the original line but comment it out.",
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
rl.question(
|
|
336
|
+
cyanBright(`Do you want to apply the changes above? (y/N)\n`),
|
|
337
|
+
(answer) => {
|
|
338
|
+
if (answer === 'y') {
|
|
339
|
+
writeOutputFiles();
|
|
340
|
+
} else {
|
|
341
|
+
console.log('Changes not applied.');
|
|
342
|
+
}
|
|
343
|
+
rl.close();
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function writeOutputFiles(): void {
|
|
349
|
+
const transforms = [...results.different, ...results.differentLayout];
|
|
350
|
+
for (const transform of transforms) {
|
|
351
|
+
console.log(`Applying ${transform.template} => ${transform.target}`);
|
|
352
|
+
transform.writeTarget();
|
|
353
|
+
}
|
|
354
|
+
console.log(
|
|
355
|
+
`Changes applied to ${transforms.length} file${
|
|
356
|
+
transforms.length !== 1 ? 's' : ''
|
|
357
|
+
}!`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function setDefinedTemplate(targetText: string, templateName: string): string {
|
|
363
|
+
return `#@TEMPLATE = ${path.basename(templateName)}\n` + targetText;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getDefinedTemplate(targetText: string): string | undefined {
|
|
367
|
+
const match = targetText.match(/^#\s*@TEMPLATE\s*=(.+)/);
|
|
368
|
+
return match ? match[1].trim() : undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function getEnvConfigTransforms(template: string): string[] {
|
|
372
|
+
const results: string[] = [];
|
|
373
|
+
const defaultTarget = defaultTargetFor(template);
|
|
374
|
+
const dir = path.posix.dirname(defaultTarget);
|
|
375
|
+
const defaultTargetName = path.posix.basename(defaultTarget);
|
|
376
|
+
const templateName = path.posix.basename(template);
|
|
377
|
+
// check all files in the same directory
|
|
378
|
+
for (const fileName of fs.readdirSync(dir)) {
|
|
379
|
+
// target must include '.env' and not end with '.template'
|
|
380
|
+
if (isEnvFile(fileName) && !fileName.endsWith('.template')) {
|
|
381
|
+
const file = path.posix.join(dir, fileName);
|
|
382
|
+
const fileText = readFileSync(file).toString();
|
|
383
|
+
const definedTemplate = getDefinedTemplate(fileText);
|
|
384
|
+
// file is a valid target if: it defines this template in a directive,
|
|
385
|
+
// or: its the default target and it has no template directive
|
|
386
|
+
if (
|
|
387
|
+
definedTemplate === templateName ||
|
|
388
|
+
(!definedTemplate && fileName === defaultTargetName)
|
|
389
|
+
) {
|
|
390
|
+
results.push(file);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// if no targets were found and the default target doesn't exist then create it
|
|
395
|
+
if (results.length === 0 && !fs.existsSync(defaultTarget)) {
|
|
396
|
+
return [defaultTarget];
|
|
397
|
+
}
|
|
398
|
+
return results;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/*
|
|
402
|
+
* Compare a single template and target.
|
|
403
|
+
* If target file doesn't exist then create it.
|
|
404
|
+
* If both exist and there are differences, return 'different' and do nothing.
|
|
405
|
+
*/
|
|
406
|
+
export function transformComparison(
|
|
407
|
+
transform: ConfigTransform,
|
|
408
|
+
replace: boolean,
|
|
409
|
+
verbose: boolean,
|
|
410
|
+
): DiffResult['result'] {
|
|
411
|
+
if (!transform.targetExists) {
|
|
412
|
+
transform.writeTarget();
|
|
413
|
+
console.log(
|
|
414
|
+
`Target file ${transform.target} did not exist and has been created.`,
|
|
415
|
+
);
|
|
416
|
+
return 'created';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const { result, diff } = transform.compare(replace, verbose);
|
|
420
|
+
if (result === 'same') {
|
|
421
|
+
if (verbose) {
|
|
422
|
+
console.log(gray(`File ${transform.target} has no changes.`));
|
|
423
|
+
}
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
console.log(
|
|
427
|
+
cyanBright(
|
|
428
|
+
`File ${transform.target} has changes` +
|
|
429
|
+
(transform.target !== defaultTargetFor(transform.template)
|
|
430
|
+
? `. Using template ${path.basename(transform.template)}`
|
|
431
|
+
: '') +
|
|
432
|
+
`${verbose ? '' : ' (-v to show full diff)'}:`,
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
if (result === 'differentLayout' && !verbose) {
|
|
437
|
+
console.log(
|
|
438
|
+
gray(` (Changes to whitespace, comments and line-order only)`),
|
|
439
|
+
);
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
logDiffLines(diff);
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function logDiffLines(diff: Diff.Change[]): void {
|
|
448
|
+
for (const part of diff) {
|
|
449
|
+
let value = part.value;
|
|
450
|
+
// last line without newline causes formatting issues
|
|
451
|
+
if (value !== '' && !value.endsWith('\n')) {
|
|
452
|
+
value += '\n';
|
|
453
|
+
}
|
|
454
|
+
const color: Chalk = part.added ? green : part.removed ? red : white;
|
|
455
|
+
const prefix = part.added ? '+ ' : part.removed ? '- ' : ' ';
|
|
456
|
+
value = value.replace(/(.*)\n/g, `${prefix}$1\n`);
|
|
457
|
+
process.stdout.write(color(value));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function defaultTargetFor(template: string): string {
|
|
462
|
+
return template.slice(0, -9); // remove ".template"
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/*
|
|
466
|
+
* Determine if the file is a .env file. Matches globs *.env.* & *.env
|
|
467
|
+
*/
|
|
468
|
+
function isEnvFile(file: string): boolean {
|
|
469
|
+
return file.match(/\.env\b/) ? true : false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/*
|
|
473
|
+
* Determine if a value looks like a placeholder
|
|
474
|
+
*/
|
|
475
|
+
function isPlaceholder(value: string): boolean {
|
|
476
|
+
// placeholders look like: <{aaa}> or <aaa{bbb}>
|
|
477
|
+
return value.match(/^<[^>]*?\{[^>]*?\}[^>]*?>$/) ? true : false;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Orders an object by keys */
|
|
481
|
+
function orderByKey<T>(unordered: Record<string, T>): Record<string, T> {
|
|
482
|
+
return Object.keys(unordered)
|
|
483
|
+
.sort()
|
|
484
|
+
.reduce((obj, key) => {
|
|
485
|
+
obj[key] = unordered[key];
|
|
486
|
+
return obj;
|
|
487
|
+
}, {});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Dev helper. Script can be run directly with command (without building the lib):
|
|
491
|
+
// yarn ts-node libs/cli/src/commands/apply-templates/apply-templates.ts apply-templates
|
|
492
|
+
if (require.main === module) {
|
|
493
|
+
yargs.command(applyTemplates).demandCommand().argv;
|
|
494
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import { cyanBright, red } from 'chalk';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
/** A single item as returned by `bw list items`. */
|
|
7
|
+
interface BitwardenItem {
|
|
8
|
+
name: string;
|
|
9
|
+
login?: {
|
|
10
|
+
username: string;
|
|
11
|
+
password: string;
|
|
12
|
+
};
|
|
13
|
+
fields?: [
|
|
14
|
+
{
|
|
15
|
+
name: string;
|
|
16
|
+
value: string;
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Interface for a vault which provides values. */
|
|
22
|
+
export interface Vault {
|
|
23
|
+
error: string | undefined;
|
|
24
|
+
getValue(name: string, fieldName: string): string | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Represents a Bitwarden vault accessed through Bitwarden CLI commands. */
|
|
28
|
+
export class BitwardenVault implements Vault {
|
|
29
|
+
constructor(private serverUrl: string | undefined) {
|
|
30
|
+
this.error = this.sync();
|
|
31
|
+
if (!this.error) {
|
|
32
|
+
this.items = this.fetchItems();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** If there is an error getting items from the vault this will not be undefined. */
|
|
36
|
+
public error: string | undefined;
|
|
37
|
+
|
|
38
|
+
private readonly items: BitwardenItem[] | undefined;
|
|
39
|
+
|
|
40
|
+
/** Get a value from the vault. If the value is not found returns undefined. */
|
|
41
|
+
public getValue(name: string, fieldName: string): string | undefined {
|
|
42
|
+
for (const item of this.items ?? []) {
|
|
43
|
+
if (item.name === name) {
|
|
44
|
+
if (item.login && fieldName === 'username') {
|
|
45
|
+
return item.login.username;
|
|
46
|
+
}
|
|
47
|
+
if (item.login && fieldName === 'password') {
|
|
48
|
+
return item.login.password;
|
|
49
|
+
}
|
|
50
|
+
if (item.fields) {
|
|
51
|
+
for (const field of item.fields) {
|
|
52
|
+
if (field.name === fieldName) {
|
|
53
|
+
return field.value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Syncs the Bitwarden vault. Note: this only downloads data.
|
|
64
|
+
*
|
|
65
|
+
* We could make this more interactive:
|
|
66
|
+
* - Get status ('yarn bw status') and parse it
|
|
67
|
+
* - Set server url automatically if its not set. Fail with error if it is set but different
|
|
68
|
+
* - Prompt for username & password if not logged in. Login and store session id in memory
|
|
69
|
+
* - Prompt for password if locked. Unlock and store session id in memory
|
|
70
|
+
* - Session id can then be passed to `sync` & `list items` commands with --session
|
|
71
|
+
* Reasons to not do this:
|
|
72
|
+
* - The script becomes more complex
|
|
73
|
+
* - Additional security concern of handling password entry
|
|
74
|
+
* - Password would need to be entered every time the script is run (rather than just once per terminal session)
|
|
75
|
+
* - Blocker: unlocked status is not reported correctly: https://github.com/bitwarden/clients/issues/2729. This
|
|
76
|
+
* makes it really hard to determine the correct course of action.
|
|
77
|
+
*/
|
|
78
|
+
private sync(): string | undefined {
|
|
79
|
+
try {
|
|
80
|
+
execSync(`yarn bw --nointeraction sync`);
|
|
81
|
+
console.log(`Bitwarden vault synced.`);
|
|
82
|
+
return undefined;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.log();
|
|
85
|
+
// Try to determine what is the issue and provide some direction
|
|
86
|
+
if (e instanceof Error && e.message.includes('getaddrinfo ENOTFOUND')) {
|
|
87
|
+
return 'The Bitwarden server could not be found. Do you need to connect to the VPN?';
|
|
88
|
+
}
|
|
89
|
+
if (e instanceof Error && e.message.includes('You are not logged in')) {
|
|
90
|
+
return this.serverUrl !== undefined
|
|
91
|
+
? 'You are not logged in. Run command `' +
|
|
92
|
+
cyanBright(`yarn bw config server ${this.serverUrl}`) +
|
|
93
|
+
'` to set the server url, then command `' +
|
|
94
|
+
cyanBright('yarn bw login') +
|
|
95
|
+
'` and follow the instructions to unlock your vault.'
|
|
96
|
+
: 'You are not logged in. Run command `' +
|
|
97
|
+
cyanBright('yarn bw login') +
|
|
98
|
+
'` and follow the instructions to unlock your vault.';
|
|
99
|
+
}
|
|
100
|
+
if (e instanceof Error && e.message.includes('Vault is locked')) {
|
|
101
|
+
return (
|
|
102
|
+
'Your vault is locked. Do you need to connect to the VPN? Run command `' +
|
|
103
|
+
cyanBright('yarn bw unlock') +
|
|
104
|
+
'` and follow the instructions to unlock your vault.'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return (
|
|
108
|
+
'Unexpected error syncing Bitwarden vault. You may need to run command `' +
|
|
109
|
+
cyanBright('yarn install') +
|
|
110
|
+
'` to install the Bitwarden CLI otherwise use command `' +
|
|
111
|
+
cyanBright('yarn bw status') +
|
|
112
|
+
'` to show the full status.'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Gets Bitwarden collection items from the vault.
|
|
119
|
+
*/
|
|
120
|
+
private fetchItems(): BitwardenItem[] {
|
|
121
|
+
try {
|
|
122
|
+
const json = execSync('yarn bw --nointeraction list items').toString();
|
|
123
|
+
return JSON.parse(json) as BitwardenItem[];
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.log();
|
|
126
|
+
console.error(red('ERROR: Could not list items in the vault.'));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './apply-templates';
|