@davidvornholt/standards 0.20.0 → 0.21.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 +4 -3
- package/package.json +9 -1
- package/src/cli.ts +1 -1
- package/src/creds-args.ts +9 -0
- package/src/creds-cloudflare-account.ts +2 -2
- package/src/creds-cloudflare-id.ts +10 -0
- package/src/creds-cloudflare-zone.ts +3 -3
- package/src/creds-commands.ts +13 -2
- package/src/creds-naming.ts +13 -0
- package/src/creds-plan-report.ts +87 -0
- package/src/creds-plan-run.ts +2 -11
- package/src/creds-plan-types.ts +19 -0
- package/src/creds-plan-unmanaged.ts +102 -0
- package/src/creds-plan.ts +25 -21
- package/src/creds-revoke.ts +162 -0
- package/src/creds-store-lock-holder.ts +8 -1
- package/src/creds-store-lock.ts +2 -1
- package/src/dev-env-compose.ts +120 -0
- package/src/dev-env-destination-duplicates.ts +10 -10
- package/src/dev-env-destination-gitignore.ts +22 -0
- package/src/dev-env-destination.ts +52 -30
- package/src/dev-env-document.ts +19 -6
- package/src/dev-env-dotenv.ts +12 -3
- package/src/dev-env-reconciliation.ts +170 -0
- package/src/dev-env-secrets.ts +41 -0
- package/src/dev-env-transaction-recovery.ts +74 -11
- package/src/dev-env-transaction.ts +61 -15
- package/src/dev-env.ts +115 -63
package/src/dev-env.ts
CHANGED
|
@@ -1,89 +1,107 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
3
|
+
import { composeDevEnv } from './dev-env-compose';
|
|
4
4
|
import {
|
|
5
|
+
type DevEnvRemoval,
|
|
5
6
|
type DevEnvWrite,
|
|
6
7
|
devEnvDestinationProblems,
|
|
7
8
|
} from './dev-env-destination';
|
|
9
|
+
import { devEnvGitIgnoreProblem } from './dev-env-destination-gitignore';
|
|
8
10
|
import { parseDevEnvDocument } from './dev-env-document';
|
|
9
11
|
import { renderDotenv } from './dev-env-dotenv';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
+
import { planDevEnvRemovals } from './dev-env-reconciliation';
|
|
13
|
+
import { DEV_SECRETS_FILE, readDevSecrets } from './dev-env-secrets';
|
|
14
|
+
import { applyDevEnvChanges } from './dev-env-transaction';
|
|
15
|
+
import { parseYaml } from './yaml-parse';
|
|
12
16
|
|
|
13
|
-
export const
|
|
17
|
+
export const DEV_CONFIG_FILE = 'config/dev.yaml';
|
|
18
|
+
export const DEV_LOCAL_FILE = 'config/dev.local.yaml';
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
export type DevEnvPlainInput = { readonly raw: unknown } | null;
|
|
16
21
|
|
|
17
|
-
type
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
export type DevEnvInputs = {
|
|
23
|
+
readonly config: DevEnvPlainInput;
|
|
24
|
+
readonly secrets: unknown;
|
|
25
|
+
readonly local: DevEnvPlainInput;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const isMissingPathError = (error: unknown): boolean =>
|
|
29
|
+
(error as { readonly code?: unknown } | null)?.code === 'ENOENT';
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
const readPlainLayer = (consumer: string, rel: string) => {
|
|
32
|
+
const path = join(consumer, rel);
|
|
33
|
+
try {
|
|
34
|
+
lstatSync(path);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (isMissingPathError(error)) {
|
|
37
|
+
return { input: null, present: false, problems: [] };
|
|
38
|
+
}
|
|
39
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
26
40
|
return {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
: `could not decrypt ${DEV_SECRETS_FILE}`,
|
|
41
|
+
input: null,
|
|
42
|
+
present: true,
|
|
43
|
+
problems: [`could not inspect ${rel}: ${detail}`],
|
|
31
44
|
};
|
|
32
45
|
}
|
|
46
|
+
let raw: string;
|
|
33
47
|
try {
|
|
34
|
-
|
|
48
|
+
raw = readFileSync(path, 'utf8');
|
|
35
49
|
} catch (error) {
|
|
50
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
36
51
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}`,
|
|
52
|
+
input: null,
|
|
53
|
+
present: true,
|
|
54
|
+
problems: [`could not read ${rel}: ${detail}`],
|
|
41
55
|
};
|
|
42
56
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// track them. `git check-ignore` is authoritative; anything but a clear
|
|
47
|
-
// "ignored" answer fails closed, including running outside a git checkout.
|
|
48
|
-
const gitIgnoreProblem = (consumer: string, rel: string): string | null => {
|
|
49
|
-
const result = spawnSync('git', ['check-ignore', '-q', '--', rel], {
|
|
50
|
-
cwd: consumer,
|
|
51
|
-
});
|
|
52
|
-
if (result.error !== undefined || result.status === null) {
|
|
53
|
-
return `cannot run git to verify ${rel} is gitignored`;
|
|
54
|
-
}
|
|
55
|
-
if (result.status === 0) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
if (result.status === 1) {
|
|
59
|
-
return `${rel} is not gitignored; ignore it before generating dev env files`;
|
|
57
|
+
const parsed = parseYaml(raw, rel);
|
|
58
|
+
if (parsed.problem !== null) {
|
|
59
|
+
return { input: null, present: true, problems: [parsed.problem] };
|
|
60
60
|
}
|
|
61
|
-
return
|
|
61
|
+
return { input: { raw: parsed.value ?? {} }, present: true, problems: [] };
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
const documentProblems = (
|
|
65
|
+
source: string,
|
|
66
|
+
input: DevEnvPlainInput,
|
|
67
|
+
): ReadonlyArray<string> =>
|
|
68
|
+
input === null ? [] : parseDevEnvDocument(input.raw, source).problems;
|
|
69
|
+
|
|
64
70
|
export type DevEnvPlan = {
|
|
65
71
|
readonly writes: ReadonlyArray<DevEnvWrite>;
|
|
72
|
+
readonly removals: ReadonlyArray<DevEnvRemoval>;
|
|
66
73
|
readonly problems: ReadonlyArray<string>;
|
|
67
74
|
};
|
|
68
75
|
|
|
69
|
-
export const
|
|
76
|
+
export const planDevEnvChanges = (
|
|
70
77
|
consumer: string,
|
|
71
|
-
|
|
78
|
+
inputs: DevEnvInputs,
|
|
72
79
|
): DevEnvPlan => {
|
|
73
|
-
const
|
|
74
|
-
|
|
80
|
+
const layer = (source: string, input: DevEnvPlainInput) =>
|
|
81
|
+
input === null
|
|
82
|
+
? null
|
|
83
|
+
: { source, document: parseDevEnvDocument(input.raw, source) };
|
|
84
|
+
const composed = composeDevEnv(
|
|
85
|
+
layer(DEV_CONFIG_FILE, inputs.config),
|
|
86
|
+
{
|
|
87
|
+
source: DEV_SECRETS_FILE,
|
|
88
|
+
document: parseDevEnvDocument(inputs.secrets, DEV_SECRETS_FILE),
|
|
89
|
+
},
|
|
90
|
+
layer(DEV_LOCAL_FILE, inputs.local),
|
|
91
|
+
);
|
|
92
|
+
const problems: Array<string> = [...composed.problems];
|
|
75
93
|
const writes: Array<DevEnvWrite> = [];
|
|
76
|
-
for (const target of
|
|
94
|
+
for (const target of composed.targets) {
|
|
77
95
|
const workspaceDir = `${target.group}/${target.workspace}`;
|
|
78
96
|
const rel = `${workspaceDir}/.env.local`;
|
|
79
97
|
if (existsSync(join(consumer, workspaceDir, 'package.json'))) {
|
|
80
|
-
const ignoreProblem =
|
|
98
|
+
const ignoreProblem = devEnvGitIgnoreProblem(consumer, rel);
|
|
81
99
|
if (ignoreProblem === null) {
|
|
82
100
|
writes.push({
|
|
83
101
|
rel,
|
|
84
102
|
content: renderDotenv(
|
|
85
103
|
`${target.group}.${target.workspace}`,
|
|
86
|
-
|
|
104
|
+
target.sources,
|
|
87
105
|
target.env,
|
|
88
106
|
),
|
|
89
107
|
});
|
|
@@ -92,36 +110,62 @@ export const planDevEnvWrites = (
|
|
|
92
110
|
}
|
|
93
111
|
} else {
|
|
94
112
|
problems.push(
|
|
95
|
-
`${
|
|
113
|
+
`${target.sources.join(' + ')} defines ${target.group}.${target.workspace}, but ${workspaceDir}/package.json does not exist`,
|
|
96
114
|
);
|
|
97
115
|
}
|
|
98
116
|
}
|
|
99
|
-
|
|
117
|
+
const removalPlan = planDevEnvRemovals(consumer, writes);
|
|
118
|
+
return {
|
|
119
|
+
writes,
|
|
120
|
+
removals: removalPlan.removals,
|
|
121
|
+
problems: [...problems, ...removalPlan.problems],
|
|
122
|
+
};
|
|
100
123
|
};
|
|
101
124
|
|
|
102
125
|
export const runDevEnv = async (consumer: string): Promise<boolean> => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
const config = readPlainLayer(consumer, DEV_CONFIG_FILE);
|
|
127
|
+
const secrets = readDevSecrets(consumer);
|
|
128
|
+
const local = readPlainLayer(consumer, DEV_LOCAL_FILE);
|
|
129
|
+
const localIgnoreProblem = local.present
|
|
130
|
+
? devEnvGitIgnoreProblem(consumer, DEV_LOCAL_FILE)
|
|
131
|
+
: null;
|
|
132
|
+
const inputProblems = [
|
|
133
|
+
...config.problems,
|
|
134
|
+
...(secrets.ok ? [] : [secrets.problem]),
|
|
135
|
+
...local.problems,
|
|
136
|
+
...(localIgnoreProblem === null ? [] : [localIgnoreProblem]),
|
|
137
|
+
...documentProblems(DEV_CONFIG_FILE, config.input),
|
|
138
|
+
...(secrets.ok
|
|
139
|
+
? parseDevEnvDocument(secrets.value, DEV_SECRETS_FILE).problems
|
|
140
|
+
: []),
|
|
141
|
+
...documentProblems(DEV_LOCAL_FILE, local.input),
|
|
142
|
+
];
|
|
143
|
+
if (inputProblems.length > 0 || !secrets.ok) {
|
|
144
|
+
console.error(`standards dev-env: ${inputProblems.length} problem(s):`);
|
|
145
|
+
console.error(inputProblems.map((problem) => ` - ${problem}`).join('\n'));
|
|
112
146
|
return false;
|
|
113
147
|
}
|
|
114
|
-
const plan =
|
|
148
|
+
const plan = planDevEnvChanges(consumer, {
|
|
149
|
+
config: config.input,
|
|
150
|
+
secrets: secrets.value,
|
|
151
|
+
local: local.input,
|
|
152
|
+
});
|
|
115
153
|
const problems = [
|
|
116
154
|
...plan.problems,
|
|
117
|
-
...(await devEnvDestinationProblems(consumer,
|
|
155
|
+
...(await devEnvDestinationProblems(consumer, [
|
|
156
|
+
...plan.writes,
|
|
157
|
+
...plan.removals,
|
|
158
|
+
])),
|
|
118
159
|
];
|
|
119
160
|
if (problems.length > 0) {
|
|
120
161
|
console.error(`standards dev-env: ${problems.length} problem(s):`);
|
|
121
162
|
console.error(problems.map((problem) => ` - ${problem}`).join('\n'));
|
|
122
163
|
return false;
|
|
123
164
|
}
|
|
124
|
-
const generated = await
|
|
165
|
+
const generated = await applyDevEnvChanges(consumer, [
|
|
166
|
+
...plan.writes,
|
|
167
|
+
...plan.removals,
|
|
168
|
+
]);
|
|
125
169
|
if (!generated.ok) {
|
|
126
170
|
console.error('standards dev-env: generation failed:');
|
|
127
171
|
console.error(
|
|
@@ -140,8 +184,16 @@ export const runDevEnv = async (consumer: string): Promise<boolean> => {
|
|
|
140
184
|
for (const write of plan.writes) {
|
|
141
185
|
console.log(` wrote ${write.rel}`);
|
|
142
186
|
}
|
|
187
|
+
for (const removal of plan.removals) {
|
|
188
|
+
console.log(` removed ${removal.rel}`);
|
|
189
|
+
}
|
|
190
|
+
const inputFiles = [
|
|
191
|
+
...(config.input === null ? [] : [DEV_CONFIG_FILE]),
|
|
192
|
+
DEV_SECRETS_FILE,
|
|
193
|
+
...(local.input === null ? [] : [DEV_LOCAL_FILE]),
|
|
194
|
+
];
|
|
143
195
|
console.log(
|
|
144
|
-
`standards dev-env: generated ${plan.writes.length} env file(s) from ${
|
|
196
|
+
`standards dev-env: generated ${plan.writes.length} and removed ${plan.removals.length} env file(s) from ${inputFiles.join(' + ')}`,
|
|
145
197
|
);
|
|
146
198
|
return true;
|
|
147
199
|
};
|