@lukoweb/apitogo 0.2.35 → 0.2.37
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/dist/cli/cli.js +1567 -2709
- package/dist/declarations/cli/platform/client/platform-api.d.ts +9 -0
- package/dist/declarations/lib/authentication/providers/openid.d.ts +6 -1
- package/package.json +1 -1
- package/src/config/v2/home-redirect.ts +8 -0
- package/src/config/v2/index.ts +2 -0
- package/src/config/v2/init.ts +180 -273
- package/src/config/v2/paths.ts +63 -54
- package/src/config/v2/protected-routes.ts +43 -0
- package/src/config/v2/resolve.ts +40 -1
- package/src/config/v2/rules.ts +27 -0
- package/src/config/v2/schema.ts +246 -245
- package/src/config/v2/services.ts +97 -15
- package/src/config/v2/test-project.ts +31 -0
- package/src/config/v2/wrapper.ts +28 -2
- package/src/lib/authentication/providers/openid.tsx +611 -516
- package/src/lib/authentication/providers/supabase.tsx +412 -412
|
@@ -598,6 +598,15 @@ export declare class PlatformApiClient {
|
|
|
598
598
|
getGatewayPlan(projectId: string, planId: string): Promise<GatewayPlanDto>;
|
|
599
599
|
updateGatewayPlan(projectId: string, planId: string, body: UpdateGatewayPlanRequest): Promise<GatewayPlanDto>;
|
|
600
600
|
deleteGatewayPlan(projectId: string, planId: string): Promise<void>;
|
|
601
|
+
createTestSignIn(projectId: string, body: {
|
|
602
|
+
email: string;
|
|
603
|
+
returnUrl?: string;
|
|
604
|
+
}): Promise<{
|
|
605
|
+
userId: string;
|
|
606
|
+
created: boolean;
|
|
607
|
+
url: string;
|
|
608
|
+
expiresInSeconds: number;
|
|
609
|
+
}>;
|
|
601
610
|
getGatewayAuth(projectId: string): Promise<GatewayAuthConfigDto>;
|
|
602
611
|
updateGatewayAuth(projectId: string, body: UpdateGatewayAuthRequest): Promise<GatewayAuthConfigDto>;
|
|
603
612
|
getGatewaySettings(projectId: string): Promise<GatewaySettingsDto>;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import * as oauth from "oauth4webapi";
|
|
2
2
|
import type { NavigateFunction } from "react-router";
|
|
3
3
|
import type { OpenIDAuthenticationConfig } from "../../../config/config.js";
|
|
4
|
+
import type { ApiIdentity } from "../../core/ZudokuContext.js";
|
|
4
5
|
import type { AuthActionContext, AuthActionOptions, AuthenticationPlugin, AuthenticationProviderInitializer } from "../authentication.js";
|
|
5
6
|
import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
|
|
7
|
+
export declare const SIGNED_IN_HINT_KEY = "apitogo-signed-in-hint";
|
|
8
|
+
export declare const SILENT_ATTEMPT_KEY = "apitogo-silent-signin";
|
|
6
9
|
export interface OpenIdProviderData {
|
|
7
10
|
type: "openid" | undefined;
|
|
8
11
|
accessToken: string;
|
|
@@ -41,12 +44,14 @@ export declare class OpenIDAuthenticationProvider extends CoreAuthenticationPlug
|
|
|
41
44
|
redirectTo?: string;
|
|
42
45
|
replace?: boolean;
|
|
43
46
|
}): Promise<void>;
|
|
44
|
-
signIn(_: AuthActionContext, { redirectTo, replace }
|
|
47
|
+
signIn(_: AuthActionContext, { redirectTo, replace }?: AuthActionOptions): Promise<void>;
|
|
45
48
|
refreshUserProfile(): Promise<boolean>;
|
|
46
49
|
private authorize;
|
|
47
50
|
getAccessToken(): Promise<string>;
|
|
48
51
|
signRequest: (request: Request) => Promise<Request>;
|
|
52
|
+
getIdentities: () => Promise<ApiIdentity[]>;
|
|
49
53
|
signOut: (_: AuthActionContext) => Promise<void>;
|
|
54
|
+
initialize(): Promise<void>;
|
|
50
55
|
onPageLoad: () => Promise<void>;
|
|
51
56
|
handleCallback: () => Promise<string>;
|
|
52
57
|
getRoutes(): {
|
package/package.json
CHANGED
|
@@ -75,6 +75,8 @@ export type HomeRedirectInput = {
|
|
|
75
75
|
apiReferencePath?: string;
|
|
76
76
|
/** The portal has console pages (sign-in, keys, plans). */
|
|
77
77
|
userPanel: boolean;
|
|
78
|
+
/** The portal has a pricing page, at this path. */
|
|
79
|
+
pricingPath?: string;
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
/**
|
|
@@ -84,6 +86,9 @@ export type HomeRedirectInput = {
|
|
|
84
86
|
* - Hosted docs without an index page: the first page, so the team's own docs never need an extra file.
|
|
85
87
|
* - Hosted docs without any page: the API reference, when there is one.
|
|
86
88
|
* - External docs and no console: the external docs site, as the portal has nothing else to show.
|
|
89
|
+
* - No hosted docs but a console: the pricing page when there is one, else the account pages, which lead a signed-out
|
|
90
|
+
* visitor to sign in. The console has no page of its own at `/`, so the address `deploy` prints showed "Page not
|
|
91
|
+
* found" (API-73).
|
|
87
92
|
*/
|
|
88
93
|
export function homeRedirect(
|
|
89
94
|
input: HomeRedirectInput,
|
|
@@ -100,5 +105,8 @@ export function homeRedirect(
|
|
|
100
105
|
if (input.externalDocsUrl && !input.userPanel) {
|
|
101
106
|
return { from: "/", to: input.externalDocsUrl };
|
|
102
107
|
}
|
|
108
|
+
if (input.userPanel) {
|
|
109
|
+
return { from: "/", to: input.pricingPath ?? "/account" };
|
|
110
|
+
}
|
|
103
111
|
return undefined;
|
|
104
112
|
}
|
package/src/config/v2/index.ts
CHANGED
package/src/config/v2/init.ts
CHANGED
|
@@ -1,273 +1,180 @@
|
|
|
1
|
-
import { access, mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { ApitogoError } from "../../lib/util/invariant.js";
|
|
4
|
-
import { APITOGO_CONFIG_FILENAME } from "../apitogo-config-core.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
async function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// No existing docs to reference: give the portal an empty pages folder with one starter page.
|
|
182
|
-
const starter = path.join(root, "portal", "pages", "index.mdx");
|
|
183
|
-
if (await exists(starter)) keep.push(starter);
|
|
184
|
-
else
|
|
185
|
-
entries.push({
|
|
186
|
-
kind: "file",
|
|
187
|
-
path: starter,
|
|
188
|
-
purpose: "portal",
|
|
189
|
-
content: starterPage(name),
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
dir,
|
|
195
|
-
configPath,
|
|
196
|
-
layout,
|
|
197
|
-
layoutSource: options.layout ? "flag" : "detected",
|
|
198
|
-
layoutReason,
|
|
199
|
-
config,
|
|
200
|
-
docsCandidates,
|
|
201
|
-
docsIncluded,
|
|
202
|
-
needsDocsChoice,
|
|
203
|
-
entries,
|
|
204
|
-
keep,
|
|
205
|
-
replacesConfig,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export type InitResult = {
|
|
210
|
-
created: string[];
|
|
211
|
-
kept: string[];
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
/** Writes the plan. Existing files are skipped, except the config when the plan replaces it. */
|
|
215
|
-
export async function applyInitPlan(plan: InitPlan): Promise<InitResult> {
|
|
216
|
-
const created: string[] = [];
|
|
217
|
-
const kept = [...plan.keep];
|
|
218
|
-
|
|
219
|
-
await mkdir(plan.dir, { recursive: true });
|
|
220
|
-
await ensureStateDir(statePathsFor(plan.dir, plan.layout));
|
|
221
|
-
for (const entry of plan.entries.filter((e) => e.purpose === "state"))
|
|
222
|
-
created.push(entry.path);
|
|
223
|
-
|
|
224
|
-
for (const entry of plan.entries.filter((e) => e.purpose !== "state")) {
|
|
225
|
-
if (entry.kind === "dir") {
|
|
226
|
-
await mkdir(entry.path, { recursive: true });
|
|
227
|
-
created.push(entry.path);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
await mkdir(path.dirname(entry.path), { recursive: true });
|
|
231
|
-
try {
|
|
232
|
-
await writeFile(entry.path, entry.content ?? "", {
|
|
233
|
-
flag: entry.purpose === "config" && plan.replacesConfig ? "w" : "wx",
|
|
234
|
-
});
|
|
235
|
-
created.push(entry.path);
|
|
236
|
-
} catch (error) {
|
|
237
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
238
|
-
kept.push(entry.path);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return { created, kept };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** The "will create" list shown before anything is written. */
|
|
246
|
-
export function formatInitPlan(plan: InitPlan): string[] {
|
|
247
|
-
const rel = (target: string) =>
|
|
248
|
-
path.relative(plan.dir, target).split(path.sep).join("/") || ".";
|
|
249
|
-
const lines = [
|
|
250
|
-
`Layout: ${plan.layout} (${plan.layoutReason})`,
|
|
251
|
-
plan.docsIncluded.length > 0
|
|
252
|
-
? `Existing docs are used in place: ${plan.docsIncluded.join(", ")}`
|
|
253
|
-
: "No existing docs found: a starter page is created.",
|
|
254
|
-
"Will create:",
|
|
255
|
-
...plan.entries
|
|
256
|
-
.filter((entry) => !(entry.kind === "dir" && entry.purpose === "state"))
|
|
257
|
-
.map(
|
|
258
|
-
(entry) =>
|
|
259
|
-
` ${rel(entry.path)}${plan.replacesConfig && entry.purpose === "config" ? " (replaces the existing file)" : ""}`,
|
|
260
|
-
),
|
|
261
|
-
` ${rel(statePathsFor(plan.dir, plan.layout).dir)}/generated/ and build/ (generated state, ignored by git)`,
|
|
262
|
-
];
|
|
263
|
-
if (plan.keep.length > 0) {
|
|
264
|
-
lines.push(
|
|
265
|
-
"Will keep as they are:",
|
|
266
|
-
...plan.keep.map((target) => ` ${rel(target)}`),
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
lines.push(
|
|
270
|
-
"Your own files are never moved, renamed or edited, and your .gitignore is left alone.",
|
|
271
|
-
);
|
|
272
|
-
return lines;
|
|
273
|
-
}
|
|
1
|
+
import { access, mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ApitogoError } from "../../lib/util/invariant.js";
|
|
4
|
+
import { APITOGO_CONFIG_FILENAME } from "../apitogo-config-core.js";
|
|
5
|
+
import { parseConfigV2 } from "./parse.js";
|
|
6
|
+
import { layoutRoot } from "./paths.js";
|
|
7
|
+
import type { ConfigV2 } from "./schema.js";
|
|
8
|
+
|
|
9
|
+
export class InitError extends ApitogoError {
|
|
10
|
+
constructor(message: string, developerHint?: string) {
|
|
11
|
+
super(message, { developerHint, title: "Cannot initialise the project" });
|
|
12
|
+
this.name = "InitError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type InitOptions = {
|
|
17
|
+
/** Project directory; created if it does not exist. */
|
|
18
|
+
dir: string;
|
|
19
|
+
/** Project name; defaults to the directory name. */
|
|
20
|
+
name?: string;
|
|
21
|
+
/** Replace an existing `apitogo.config.json`. It is the only existing file `init` ever replaces. */
|
|
22
|
+
force?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type PlannedEntry = {
|
|
26
|
+
kind: "dir" | "file";
|
|
27
|
+
/** Absolute path. */
|
|
28
|
+
path: string;
|
|
29
|
+
purpose: "config" | "root";
|
|
30
|
+
content?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type InitPlan = {
|
|
34
|
+
dir: string;
|
|
35
|
+
configPath: string;
|
|
36
|
+
/** The config that will be written; already validated against the v2 schema. */
|
|
37
|
+
config: ConfigV2;
|
|
38
|
+
/** Everything `init` will create. */
|
|
39
|
+
entries: PlannedEntry[];
|
|
40
|
+
/** Files that already exist and stay untouched. */
|
|
41
|
+
keep: string[];
|
|
42
|
+
/** An existing config will be replaced (only with `force`). */
|
|
43
|
+
replacesConfig: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
async function exists(target: string): Promise<boolean> {
|
|
47
|
+
try {
|
|
48
|
+
await access(target);
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function starterPage(name: string): string {
|
|
56
|
+
return `---
|
|
57
|
+
title: Welcome
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
# ${name}
|
|
61
|
+
|
|
62
|
+
This page was created by \`apitogo init\`. Edit it, and add more \`.md\` or \`.mdx\` files next to it: each one becomes a page of your portal.
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Turns paths the user named into globs, checking that each exists inside the project. */
|
|
67
|
+
export async function docsFromPaths(
|
|
68
|
+
dir: string,
|
|
69
|
+
requested: readonly string[],
|
|
70
|
+
): Promise<string[]> {
|
|
71
|
+
const globs: string[] = [];
|
|
72
|
+
for (const requestedPath of requested) {
|
|
73
|
+
const absolute = path.resolve(dir, requestedPath);
|
|
74
|
+
const relative = path.relative(dir, absolute).split(path.sep).join("/");
|
|
75
|
+
if (
|
|
76
|
+
relative === "" ||
|
|
77
|
+
relative.startsWith("..") ||
|
|
78
|
+
path.isAbsolute(relative)
|
|
79
|
+
) {
|
|
80
|
+
throw new InitError(
|
|
81
|
+
`Docs path "${requestedPath}" is outside the project directory.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const info = await stat(absolute).catch(() => undefined);
|
|
85
|
+
if (!info)
|
|
86
|
+
throw new InitError(
|
|
87
|
+
`Docs path "${requestedPath}" does not exist in ${dir}.`,
|
|
88
|
+
);
|
|
89
|
+
globs.push(info.isDirectory() ? `${relative}/**/*.{md,mdx}` : relative);
|
|
90
|
+
}
|
|
91
|
+
return globs;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Decides what `init` would do without touching the disk (Reference, section 1.2): the config and the `apitogo/`
|
|
96
|
+
* folder with its own `.gitignore`, nothing else. Content and detection belong to `add`. Existing files are never
|
|
97
|
+
* moved, renamed or modified; the root `.gitignore` is never edited.
|
|
98
|
+
*/
|
|
99
|
+
export async function planInit(options: InitOptions): Promise<InitPlan> {
|
|
100
|
+
const dir = path.resolve(options.dir);
|
|
101
|
+
const configPath = path.join(dir, APITOGO_CONFIG_FILENAME);
|
|
102
|
+
|
|
103
|
+
const replacesConfig = await exists(configPath);
|
|
104
|
+
if (replacesConfig && !options.force) {
|
|
105
|
+
throw new InitError(
|
|
106
|
+
`${configPath} already exists.`,
|
|
107
|
+
"Use `apitogo add <service>` to add to it, or pass --force to replace it. Nothing else in the project is changed.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const name = options.name?.trim() || path.basename(dir);
|
|
112
|
+
const config = parseConfigV2({ version: 2, project: { name } });
|
|
113
|
+
const root = layoutRoot(dir);
|
|
114
|
+
const gitignore = path.join(root, ".gitignore");
|
|
115
|
+
|
|
116
|
+
const entries: PlannedEntry[] = [
|
|
117
|
+
{
|
|
118
|
+
kind: "file",
|
|
119
|
+
path: configPath,
|
|
120
|
+
purpose: "config",
|
|
121
|
+
content: `${JSON.stringify(config, null, 2)}\n`,
|
|
122
|
+
},
|
|
123
|
+
{ kind: "file", path: gitignore, purpose: "root", content: ".state/\n" },
|
|
124
|
+
];
|
|
125
|
+
const keep = (await exists(gitignore)) ? [gitignore] : [];
|
|
126
|
+
|
|
127
|
+
return { dir, configPath, config, entries, keep, replacesConfig };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type InitResult = {
|
|
131
|
+
created: string[];
|
|
132
|
+
kept: string[];
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Writes the plan. Existing files are skipped, except the config when the plan replaces it. */
|
|
136
|
+
export async function applyInitPlan(plan: InitPlan): Promise<InitResult> {
|
|
137
|
+
const created: string[] = [];
|
|
138
|
+
const kept: string[] = [];
|
|
139
|
+
|
|
140
|
+
await mkdir(plan.dir, { recursive: true });
|
|
141
|
+
for (const entry of plan.entries) {
|
|
142
|
+
await mkdir(path.dirname(entry.path), { recursive: true });
|
|
143
|
+
try {
|
|
144
|
+
await writeFile(entry.path, entry.content ?? "", {
|
|
145
|
+
flag: entry.purpose === "config" && plan.replacesConfig ? "w" : "wx",
|
|
146
|
+
});
|
|
147
|
+
created.push(entry.path);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
150
|
+
kept.push(entry.path);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { created, kept };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The "will create" list shown before anything is written. */
|
|
158
|
+
export function formatInitPlan(plan: InitPlan): string[] {
|
|
159
|
+
const rel = (target: string) =>
|
|
160
|
+
path.relative(plan.dir, target).split(path.sep).join("/") || ".";
|
|
161
|
+
const lines = [
|
|
162
|
+
"Will create:",
|
|
163
|
+
...plan.entries
|
|
164
|
+
.filter((entry) => !plan.keep.includes(entry.path))
|
|
165
|
+
.map(
|
|
166
|
+
(entry) =>
|
|
167
|
+
` ${rel(entry.path)}${plan.replacesConfig && entry.purpose === "config" ? " (replaces the existing file)" : ""}`,
|
|
168
|
+
),
|
|
169
|
+
];
|
|
170
|
+
if (plan.keep.length > 0) {
|
|
171
|
+
lines.push(
|
|
172
|
+
"Will keep as they are:",
|
|
173
|
+
...plan.keep.map((target) => ` ${rel(target)}`),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
lines.push(
|
|
177
|
+
"Your own files are never moved, renamed or edited, and your .gitignore is left alone.",
|
|
178
|
+
);
|
|
179
|
+
return lines;
|
|
180
|
+
}
|