@fairgarden/distribution 0.1.0-0.canary.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 +97 -0
- package/dist/add-module.d.ts +62 -0
- package/dist/add-module.d.ts.map +1 -0
- package/dist/add-module.js +279 -0
- package/dist/add-module.js.map +1 -0
- package/dist/calver.d.ts +65 -0
- package/dist/calver.d.ts.map +1 -0
- package/dist/calver.js +115 -0
- package/dist/calver.js.map +1 -0
- package/dist/canary.d.ts +58 -0
- package/dist/canary.d.ts.map +1 -0
- package/dist/canary.js +130 -0
- package/dist/canary.js.map +1 -0
- package/dist/changelog.d.ts +111 -0
- package/dist/changelog.d.ts.map +1 -0
- package/dist/changelog.js +285 -0
- package/dist/changelog.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1076 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-edit.d.ts +18 -0
- package/dist/config-edit.d.ts.map +1 -0
- package/dist/config-edit.js +166 -0
- package/dist/config-edit.js.map +1 -0
- package/dist/extends.d.ts +60 -0
- package/dist/extends.d.ts.map +1 -0
- package/dist/extends.js +173 -0
- package/dist/extends.js.map +1 -0
- package/dist/extract.d.ts +31 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +177 -0
- package/dist/extract.js.map +1 -0
- package/dist/forks.d.ts +116 -0
- package/dist/forks.d.ts.map +1 -0
- package/dist/forks.js +357 -0
- package/dist/forks.js.map +1 -0
- package/dist/git-url.d.ts +19 -0
- package/dist/git-url.d.ts.map +1 -0
- package/dist/git-url.js +55 -0
- package/dist/git-url.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/inherit.d.ts +18 -0
- package/dist/inherit.d.ts.map +1 -0
- package/dist/inherit.js +84 -0
- package/dist/inherit.js.map +1 -0
- package/dist/manifest.d.ts +43 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +64 -0
- package/dist/manifest.js.map +1 -0
- package/dist/overrides.d.ts +32 -0
- package/dist/overrides.d.ts.map +1 -0
- package/dist/overrides.js +81 -0
- package/dist/overrides.js.map +1 -0
- package/dist/policy.d.ts +86 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +269 -0
- package/dist/policy.js.map +1 -0
- package/dist/readme.d.ts +60 -0
- package/dist/readme.d.ts.map +1 -0
- package/dist/readme.js +304 -0
- package/dist/readme.js.map +1 -0
- package/dist/release.d.ts +164 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +454 -0
- package/dist/release.js.map +1 -0
- package/dist/scaffold.d.ts +45 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +492 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/submodules.d.ts +144 -0
- package/dist/submodules.d.ts.map +1 -0
- package/dist/submodules.js +315 -0
- package/dist/submodules.js.map +1 -0
- package/dist/workflows.d.ts +102 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +710 -0
- package/dist/workflows.js.map +1 -0
- package/package.json +54 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
5
|
+
import { addModule } from "./add-module.js";
|
|
6
|
+
import { extractModule } from "./extract.js";
|
|
7
|
+
import { writeReadmes } from "./readme.js";
|
|
8
|
+
import { writeDistributionChangelogCheck, writeDistributionWorkflow, writeWorkflows } from "./workflows.js";
|
|
9
|
+
import { writeOverrides } from "./overrides.js";
|
|
10
|
+
import { checkReleasable, planRelease, readRelease, release, releaseDistribution, reportToActions as reportRelease, startPrerelease, } from "./release.js";
|
|
11
|
+
import { reportToActions as reportCanary, stampCanary } from "./canary.js";
|
|
12
|
+
import { describeRemote, isSsh, toHttps } from "./git-url.js";
|
|
13
|
+
import { distributionRepo, isEmpty, moduleRepo, monolithRepo, write, } from "./scaffold.js";
|
|
14
|
+
import { describeViolations, inspectExtends } from "./extends.js";
|
|
15
|
+
import { contribute, forkModule, integrate, unforkModule } from "./forks.js";
|
|
16
|
+
import { inherit } from "./inherit.js";
|
|
17
|
+
import { bumpEntry, checkChangelog, crossedReleases, CHANGELOG, moduleLabel, noteInDistribution, notesFor, pullRequestFromActions, } from "./changelog.js";
|
|
18
|
+
import { buildPolicy, findPolicy, POLICIES, setupTurbo, testPolicy, usePolicy } from "./policy.js";
|
|
19
|
+
import { stampModules } from "./manifest.js";
|
|
20
|
+
import { containsAll, inspect, isDistribution, isPublic, moveTo, repositoryRoot, setUrl, submodules, target, } from "./submodules.js";
|
|
21
|
+
// Piping into `head` closes stdout early; that is ordinary use, not a failure.
|
|
22
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
23
|
+
stream.on('error', (error) => {
|
|
24
|
+
if (error.code === 'EPIPE')
|
|
25
|
+
process.exit(0);
|
|
26
|
+
throw error;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const USAGE = `Usage: fg-dist <command> [options]
|
|
30
|
+
|
|
31
|
+
Commands:
|
|
32
|
+
init <kind> [dir] Scaffold a repository; kind is "distribution", "monolith" or "module"
|
|
33
|
+
add-module <url> Add a module repository as a submodule and link it
|
|
34
|
+
extract <path> Turn a directory here into its own repository and submodule
|
|
35
|
+
use-https [name...] Rewrite submodule urls from ssh to https
|
|
36
|
+
readme Record versions in the readmes; run in a distribution or a module
|
|
37
|
+
workflows [name...] Give every module a publishing workflow and a changelog check;
|
|
38
|
+
--distribution, this one a publishing workflow too
|
|
39
|
+
overrides Point the workspace at this distribution's own checkouts
|
|
40
|
+
canary Stamp a canary version into the manifest, for CI
|
|
41
|
+
release Move a module on from the release it just published
|
|
42
|
+
prerelease Start the next line on a branch, leaving main where it is
|
|
43
|
+
check Fail when this ships anything older than what it extends
|
|
44
|
+
inherit Add the modules the distribution this extends ships, and this does not
|
|
45
|
+
sync Report which modules have newer versions available
|
|
46
|
+
bump [name...] Move modules to their newest non-major version; a fork merges it
|
|
47
|
+
fork <name> <url> Check a module out from your fork, keeping where it came from
|
|
48
|
+
contribute <name> Push the module's branch and open a pull request upstream
|
|
49
|
+
unfork <name> Go back to the upstream a fork came from, once it has the fork's work
|
|
50
|
+
changelog check Fail a pull request that adds no changelog line linking itself (CI)
|
|
51
|
+
changelog notes [v] Print a version's changelog section, for its release notes
|
|
52
|
+
policy build Build the organization's policy: policies/, on every module's own
|
|
53
|
+
policy test Test each module's rules, then the organization's on top of them
|
|
54
|
+
policy use Put the built policy beside this service, for it to run
|
|
55
|
+
policy setup Have turbo build the policy once, before each service that uses it
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
--cwd <dir> Repository directory (default: the working directory)
|
|
59
|
+
--major Allow major upgrades (bump); the next major (release, prerelease)
|
|
60
|
+
--minor Release the next minor, or branch one (release, prerelease)
|
|
61
|
+
--patch Release the next patch, staying on this line (release)
|
|
62
|
+
--id <name> Prerelease identifier: alpha, beta, rc (release, prerelease)
|
|
63
|
+
--stable A distribution's release leaves its prerelease stages (release)
|
|
64
|
+
--no-fetch Use the refs already fetched (sync, bump)
|
|
65
|
+
--dry-run Report what would change without changing it
|
|
66
|
+
--check Verify instead of writing (readme, release, overrides)
|
|
67
|
+
--dry-run does the same for workflows, readme and overrides
|
|
68
|
+
--name <name> Package name for "init", mount name for add-module
|
|
69
|
+
--at <dir> Where add-module puts the submodule (default: apps/<name>)
|
|
70
|
+
--no-git Skip "git init" when scaffolding
|
|
71
|
+
--url <git-url> Remote the scaffolded repository will live at (init)
|
|
72
|
+
--extends <name> Distribution the scaffolded one extends (init distribution)
|
|
73
|
+
--separate Scaffold a distribution whose apps deploy separately
|
|
74
|
+
--ssh Record a submodule's URL as given, without rewriting it
|
|
75
|
+
--no-verify Skip checking whether the https urls can be cloned
|
|
76
|
+
--no-tag Do not tag the extracted module with its declared version
|
|
77
|
+
--force Overwrite workflows that are already there (workflows)
|
|
78
|
+
--distribution Publish the distribution itself, for others to extend (workflows)
|
|
79
|
+
--no-push Push nothing and open no pull request (release, fork, contribute, bump)
|
|
80
|
+
--to <ref> Upstream tag, branch or commit to go back to (unfork)
|
|
81
|
+
--base <branch> Branch the pull request goes into (contribute); what it merges into (changelog check)
|
|
82
|
+
--pr <number> The pull request to check, outside GitHub Actions (changelog check)
|
|
83
|
+
--repo <owner/name> The repository its link names, outside GitHub Actions (changelog check)
|
|
84
|
+
--out <file> Where policy build writes the bundle (default: policies/dist/)
|
|
85
|
+
`;
|
|
86
|
+
const parseArgs = (argv) => {
|
|
87
|
+
const args = {
|
|
88
|
+
command: undefined,
|
|
89
|
+
names: [],
|
|
90
|
+
cwd: process.cwd(),
|
|
91
|
+
check: false,
|
|
92
|
+
major: false,
|
|
93
|
+
fetch: true,
|
|
94
|
+
dryRun: false,
|
|
95
|
+
name: undefined,
|
|
96
|
+
at: undefined,
|
|
97
|
+
git: true,
|
|
98
|
+
url: undefined,
|
|
99
|
+
extends: undefined,
|
|
100
|
+
separate: false,
|
|
101
|
+
ssh: false,
|
|
102
|
+
verify: true,
|
|
103
|
+
tag: true,
|
|
104
|
+
force: false,
|
|
105
|
+
push: true,
|
|
106
|
+
bump: undefined,
|
|
107
|
+
id: undefined,
|
|
108
|
+
out: undefined,
|
|
109
|
+
to: undefined,
|
|
110
|
+
base: undefined,
|
|
111
|
+
distribution: false,
|
|
112
|
+
stable: false,
|
|
113
|
+
pr: undefined,
|
|
114
|
+
repo: undefined,
|
|
115
|
+
};
|
|
116
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
117
|
+
const arg = argv[index];
|
|
118
|
+
if (arg === '--cwd') {
|
|
119
|
+
args.cwd = path.resolve(argv[++index] ?? '.');
|
|
120
|
+
}
|
|
121
|
+
else if (arg === '--check') {
|
|
122
|
+
args.check = true;
|
|
123
|
+
}
|
|
124
|
+
else if (arg === '--major') {
|
|
125
|
+
args.major = true;
|
|
126
|
+
args.bump = 'major';
|
|
127
|
+
}
|
|
128
|
+
else if (arg === '--minor') {
|
|
129
|
+
args.bump = 'minor';
|
|
130
|
+
}
|
|
131
|
+
else if (arg === '--patch') {
|
|
132
|
+
args.bump = 'patch';
|
|
133
|
+
}
|
|
134
|
+
else if (arg === '--id') {
|
|
135
|
+
args.id = argv[++index];
|
|
136
|
+
}
|
|
137
|
+
else if (arg === '--no-fetch') {
|
|
138
|
+
args.fetch = false;
|
|
139
|
+
}
|
|
140
|
+
else if (arg === '--dry-run') {
|
|
141
|
+
args.dryRun = true;
|
|
142
|
+
}
|
|
143
|
+
else if (arg === '--name') {
|
|
144
|
+
args.name = argv[++index];
|
|
145
|
+
}
|
|
146
|
+
else if (arg === '--at') {
|
|
147
|
+
args.at = argv[++index];
|
|
148
|
+
}
|
|
149
|
+
else if (arg === '--no-git') {
|
|
150
|
+
args.git = false;
|
|
151
|
+
}
|
|
152
|
+
else if (arg === '--url') {
|
|
153
|
+
args.url = argv[++index];
|
|
154
|
+
}
|
|
155
|
+
else if (arg === '--extends') {
|
|
156
|
+
args.extends = argv[++index];
|
|
157
|
+
}
|
|
158
|
+
else if (arg === '--separate') {
|
|
159
|
+
args.separate = true;
|
|
160
|
+
}
|
|
161
|
+
else if (arg === '--ssh') {
|
|
162
|
+
args.ssh = true;
|
|
163
|
+
}
|
|
164
|
+
else if (arg === '--no-verify') {
|
|
165
|
+
args.verify = false;
|
|
166
|
+
}
|
|
167
|
+
else if (arg === '--no-tag') {
|
|
168
|
+
args.tag = false;
|
|
169
|
+
}
|
|
170
|
+
else if (arg === '--force') {
|
|
171
|
+
args.force = true;
|
|
172
|
+
}
|
|
173
|
+
else if (arg === '--no-push') {
|
|
174
|
+
args.push = false;
|
|
175
|
+
}
|
|
176
|
+
else if (arg === '--pr') {
|
|
177
|
+
args.pr = Number(argv[++index]);
|
|
178
|
+
}
|
|
179
|
+
else if (arg === '--repo') {
|
|
180
|
+
args.repo = argv[++index];
|
|
181
|
+
}
|
|
182
|
+
else if (arg === '--stable') {
|
|
183
|
+
args.stable = true;
|
|
184
|
+
}
|
|
185
|
+
else if (arg === '--distribution') {
|
|
186
|
+
args.distribution = true;
|
|
187
|
+
}
|
|
188
|
+
else if (arg === '--to') {
|
|
189
|
+
args.to = argv[++index];
|
|
190
|
+
}
|
|
191
|
+
else if (arg === '--base') {
|
|
192
|
+
args.base = argv[++index];
|
|
193
|
+
}
|
|
194
|
+
else if (arg === '--out') {
|
|
195
|
+
args.out = path.resolve(argv[++index] ?? '.');
|
|
196
|
+
}
|
|
197
|
+
else if (!arg.startsWith('-')) {
|
|
198
|
+
if (args.command)
|
|
199
|
+
args.names.push(arg);
|
|
200
|
+
else
|
|
201
|
+
args.command = arg;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
throw new Error(`Unrecognized argument: ${arg}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return args;
|
|
208
|
+
};
|
|
209
|
+
/** One line per module, aligned, saying what is available. */
|
|
210
|
+
/**
|
|
211
|
+
* Where a fork could go back to upstream, when upstream has taken what it
|
|
212
|
+
* added: a release past the pinned one, or its branch. Not a fork that has
|
|
213
|
+
* added nothing yet, which is where every fork starts.
|
|
214
|
+
*/
|
|
215
|
+
const canReturn = (state) => {
|
|
216
|
+
const merged = state.fork?.merged;
|
|
217
|
+
if (!merged)
|
|
218
|
+
return undefined;
|
|
219
|
+
if (merged === state.current && state.fork?.ahead === 0)
|
|
220
|
+
return undefined;
|
|
221
|
+
return merged.replace(/^upstream\//, '');
|
|
222
|
+
};
|
|
223
|
+
const describeState = (state) => {
|
|
224
|
+
const { current, available, untagged, dirty } = state;
|
|
225
|
+
const parts = [];
|
|
226
|
+
const at = current ? (state.exact ? current : `${current}+`) : 'untagged';
|
|
227
|
+
parts.push(at);
|
|
228
|
+
if (available.length > 0) {
|
|
229
|
+
const kinds = ['patch', 'minor', 'major']
|
|
230
|
+
.filter((kind) => state.upgrades[kind])
|
|
231
|
+
.map((kind) => `${state.upgrades[kind]} (${kind})`);
|
|
232
|
+
parts.push(`-> ${kinds.join(', ')}`);
|
|
233
|
+
}
|
|
234
|
+
else if (current) {
|
|
235
|
+
parts.push('up to date');
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
parts.push('no version tags');
|
|
239
|
+
}
|
|
240
|
+
if (untagged > 0)
|
|
241
|
+
parts.push(`${untagged} untagged commit${untagged === 1 ? '' : 's'}`);
|
|
242
|
+
if (state.fork) {
|
|
243
|
+
const { upstream, ahead } = state.fork;
|
|
244
|
+
const from = `fork of ${describeRemote(upstream)}`;
|
|
245
|
+
const returnTo = canReturn(state);
|
|
246
|
+
parts.push(returnTo
|
|
247
|
+
? `${from}, all in upstream ${returnTo}`
|
|
248
|
+
: ahead === 0
|
|
249
|
+
? `${from}, nothing of its own`
|
|
250
|
+
: `${from}, ${ahead} commit${ahead === 1 ? '' : 's'} not upstream`);
|
|
251
|
+
}
|
|
252
|
+
if (state.pushed === false)
|
|
253
|
+
parts.push('not pushed');
|
|
254
|
+
if (dirty)
|
|
255
|
+
parts.push('uncommitted changes');
|
|
256
|
+
if (state.fetched === false)
|
|
257
|
+
parts.push('could not fetch');
|
|
258
|
+
if (isSsh(state.submodule.url))
|
|
259
|
+
parts.push('ssh url');
|
|
260
|
+
return parts.join(' ');
|
|
261
|
+
};
|
|
262
|
+
/** Submodules of the repository holding `cwd`, optionally filtered by name. */
|
|
263
|
+
const modulesFor = (cwd, names) => {
|
|
264
|
+
const root = repositoryRoot(cwd);
|
|
265
|
+
if (!root)
|
|
266
|
+
throw new Error(`${cwd} is not inside a git repository.`);
|
|
267
|
+
const all = submodules(root);
|
|
268
|
+
if (all.length === 0) {
|
|
269
|
+
throw new Error(`${root} has no submodules to sync.`);
|
|
270
|
+
}
|
|
271
|
+
if (names.length === 0)
|
|
272
|
+
return { root, modules: all };
|
|
273
|
+
const modules = all.filter((module) => names.includes(module.name) || names.includes(module.relativePath));
|
|
274
|
+
const missing = names.filter((name) => !all.some((m) => m.name === name || m.relativePath === name));
|
|
275
|
+
if (missing.length > 0) {
|
|
276
|
+
throw new Error(`No such submodule: ${missing.join(', ')}. ` +
|
|
277
|
+
`Known: ${all.map((m) => m.name).join(', ')}.`);
|
|
278
|
+
}
|
|
279
|
+
return { root, modules };
|
|
280
|
+
};
|
|
281
|
+
const pad = (values) => values.reduce((widest, value) => Math.max(widest, value.length), 0);
|
|
282
|
+
const main = async () => {
|
|
283
|
+
const args = parseArgs(process.argv.slice(2));
|
|
284
|
+
if (!args.command || args.command === 'help') {
|
|
285
|
+
process.stdout.write(USAGE);
|
|
286
|
+
return args.command ? 0 : 1;
|
|
287
|
+
}
|
|
288
|
+
if (args.command === 'init') {
|
|
289
|
+
const [kind, where] = args.names;
|
|
290
|
+
if (kind !== 'distribution' && kind !== 'monolith' && kind !== 'module') {
|
|
291
|
+
process.stderr.write('init needs a kind: "distribution", "monolith" or "module".\n');
|
|
292
|
+
return 1;
|
|
293
|
+
}
|
|
294
|
+
const target = path.resolve(args.cwd, where ?? '.');
|
|
295
|
+
await mkdir(target, { recursive: true });
|
|
296
|
+
if (!(await isEmpty(target))) {
|
|
297
|
+
process.stderr.write(`${target} is not empty; refusing to scaffold over it.\n`);
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
300
|
+
const name = args.name ?? path.basename(target);
|
|
301
|
+
// A module is consumed as a submodule, so record the URL it will be
|
|
302
|
+
// cloned from in the form a keyless clone can use.
|
|
303
|
+
const origin = args.url ? (args.ssh ? args.url : toHttps(args.url)) : undefined;
|
|
304
|
+
const files = kind === 'distribution'
|
|
305
|
+
? distributionRepo(name, origin, args.extends, { monolith: !args.separate })
|
|
306
|
+
: kind === 'monolith'
|
|
307
|
+
? monolithRepo(name, origin)
|
|
308
|
+
: moduleRepo(name, origin);
|
|
309
|
+
const written = await write(target, files);
|
|
310
|
+
if (args.git) {
|
|
311
|
+
try {
|
|
312
|
+
execFileSync('git', ['init', '--quiet'], { cwd: target, stdio: 'ignore' });
|
|
313
|
+
if (origin) {
|
|
314
|
+
execFileSync('git', ['remote', 'add', 'origin', origin], {
|
|
315
|
+
cwd: target,
|
|
316
|
+
stdio: 'ignore',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
process.stderr.write('Could not run git init; scaffolding is still written.\n');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const file of written)
|
|
325
|
+
process.stdout.write(` ${file}\n`);
|
|
326
|
+
process.stdout.write(`\n${kind} scaffolded in ${target}.\n`);
|
|
327
|
+
if (origin) {
|
|
328
|
+
process.stdout.write(`origin is ${origin}\n`);
|
|
329
|
+
if (args.url && origin !== args.url) {
|
|
330
|
+
process.stdout.write(`(rewritten from ${args.url}; a submodule is cloned without an SSH key)\n`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
process.stdout.write('No remote set. Pass --url so a monolith can add this as a submodule.\n');
|
|
335
|
+
}
|
|
336
|
+
process.stdout.write(kind === 'module'
|
|
337
|
+
? 'Next: pnpm install, then commit and push so a distribution can add it.\n'
|
|
338
|
+
: args.extends && kind === 'distribution'
|
|
339
|
+
? `Next: pnpm install, then \`fg-dist inherit\` to ship what ${args.extends} ships.\n`
|
|
340
|
+
: 'Next: pnpm install, then `fg-dist add-module <url>` to ship a module.\n');
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
if (args.command === 'add-module') {
|
|
344
|
+
const [url] = args.names;
|
|
345
|
+
if (!url) {
|
|
346
|
+
process.stderr.write('add-module needs a repository url.\n');
|
|
347
|
+
return 1;
|
|
348
|
+
}
|
|
349
|
+
const result = await addModule(args.cwd, url, {
|
|
350
|
+
at: args.at,
|
|
351
|
+
mount: args.name,
|
|
352
|
+
ssh: args.ssh,
|
|
353
|
+
});
|
|
354
|
+
process.stdout.write(`Added ${result.relativePath} from ${result.url}\n`);
|
|
355
|
+
if (result.packageName && result.version) {
|
|
356
|
+
process.stdout.write(`Shipping ${result.packageName}@${result.version}, as the checkout declares it\n`);
|
|
357
|
+
}
|
|
358
|
+
if (result.rewritten) {
|
|
359
|
+
process.stdout.write(`(rewritten from ${url}; a submodule is cloned without an SSH key)\n`);
|
|
360
|
+
}
|
|
361
|
+
else if (args.ssh && isSsh(result.url)) {
|
|
362
|
+
process.stdout.write(`(recorded as SSH, so only clones with a key for ${describeRemote(result.url)} can build this)\n`);
|
|
363
|
+
}
|
|
364
|
+
if (result.linked && result.monolithPackageJson) {
|
|
365
|
+
process.stdout.write(`Linked through the workspace in ` +
|
|
366
|
+
`${path.relative(args.cwd, result.monolithPackageJson)}\n`);
|
|
367
|
+
}
|
|
368
|
+
else if (!result.packageName) {
|
|
369
|
+
process.stdout.write('It has no package.json, so nothing was linked.\n');
|
|
370
|
+
}
|
|
371
|
+
if (result.overrides) {
|
|
372
|
+
process.stdout.write('Workspace overrides updated, so this resolves from the tree rather than npm\n');
|
|
373
|
+
}
|
|
374
|
+
else if (result.overridesError) {
|
|
375
|
+
process.stderr.write(`\nThe module is added, but the workspace overrides were not updated:\n` +
|
|
376
|
+
` ${result.overridesError}\n` +
|
|
377
|
+
'Run `fg-dist overrides` once that is sorted.\n');
|
|
378
|
+
}
|
|
379
|
+
if (result.mounted && result.monolithConfig) {
|
|
380
|
+
process.stdout.write(`Mounted at /${result.mount} in ${path.relative(args.cwd, result.monolithConfig)}\n`);
|
|
381
|
+
}
|
|
382
|
+
else if (result.mountError && result.monolithPackageJson) {
|
|
383
|
+
process.stderr.write(`\nCould not mount it: ${result.mountError}\n` +
|
|
384
|
+
`Add this to the monolith's Next config yourself:\n` +
|
|
385
|
+
` ${result.mount}: '${result.packageName ?? result.relativePath}',\n`);
|
|
386
|
+
}
|
|
387
|
+
else if (!result.isApp) {
|
|
388
|
+
// A package is shared by the apps, not served at a path of its own.
|
|
389
|
+
process.stdout.write('It has no routes, so it is a package the apps depend on rather than one to mount.\n');
|
|
390
|
+
}
|
|
391
|
+
else if (!result.monolithPackageJson) {
|
|
392
|
+
// A distribution too complex to serve from one deployment has no
|
|
393
|
+
// monolith; each app under apps/ is deployed on its own.
|
|
394
|
+
process.stdout.write('No monolith app here, so it is shipped but not mounted. Deploy it on its own.\n');
|
|
395
|
+
}
|
|
396
|
+
else if (result.monolithConfig) {
|
|
397
|
+
process.stdout.write(`Already mounted at /${result.mount}.\n`);
|
|
398
|
+
}
|
|
399
|
+
if (result.turbo) {
|
|
400
|
+
process.stdout.write("turbo.json: the monolith's build waits for its libraries, not its own build\n");
|
|
401
|
+
}
|
|
402
|
+
process.stdout.write(result.monolithPackageJson
|
|
403
|
+
? '\nRun pnpm install and `fg-monolith merge-package-json`.\n'
|
|
404
|
+
: '\nRun pnpm install.\n');
|
|
405
|
+
return 0;
|
|
406
|
+
}
|
|
407
|
+
if (args.command === 'readme') {
|
|
408
|
+
const root = repositoryRoot(args.cwd);
|
|
409
|
+
if (!root)
|
|
410
|
+
throw new Error(`${args.cwd} is not inside a git repository.`);
|
|
411
|
+
const { updates, modules, kind } = await writeReadmes(root, {
|
|
412
|
+
check: args.check || args.dryRun,
|
|
413
|
+
});
|
|
414
|
+
const changed = updates.filter((update) => update.changed);
|
|
415
|
+
for (const module of modules) {
|
|
416
|
+
process.stdout.write(`${module.relativePath} ${module.name}@${module.version}\n`);
|
|
417
|
+
}
|
|
418
|
+
if (changed.length === 0) {
|
|
419
|
+
process.stdout.write(kind === 'module'
|
|
420
|
+
? 'This module\'s readme already states its version.\n'
|
|
421
|
+
: '\nEvery readme already states its version.\n');
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
if (args.check) {
|
|
425
|
+
process.stderr.write(`\n${changed.length} readme(s) are out of date:\n${changed
|
|
426
|
+
.map((update) => ` ${path.relative(root, update.file)}`)
|
|
427
|
+
.join('\n')}\nRun \`fg-dist readme\` to update them.\n`);
|
|
428
|
+
return 1;
|
|
429
|
+
}
|
|
430
|
+
process.stdout.write(`\nUpdated:\n${changed
|
|
431
|
+
.map((update) => ` ${path.relative(root, update.file)}`)
|
|
432
|
+
.join('\n')}\n`);
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
if (args.command === 'workflows') {
|
|
436
|
+
const root = repositoryRoot(args.cwd);
|
|
437
|
+
if (!root)
|
|
438
|
+
throw new Error(`${args.cwd} is not inside a git repository.`);
|
|
439
|
+
if (args.distribution) {
|
|
440
|
+
const update = await writeDistributionWorkflow(root, { force: args.force, dryRun: args.dryRun });
|
|
441
|
+
if (update.skipped) {
|
|
442
|
+
process.stdout.write('The distribution has a publishing workflow already. Pass --force to overwrite it.\n');
|
|
443
|
+
return 0;
|
|
444
|
+
}
|
|
445
|
+
process.stdout.write(`${update.files.join(', ')}\n`);
|
|
446
|
+
if (update.unprivated) {
|
|
447
|
+
process.stdout.write('\nTook "private": true off package.json — a private package cannot publish.\n');
|
|
448
|
+
}
|
|
449
|
+
if (update.missingAccess) {
|
|
450
|
+
process.stdout.write('Nothing says whether it publishes publicly. For others to extend it, add\n' +
|
|
451
|
+
' "publishConfig": { "access": "public" }\n');
|
|
452
|
+
}
|
|
453
|
+
process.stdout.write('\nIt publishes the manifest, recording every module it ships, and its policy. ' +
|
|
454
|
+
'Version it with `fg-dist release`, then run the workflow.\n');
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
const updates = await writeWorkflows(root, {
|
|
458
|
+
force: args.force,
|
|
459
|
+
dryRun: args.dryRun,
|
|
460
|
+
names: args.names,
|
|
461
|
+
});
|
|
462
|
+
// The distribution checks its own changelog too, for policy changes —
|
|
463
|
+
// unless only some modules were named.
|
|
464
|
+
if (isDistribution(root) && args.names.length === 0) {
|
|
465
|
+
updates.push(await writeDistributionChangelogCheck(root, { force: args.force, dryRun: args.dryRun }));
|
|
466
|
+
}
|
|
467
|
+
if (updates.length === 0) {
|
|
468
|
+
process.stdout.write('No modules here. Run this in a distribution, which is where the modules are.\n');
|
|
469
|
+
return 0;
|
|
470
|
+
}
|
|
471
|
+
const written = updates.filter((update) => !update.skipped);
|
|
472
|
+
for (const update of updates) {
|
|
473
|
+
process.stdout.write(update.skipped
|
|
474
|
+
? `${update.relativePath} has one already\n`
|
|
475
|
+
: `${update.relativePath} ${update.files.join(', ')}\n`);
|
|
476
|
+
}
|
|
477
|
+
if (written.length === 0) {
|
|
478
|
+
process.stdout.write('\nEvery module already publishes. Pass --force to overwrite the workflows.\n');
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
const unprivated = written.filter((update) => update.unprivated);
|
|
482
|
+
const missingFiles = written.filter((update) => update.missingFiles);
|
|
483
|
+
if (args.dryRun) {
|
|
484
|
+
process.stdout.write(`\n${written.length} module(s) would be written. Nothing changed.\n`);
|
|
485
|
+
return 0;
|
|
486
|
+
}
|
|
487
|
+
if (unprivated.length > 0) {
|
|
488
|
+
process.stdout.write(`\nTook "private": true off ${unprivated.map((u) => u.relativePath).join(', ')} — ` +
|
|
489
|
+
'a private package cannot publish.\n');
|
|
490
|
+
}
|
|
491
|
+
const missingAccess = written.filter((update) => update.missingAccess);
|
|
492
|
+
if (missingAccess.length > 0) {
|
|
493
|
+
process.stdout.write(`\nNo "publishConfig.access" in ${missingAccess.map((u) => u.relativePath).join(', ')}.\n` +
|
|
494
|
+
'Not set here, because npm treats an explicit value as an instruction to\n' +
|
|
495
|
+
'change an existing package\'s visibility. Say which it is yourself:\n' +
|
|
496
|
+
' "publishConfig": { "access": "public" } — and provenance is generated\n' +
|
|
497
|
+
' "publishConfig": { "access": "restricted" } — and it is not\n');
|
|
498
|
+
}
|
|
499
|
+
if (missingFiles.length > 0) {
|
|
500
|
+
process.stdout.write(`\nNo "files" list in ${missingFiles.map((u) => u.relativePath).join(', ')}.\n` +
|
|
501
|
+
'npm will ship whatever is in the directory. Say what belongs in the tarball:\n' +
|
|
502
|
+
' a module consumed as source ships "app", "lib", "public";\n' +
|
|
503
|
+
' one that builds ships "dist".\n');
|
|
504
|
+
}
|
|
505
|
+
process.stdout.write(`\nWrote a workflow into ${written.length} module(s).\n` +
|
|
506
|
+
'Each one is a separate repository, so commit and push them individually.\n' +
|
|
507
|
+
'npm trusted publishing has to be configured per package before the first run:\n' +
|
|
508
|
+
' publish once from a workstation, then point the trusted publisher at\n' +
|
|
509
|
+
' .github/workflows/publish.yml in that module\'s repository.\n');
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
if (args.command === 'overrides') {
|
|
513
|
+
const root = repositoryRoot(args.cwd);
|
|
514
|
+
if (!root)
|
|
515
|
+
throw new Error(`${args.cwd} is not inside a git repository.`);
|
|
516
|
+
const { file, names, changed, missing } = await writeOverrides(root, {
|
|
517
|
+
check: args.check || args.dryRun,
|
|
518
|
+
});
|
|
519
|
+
if (missing) {
|
|
520
|
+
process.stderr.write(`There is no ${path.relative(root, file) || 'pnpm-workspace.yaml'} here. ` +
|
|
521
|
+
'A distribution needs one before its modules can be overridden.\n');
|
|
522
|
+
return 1;
|
|
523
|
+
}
|
|
524
|
+
if (names.length === 0) {
|
|
525
|
+
process.stdout.write('No modules here. Run this in a distribution, which is what does the overriding.\n');
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
for (const name of names)
|
|
529
|
+
process.stdout.write(` ${name}\n`);
|
|
530
|
+
if (!changed) {
|
|
531
|
+
process.stdout.write(`\n${path.relative(root, file)} already covers every module.\n`);
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
if (args.dryRun) {
|
|
535
|
+
process.stdout.write(`\n${path.relative(root, file)} would be updated.\n`);
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
if (args.check) {
|
|
539
|
+
process.stderr.write(`\n${path.relative(root, file)} does not match the modules here.\n` +
|
|
540
|
+
'Run `fg-dist overrides` to update it.\n');
|
|
541
|
+
return 1;
|
|
542
|
+
}
|
|
543
|
+
process.stdout.write(`\nUpdated ${path.relative(root, file)}. Run \`pnpm install\` to take it up.\n`);
|
|
544
|
+
return 0;
|
|
545
|
+
}
|
|
546
|
+
if (args.command === 'canary') {
|
|
547
|
+
const root = repositoryRoot(args.cwd) ?? args.cwd;
|
|
548
|
+
const result = await stampCanary(root, { dryRun: args.dryRun });
|
|
549
|
+
await reportCanary(result);
|
|
550
|
+
process.stdout.write(result.skip
|
|
551
|
+
? `${result.name}@${result.version} is already the canary for ${result.sha}.\n`
|
|
552
|
+
: `${args.dryRun ? 'Would stamp' : 'Stamped'} ${result.name}@${result.version}\n`);
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
if (args.command === 'release') {
|
|
556
|
+
const root = repositoryRoot(args.cwd);
|
|
557
|
+
if (!root)
|
|
558
|
+
throw new Error(`${args.cwd} is not inside a git repository.`);
|
|
559
|
+
if (args.check) {
|
|
560
|
+
const releasable = await checkReleasable(root);
|
|
561
|
+
// A distribution records what it ships in what it publishes.
|
|
562
|
+
if (isDistribution(root))
|
|
563
|
+
await stampModules(root, { version: releasable.version });
|
|
564
|
+
await reportRelease(releasable);
|
|
565
|
+
process.stdout.write(`${releasable.name}@${releasable.version} is not on npm; ready to release as ` +
|
|
566
|
+
`${releasable.tag}.\n`);
|
|
567
|
+
return 0;
|
|
568
|
+
}
|
|
569
|
+
if (isDistribution(root)) {
|
|
570
|
+
if (args.bump) {
|
|
571
|
+
process.stderr.write("A distribution's version is its month and its release: move it with " +
|
|
572
|
+
'--id <alpha|beta|rc> or --stable, or neither to carry on as it is.\n');
|
|
573
|
+
return 1;
|
|
574
|
+
}
|
|
575
|
+
const next = await releaseDistribution(root, {
|
|
576
|
+
dryRun: args.dryRun,
|
|
577
|
+
id: args.id,
|
|
578
|
+
stable: args.stable,
|
|
579
|
+
});
|
|
580
|
+
const verb = args.dryRun ? 'Would move' : 'Moved';
|
|
581
|
+
process.stdout.write(next.version === next.from
|
|
582
|
+
? `${next.name} ${next.from} is not published yet; it is what the publish workflow releases next.\n`
|
|
583
|
+
: `${next.published ? `${next.from} is published. ` : `${next.from} was never published. `}` +
|
|
584
|
+
`${verb} ${next.name} to ${next.version}.\n`);
|
|
585
|
+
if (!args.dryRun && next.version !== next.from) {
|
|
586
|
+
process.stdout.write(`Commit it, then run the publish workflow, which publishes it and tags v${next.version}.\n`);
|
|
587
|
+
}
|
|
588
|
+
return 0;
|
|
589
|
+
}
|
|
590
|
+
// Which way main moves on is a judgement about what changed, so it is asked
|
|
591
|
+
// for rather than guessed at. Showing all three is the cheapest way to ask.
|
|
592
|
+
if (!args.bump) {
|
|
593
|
+
const { released, tagged, base } = await readRelease(root, { bump: 'minor' });
|
|
594
|
+
if (!released.includes('-')) {
|
|
595
|
+
process.stdout.write(`Released ${released}, ${tagged ? `tagged ${base}` : 'which is not tagged here'}\n\n` +
|
|
596
|
+
'Which way does main move on?\n\n');
|
|
597
|
+
for (const bump of ['patch', 'minor', 'major']) {
|
|
598
|
+
const plan = planRelease(released, { bump });
|
|
599
|
+
process.stdout.write(` --${bump.padEnd(6)} ${plan.next.version.padEnd(8)} ` +
|
|
600
|
+
(plan.maintenance
|
|
601
|
+
? `${plan.maintenance.branch} keeps ${released.split('.').slice(0, 2).join('.')}.x\n`
|
|
602
|
+
: 'stays on this line; nothing branches off\n'));
|
|
603
|
+
}
|
|
604
|
+
process.stdout.write(`\nOr \`fg-dist prerelease --major\` to start the next line beside main, ` +
|
|
605
|
+
'without\nmoving main off this one.\n');
|
|
606
|
+
return 1;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const outcome = await release(root, {
|
|
610
|
+
bump: args.bump,
|
|
611
|
+
id: args.id,
|
|
612
|
+
dryRun: args.dryRun,
|
|
613
|
+
push: args.push,
|
|
614
|
+
});
|
|
615
|
+
const { plan } = outcome;
|
|
616
|
+
process.stdout.write(`Released ${plan.released}, ` +
|
|
617
|
+
`${plan.tagged ? `tagged ${plan.base}` : 'which is not tagged here'}\n\n`);
|
|
618
|
+
const width = pad([plan.maintenance?.branch ?? '', plan.next.branch]);
|
|
619
|
+
if (plan.maintenance) {
|
|
620
|
+
process.stdout.write(` ${plan.maintenance.branch.padEnd(width)} ${plan.maintenance.version} ` +
|
|
621
|
+
`cut from ${plan.base}, where ${plan.released} is maintained\n`);
|
|
622
|
+
}
|
|
623
|
+
process.stdout.write(` ${plan.next.branch.padEnd(width)} ${plan.next.version} ` +
|
|
624
|
+
`opened against ${outcome.base}\n`);
|
|
625
|
+
if (args.dryRun) {
|
|
626
|
+
process.stdout.write('\nNothing written. Drop --dry-run to do it.\n');
|
|
627
|
+
return 0;
|
|
628
|
+
}
|
|
629
|
+
// Only worth saying when something was cut from the tag. A prerelease has
|
|
630
|
+
// no maintenance branch, so there is nothing the missing tag affected.
|
|
631
|
+
if (!plan.tagged && plan.maintenance) {
|
|
632
|
+
process.stdout.write(`\nThere is no v${plan.released} tag here, so ${plan.maintenance.branch} was cut ` +
|
|
633
|
+
`from HEAD.\nIf the release workflow has not run yet, run it first: the tag is ` +
|
|
634
|
+
`what\nseparates the released commit from whatever ${outcome.base} has done since.\n`);
|
|
635
|
+
}
|
|
636
|
+
if (!outcome.pushed) {
|
|
637
|
+
process.stdout.write('\nLeft local. Push them and open the pull request yourself.\n');
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
process.stdout.write(outcome.pullRequest
|
|
641
|
+
? `\n${outcome.pullRequest}\n`
|
|
642
|
+
: '\nPushed. `gh` is not here, so open the pull request yourself.\n');
|
|
643
|
+
return 0;
|
|
644
|
+
}
|
|
645
|
+
if (args.command === 'prerelease') {
|
|
646
|
+
const root = repositoryRoot(args.cwd);
|
|
647
|
+
if (!root)
|
|
648
|
+
throw new Error(`${args.cwd} is not inside a git repository.`);
|
|
649
|
+
if (args.bump !== 'major' && args.bump !== 'minor') {
|
|
650
|
+
process.stderr.write('prerelease needs --major or --minor: which line is being started.\n');
|
|
651
|
+
return 1;
|
|
652
|
+
}
|
|
653
|
+
const { plan, created, pushed } = await startPrerelease(root, {
|
|
654
|
+
bump: args.bump,
|
|
655
|
+
id: args.id,
|
|
656
|
+
dryRun: args.dryRun,
|
|
657
|
+
push: args.push,
|
|
658
|
+
});
|
|
659
|
+
process.stdout.write(` ${plan.branch} ${plan.version} cut from ${plan.from}, which main keeps\n`);
|
|
660
|
+
if (args.dryRun) {
|
|
661
|
+
process.stdout.write('\nNothing written. Drop --dry-run to do it.\n');
|
|
662
|
+
return 0;
|
|
663
|
+
}
|
|
664
|
+
if (!created)
|
|
665
|
+
return 1;
|
|
666
|
+
process.stdout.write(pushed
|
|
667
|
+
? `\nPushed. Release from it with a dist tag of its own — \`next\`, say — so\n` +
|
|
668
|
+
'`latest` goes on meaning the line main is shipping.\n'
|
|
669
|
+
: '\nLeft local. Push it when you are ready.\n');
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
if (args.command === 'policy') {
|
|
673
|
+
const [action] = args.names;
|
|
674
|
+
if (action === 'use') {
|
|
675
|
+
const { policy, status } = usePolicy(args.cwd);
|
|
676
|
+
process.stdout.write(policy
|
|
677
|
+
? status === 0
|
|
678
|
+
? `Using ${path.relative(args.cwd, policy.built) || policy.built}.\n`
|
|
679
|
+
: ''
|
|
680
|
+
: 'No organization policy here: the built-in rules decide.\n');
|
|
681
|
+
return status;
|
|
682
|
+
}
|
|
683
|
+
const policy = findPolicy(args.cwd);
|
|
684
|
+
if (!policy) {
|
|
685
|
+
process.stderr.write(`There is no organization policy here: no ${POLICIES}/.manifest above ${args.cwd}.\n`);
|
|
686
|
+
return 1;
|
|
687
|
+
}
|
|
688
|
+
if (action === 'build')
|
|
689
|
+
return buildPolicy(policy, args.cwd, args.out);
|
|
690
|
+
if (action === 'test')
|
|
691
|
+
return testPolicy(policy, args.cwd);
|
|
692
|
+
if (action === 'setup') {
|
|
693
|
+
const differ = setupTurbo(policy, { check: args.check });
|
|
694
|
+
if (differ.length === 0) {
|
|
695
|
+
process.stdout.write('turbo.json builds the policy once, before each service that uses it.\n');
|
|
696
|
+
return 0;
|
|
697
|
+
}
|
|
698
|
+
process.stdout.write(`${args.check ? 'turbo.json is missing' : 'Wrote'} ${differ.join(', ')}${args.check ? '; run fg-dist policy setup.' : ' into turbo.json.'}\n`);
|
|
699
|
+
return args.check ? 1 : 0;
|
|
700
|
+
}
|
|
701
|
+
process.stderr.write('Usage: fg-dist policy <build|test|use|setup>\n');
|
|
702
|
+
return 1;
|
|
703
|
+
}
|
|
704
|
+
if (args.command === 'check') {
|
|
705
|
+
const root = repositoryRoot(args.cwd);
|
|
706
|
+
if (!root)
|
|
707
|
+
throw new Error(`${args.cwd} is not inside a git repository.`);
|
|
708
|
+
const report = inspectExtends(root);
|
|
709
|
+
if (!report.distribution.extends) {
|
|
710
|
+
process.stdout.write(`${report.distribution.name} extends nothing, so there is no floor to check.\n`);
|
|
711
|
+
return 0;
|
|
712
|
+
}
|
|
713
|
+
if (report.unresolved) {
|
|
714
|
+
process.stderr.write(`${report.distribution.name} extends ${report.unresolved}, which is not ` +
|
|
715
|
+
'installed, so nothing could be compared against it.\n');
|
|
716
|
+
return 1;
|
|
717
|
+
}
|
|
718
|
+
if (report.violations.length > 0) {
|
|
719
|
+
process.stderr.write(`${describeViolations(report)}\n`);
|
|
720
|
+
return 1;
|
|
721
|
+
}
|
|
722
|
+
process.stdout.write(`${report.distribution.name}@${report.distribution.version} ships nothing older ` +
|
|
723
|
+
`than ${report.parent?.name}@${report.parent?.version}.\n`);
|
|
724
|
+
return 0;
|
|
725
|
+
}
|
|
726
|
+
if (args.command === 'extract') {
|
|
727
|
+
const [target] = args.names;
|
|
728
|
+
if (!target) {
|
|
729
|
+
process.stderr.write('extract needs the directory to turn into a module.\n');
|
|
730
|
+
return 1;
|
|
731
|
+
}
|
|
732
|
+
if (!args.url) {
|
|
733
|
+
process.stderr.write('extract needs --url, the repository the module will live at.\n');
|
|
734
|
+
return 1;
|
|
735
|
+
}
|
|
736
|
+
const result = await extractModule(args.cwd, target, {
|
|
737
|
+
url: args.url,
|
|
738
|
+
ssh: args.ssh,
|
|
739
|
+
tag: args.tag,
|
|
740
|
+
});
|
|
741
|
+
process.stdout.write(`${result.relativePath} is now ${result.initialised ? 'a repository' : 'its own repository'}` +
|
|
742
|
+
` and a submodule of this one\n`);
|
|
743
|
+
if (result.packageName && result.version) {
|
|
744
|
+
process.stdout.write(`Shipping ${result.packageName}@${result.version}\n`);
|
|
745
|
+
}
|
|
746
|
+
if (result.tagged) {
|
|
747
|
+
process.stdout.write(`Tagged ${result.tagged}, so sync can track it\n` +
|
|
748
|
+
` That tag is what the first release would have created, so release it\n` +
|
|
749
|
+
` with the next version rather than this one — or \`--no-tag\` next time.\n`);
|
|
750
|
+
}
|
|
751
|
+
if (result.overridesError) {
|
|
752
|
+
process.stderr.write(`\nThe module is extracted, but the workspace overrides were not updated:\n` +
|
|
753
|
+
` ${result.overridesError}\n` +
|
|
754
|
+
'Run `fg-dist overrides` once that is sorted.\n');
|
|
755
|
+
}
|
|
756
|
+
process.stdout.write(`origin is ${result.url}\n`);
|
|
757
|
+
if (result.rewritten) {
|
|
758
|
+
process.stdout.write(`(rewritten from ${args.url}; a submodule is cloned without an SSH key)\n`);
|
|
759
|
+
}
|
|
760
|
+
// Nothing was pushed: the submodule points at a remote that may not exist
|
|
761
|
+
// yet, which is fine here and fatal on anyone else's clone.
|
|
762
|
+
process.stdout.write(`\nPush it before anyone else clones this:\n` +
|
|
763
|
+
` git -C ${result.relativePath} push -u origin main --tags\n` +
|
|
764
|
+
'Then commit the new submodule here.\n');
|
|
765
|
+
return 0;
|
|
766
|
+
}
|
|
767
|
+
if (args.command === 'use-https') {
|
|
768
|
+
const { root, modules } = modulesFor(args.cwd, args.names);
|
|
769
|
+
const ssh = modules.filter((module) => isSsh(module.url));
|
|
770
|
+
if (ssh.length === 0) {
|
|
771
|
+
process.stdout.write('Every submodule url is already https.\n');
|
|
772
|
+
return 0;
|
|
773
|
+
}
|
|
774
|
+
for (const module of ssh) {
|
|
775
|
+
const https = toHttps(module.url);
|
|
776
|
+
if (https === module.url) {
|
|
777
|
+
process.stderr.write(`${module.relativePath} no https form for ${module.url}\n`);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (!args.dryRun)
|
|
781
|
+
setUrl(root, module, https);
|
|
782
|
+
process.stdout.write(`${module.relativePath} ${module.url} -> ${https}\n`);
|
|
783
|
+
}
|
|
784
|
+
if (args.dryRun) {
|
|
785
|
+
process.stdout.write('\nDry run; .gitmodules was not changed.\n');
|
|
786
|
+
return 0;
|
|
787
|
+
}
|
|
788
|
+
process.stdout.write('\n.gitmodules updated. Commit it to record the new urls.\n');
|
|
789
|
+
// https is necessary but not sufficient: a keyless clone also needs the
|
|
790
|
+
// repository to be readable without credentials.
|
|
791
|
+
if (!args.verify)
|
|
792
|
+
return 0;
|
|
793
|
+
process.stdout.write('\nChecking whether each can be cloned without credentials:\n');
|
|
794
|
+
const privateOnes = [];
|
|
795
|
+
for (const module of ssh) {
|
|
796
|
+
const https = toHttps(module.url);
|
|
797
|
+
const reachable = isPublic(https);
|
|
798
|
+
if (!reachable)
|
|
799
|
+
privateOnes.push(module.relativePath);
|
|
800
|
+
process.stdout.write(` ${module.relativePath} ${reachable ? 'public' : 'not readable'}\n`);
|
|
801
|
+
}
|
|
802
|
+
if (privateOnes.length > 0) {
|
|
803
|
+
process.stdout.write(`\n${privateOnes.length} of these cannot be read anonymously, so a build ` +
|
|
804
|
+
'host still cannot clone them. Make them public, or expect the build to fail.\n');
|
|
805
|
+
return 1;
|
|
806
|
+
}
|
|
807
|
+
process.stdout.write('\nAll readable anonymously.\n');
|
|
808
|
+
return 0;
|
|
809
|
+
}
|
|
810
|
+
if (args.command === 'sync') {
|
|
811
|
+
const { root, modules } = modulesFor(args.cwd, args.names);
|
|
812
|
+
const states = modules.map((module) => inspect(module, { fetch: args.fetch }));
|
|
813
|
+
const width = pad(states.map((state) => state.submodule.relativePath));
|
|
814
|
+
for (const state of states) {
|
|
815
|
+
process.stdout.write(`${state.submodule.relativePath.padEnd(width)} ${describeState(state)}\n`);
|
|
816
|
+
}
|
|
817
|
+
// .gitmodules is committed, so an SSH URL is what a build host will try.
|
|
818
|
+
const ssh = states.filter((state) => isSsh(state.submodule.url));
|
|
819
|
+
if (ssh.length > 0) {
|
|
820
|
+
process.stdout.write(`\n${ssh.length} module(s) are recorded with an ssh url, which a clone ` +
|
|
821
|
+
'without a key cannot use:\n');
|
|
822
|
+
for (const state of ssh) {
|
|
823
|
+
process.stdout.write(` ${state.submodule.relativePath} ${state.submodule.url}` +
|
|
824
|
+
` -> ${toHttps(state.submodule.url)}\n`);
|
|
825
|
+
}
|
|
826
|
+
process.stdout.write('Run `fg-dist use-https` to rewrite them. They also have to be ' +
|
|
827
|
+
'readable without credentials, which means public.\n');
|
|
828
|
+
}
|
|
829
|
+
// A pin the remote does not have breaks every other clone, a build host's first.
|
|
830
|
+
const unpushed = states.filter((state) => state.pushed === false);
|
|
831
|
+
if (unpushed.length > 0) {
|
|
832
|
+
process.stdout.write(`\n${unpushed.length} module(s) are pinned at a commit their remote does not have, ` +
|
|
833
|
+
'so no other clone can check them out:\n');
|
|
834
|
+
for (const state of unpushed) {
|
|
835
|
+
process.stdout.write(` ${state.submodule.relativePath} ${state.head.slice(0, 7)}\n`);
|
|
836
|
+
}
|
|
837
|
+
process.stdout.write('Push them — `fg-dist contribute <name>` does, for a change on its way upstream.\n');
|
|
838
|
+
}
|
|
839
|
+
// A fork whose work upstream has taken can go back to upstream.
|
|
840
|
+
const returnable = states.filter((state) => canReturn(state));
|
|
841
|
+
if (returnable.length > 0) {
|
|
842
|
+
process.stdout.write('\nUpstream has everything these forks add:\n');
|
|
843
|
+
for (const state of returnable) {
|
|
844
|
+
process.stdout.write(` ${state.submodule.relativePath} in ${canReturn(state)}\n`);
|
|
845
|
+
}
|
|
846
|
+
process.stdout.write('Keep them, or `fg-dist unfork <name>` to go back to upstream.\n');
|
|
847
|
+
}
|
|
848
|
+
// An extension may move ahead of what it extends, never behind it.
|
|
849
|
+
const extendsReport = inspectExtends(root);
|
|
850
|
+
if (extendsReport.violations.length > 0) {
|
|
851
|
+
process.stdout.write(`\n${describeViolations(extendsReport)}\n`);
|
|
852
|
+
}
|
|
853
|
+
else if (extendsReport.unresolved) {
|
|
854
|
+
process.stdout.write(`\nThis extends ${extendsReport.unresolved}, which is not installed, ` +
|
|
855
|
+
'so nothing could be compared against it.\n');
|
|
856
|
+
}
|
|
857
|
+
else if (extendsReport.parent) {
|
|
858
|
+
process.stdout.write(`\nUp to date with ${extendsReport.parent.name}@${extendsReport.parent.version}, ` +
|
|
859
|
+
'which it extends.\n');
|
|
860
|
+
}
|
|
861
|
+
const upgradable = states.filter((state) => target(state, { major: true }));
|
|
862
|
+
const untagged = states.filter((state) => state.current === undefined);
|
|
863
|
+
if (upgradable.length === 0) {
|
|
864
|
+
process.stdout.write(untagged.length === states.length
|
|
865
|
+
? '\nNo module carries a version tag, so there is nothing to compare. ' +
|
|
866
|
+
'Tag a release in each module to track it here.\n'
|
|
867
|
+
: '\nEverything is on its newest version.\n');
|
|
868
|
+
if (untagged.length > 0 && untagged.length < states.length) {
|
|
869
|
+
process.stdout.write(`${untagged.length} module(s) carry no version tag and were not compared.\n`);
|
|
870
|
+
}
|
|
871
|
+
return 0;
|
|
872
|
+
}
|
|
873
|
+
// A major is only held back when it is newer than the non-major target.
|
|
874
|
+
const held = upgradable.filter((state) => state.upgrades.major !== undefined);
|
|
875
|
+
process.stdout.write(`\n${upgradable.length} module(s) have newer versions. `);
|
|
876
|
+
process.stdout.write('Run `fg-dist bump` to take them');
|
|
877
|
+
process.stdout.write(held.length > 0
|
|
878
|
+
? `, or \`fg-dist bump --major\` to include the ${held.length} major upgrade(s).\n`
|
|
879
|
+
: '.\n');
|
|
880
|
+
return 0;
|
|
881
|
+
}
|
|
882
|
+
if (args.command === 'bump') {
|
|
883
|
+
const { root, modules } = modulesFor(args.cwd, args.names);
|
|
884
|
+
const states = modules.map((module) => inspect(module, { fetch: args.fetch }));
|
|
885
|
+
const moves = states
|
|
886
|
+
.map((state) => ({ state, to: target(state, { major: args.major }) }))
|
|
887
|
+
.filter((move) => move.to !== undefined);
|
|
888
|
+
if (moves.length === 0) {
|
|
889
|
+
process.stdout.write('Nothing to bump.\n');
|
|
890
|
+
const held = states.filter((state) => target(state, { major: true }));
|
|
891
|
+
if (held.length > 0 && !args.major) {
|
|
892
|
+
process.stdout.write(`${held.length} module(s) have a major upgrade available; pass --major to take it.\n`);
|
|
893
|
+
}
|
|
894
|
+
return 0;
|
|
895
|
+
}
|
|
896
|
+
// Moving a dirty submodule would fail mid-checkout or discard work.
|
|
897
|
+
const dirty = moves.filter((move) => move.state.dirty);
|
|
898
|
+
if (dirty.length > 0) {
|
|
899
|
+
process.stderr.write(`Refusing to bump, these have uncommitted changes:\n${dirty
|
|
900
|
+
.map((move) => ` ${move.state.submodule.relativePath}`)
|
|
901
|
+
.join('\n')}\n`);
|
|
902
|
+
return 1;
|
|
903
|
+
}
|
|
904
|
+
let failed = 0;
|
|
905
|
+
for (const { state, to } of moves) {
|
|
906
|
+
const from = state.current ?? state.head.slice(0, 7);
|
|
907
|
+
const line = `${state.submodule.relativePath} ${from} -> ${to}`;
|
|
908
|
+
if (args.dryRun) {
|
|
909
|
+
const how = !state.fork
|
|
910
|
+
? ''
|
|
911
|
+
: containsAll(state.submodule.path, `refs/tags/${to}`, state.head)
|
|
912
|
+
? ' (fork: upstream has all of it; would move to it)'
|
|
913
|
+
: ' (fork: would merge it in)';
|
|
914
|
+
process.stdout.write(`${line}${how}\n`);
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (!state.fork) {
|
|
918
|
+
moveTo(state.submodule, to);
|
|
919
|
+
noteInDistribution(root, bumpEntry(moduleLabel(state.submodule.path).name, from, to, state.submodule.url, crossedReleases(state.available, to)));
|
|
920
|
+
process.stdout.write(`${line}\n`);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
// A fork takes upstream's release by merging it, keeping its own work on top.
|
|
924
|
+
try {
|
|
925
|
+
const done = integrate(root, state, to, { push: args.push });
|
|
926
|
+
process.stdout.write(done.how === 'moved'
|
|
927
|
+
? `${line} (fork: upstream has all of it; \`fg-dist unfork ${state.submodule.name}\` goes back)\n`
|
|
928
|
+
: `${line} (fork: merged${done.pushed ? `, pushed to ${done.pushed}` : ', not pushed'})\n`);
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
failed += 1;
|
|
932
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
process.stdout.write(args.dryRun
|
|
936
|
+
? '\nDry run; nothing moved.\n'
|
|
937
|
+
: '\nSubmodule pointers moved. Commit them to record the new versions.\n');
|
|
938
|
+
return failed > 0 ? 1 : 0;
|
|
939
|
+
}
|
|
940
|
+
if (args.command === 'inherit') {
|
|
941
|
+
const root = repositoryRoot(args.cwd);
|
|
942
|
+
if (!root)
|
|
943
|
+
throw new Error(`${args.cwd} is not inside a git repository.`);
|
|
944
|
+
const result = await inherit(root, { dryRun: args.dryRun });
|
|
945
|
+
const width = pad(result.added.map((module) => module.path));
|
|
946
|
+
for (const module of result.added) {
|
|
947
|
+
process.stdout.write(`${module.path.padEnd(width)} ${module.name} ${module.version} at ${module.commit.slice(0, 7)}, ` +
|
|
948
|
+
`from ${describeRemote(module.repository)}` +
|
|
949
|
+
`${module.upstream ? ` (a fork of ${describeRemote(module.upstream)})` : ''}\n`);
|
|
950
|
+
}
|
|
951
|
+
for (const module of result.blocked) {
|
|
952
|
+
process.stderr.write(`${module.path} has something in it already, so ${module.name} was not added there.\n`);
|
|
953
|
+
}
|
|
954
|
+
if (result.added.length === 0 && result.blocked.length === 0) {
|
|
955
|
+
process.stdout.write(`Ships everything ${result.parent}@${result.parentVersion} ships already.\n`);
|
|
956
|
+
return 0;
|
|
957
|
+
}
|
|
958
|
+
process.stdout.write(args.dryRun
|
|
959
|
+
? '\nDry run; nothing added.\n'
|
|
960
|
+
: `\nShips what ${result.parent}@${result.parentVersion} ships. Run pnpm install, then commit.\n`);
|
|
961
|
+
return result.blocked.length > 0 ? 1 : 0;
|
|
962
|
+
}
|
|
963
|
+
if (args.command === 'changelog') {
|
|
964
|
+
const [action, version] = args.names;
|
|
965
|
+
const root = repositoryRoot(args.cwd) ?? args.cwd;
|
|
966
|
+
if (action === 'check') {
|
|
967
|
+
const actions = pullRequestFromActions();
|
|
968
|
+
const pullRequest = args.pr ?? actions.pullRequest;
|
|
969
|
+
const base = args.base ?? actions.base;
|
|
970
|
+
const repository = args.repo ?? actions.repository;
|
|
971
|
+
if (!pullRequest || !base || !repository) {
|
|
972
|
+
process.stderr.write('changelog check runs on a pull request: in GitHub Actions, or given ' +
|
|
973
|
+
'--pr <number> --base <ref> --repo <owner/name>.\n');
|
|
974
|
+
return 1;
|
|
975
|
+
}
|
|
976
|
+
const result = checkChangelog(root, {
|
|
977
|
+
pullRequest,
|
|
978
|
+
base,
|
|
979
|
+
repository,
|
|
980
|
+
distribution: isDistribution(root),
|
|
981
|
+
labels: actions.labels,
|
|
982
|
+
server: actions.server,
|
|
983
|
+
});
|
|
984
|
+
(result.ok ? process.stdout : process.stderr).write(`${result.message}\n`);
|
|
985
|
+
return result.ok ? 0 : 1;
|
|
986
|
+
}
|
|
987
|
+
if (action === 'notes') {
|
|
988
|
+
const wanted = version ?? JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')).version;
|
|
989
|
+
const text = await readFile(path.join(root, CHANGELOG), 'utf8').catch(() => '');
|
|
990
|
+
const notes = notesFor(text, wanted);
|
|
991
|
+
if (!notes) {
|
|
992
|
+
process.stderr.write(`${CHANGELOG} says nothing for ${wanted}.\n`);
|
|
993
|
+
return 1;
|
|
994
|
+
}
|
|
995
|
+
process.stdout.write(notes);
|
|
996
|
+
return 0;
|
|
997
|
+
}
|
|
998
|
+
process.stderr.write('changelog needs `check` or `notes`.\n');
|
|
999
|
+
return 1;
|
|
1000
|
+
}
|
|
1001
|
+
if (args.command === 'fork') {
|
|
1002
|
+
const [name, url] = args.names;
|
|
1003
|
+
if (!name || !url) {
|
|
1004
|
+
process.stderr.write('fork needs a module and the url of your fork: fg-dist fork <name> <url>\n');
|
|
1005
|
+
return 1;
|
|
1006
|
+
}
|
|
1007
|
+
const { root, modules } = modulesFor(args.cwd, [name]);
|
|
1008
|
+
const result = forkModule(root, modules[0], url, { ssh: args.ssh, push: args.push });
|
|
1009
|
+
process.stdout.write(`${modules[0].relativePath} is checked out from ${result.url}\n` +
|
|
1010
|
+
`forked from ${result.upstream}, which .gitmodules records as its upstream\n`);
|
|
1011
|
+
if (result.rewritten) {
|
|
1012
|
+
process.stdout.write(`(rewritten from ${url}; a submodule is cloned without an SSH key)\n`);
|
|
1013
|
+
}
|
|
1014
|
+
process.stdout.write(result.branch
|
|
1015
|
+
? `Pinned commit pushed to ${result.branch}, where everything this distribution pins in the fork lives.\n`
|
|
1016
|
+
: 'Nothing pushed. The pinned commit has to be on the fork before anyone else can check it out.\n');
|
|
1017
|
+
process.stdout.write(`\nNext: make the change on a branch in ${modules[0].relativePath}, then ` +
|
|
1018
|
+
`\`fg-dist contribute ${modules[0].name}\`.\nCommit .gitmodules and ${modules[0].relativePath} here.\n`);
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
1021
|
+
if (args.command === 'contribute') {
|
|
1022
|
+
const [name] = args.names;
|
|
1023
|
+
if (!name) {
|
|
1024
|
+
process.stderr.write('contribute needs a module: fg-dist contribute <name>\n');
|
|
1025
|
+
return 1;
|
|
1026
|
+
}
|
|
1027
|
+
const { root, modules } = modulesFor(args.cwd, [name]);
|
|
1028
|
+
const result = contribute(root, modules[0], { push: args.push, base: args.base });
|
|
1029
|
+
if (!args.push) {
|
|
1030
|
+
process.stdout.write(`Would push ${result.branch} and open a pull request into ${describeRemote(result.repository)} ${result.base}.\n`);
|
|
1031
|
+
return 0;
|
|
1032
|
+
}
|
|
1033
|
+
process.stdout.write(`Pushed ${result.branch}.\n`);
|
|
1034
|
+
if (result.pinnedOn)
|
|
1035
|
+
process.stdout.write(`Pinned commit kept on ${result.pinnedOn}, which outlives the pull request's branch.\n`);
|
|
1036
|
+
if (result.pullRequest) {
|
|
1037
|
+
process.stdout.write(`Opened ${result.pullRequest}\n`);
|
|
1038
|
+
}
|
|
1039
|
+
else if (result.compare) {
|
|
1040
|
+
process.stdout.write(`Open the pull request at\n ${result.compare}\n`);
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
process.stdout.write(`Open a pull request from ${result.branch} into ${result.base} on ${describeRemote(result.repository)}.\n`);
|
|
1044
|
+
}
|
|
1045
|
+
if (result.changelog) {
|
|
1046
|
+
process.stdout.write(`\nIts changelog check wants a line in ${result.changelog.file}, under ${result.changelog.version}. ` +
|
|
1047
|
+
'The words are yours:\n' +
|
|
1048
|
+
` ${result.changelog.line}\n` +
|
|
1049
|
+
'Commit it on the branch and push, then run this again to keep the pin current.\n');
|
|
1050
|
+
}
|
|
1051
|
+
process.stdout.write(`\nCommit ${modules[0].relativePath} here to ship the change meanwhile.\n`);
|
|
1052
|
+
return 0;
|
|
1053
|
+
}
|
|
1054
|
+
if (args.command === 'unfork') {
|
|
1055
|
+
const [name] = args.names;
|
|
1056
|
+
if (!name) {
|
|
1057
|
+
process.stderr.write('unfork needs a module: fg-dist unfork <name>\n');
|
|
1058
|
+
return 1;
|
|
1059
|
+
}
|
|
1060
|
+
const { root, modules } = modulesFor(args.cwd, [name]);
|
|
1061
|
+
const state = inspect(modules[0], { fetch: args.fetch });
|
|
1062
|
+
const result = unforkModule(root, state, { to: args.to, major: args.major, force: args.force });
|
|
1063
|
+
process.stdout.write(`${modules[0].relativePath} is checked out from ${result.url} again, at ${result.to}.\n` +
|
|
1064
|
+
`Commit .gitmodules and ${modules[0].relativePath} here. The fork itself is left as it is.\n`);
|
|
1065
|
+
return 0;
|
|
1066
|
+
}
|
|
1067
|
+
process.stderr.write(`Unknown command: ${args.command}\n\n${USAGE}`);
|
|
1068
|
+
return 1;
|
|
1069
|
+
};
|
|
1070
|
+
main().then((code) => {
|
|
1071
|
+
process.exitCode = code;
|
|
1072
|
+
}, (error) => {
|
|
1073
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
1074
|
+
process.exitCode = 1;
|
|
1075
|
+
});
|
|
1076
|
+
//# sourceMappingURL=cli.js.map
|