@lukoweb/apitogo 0.1.148 → 0.2.1
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 +5533 -1850
- package/dist/declarations/cli/common/constants.d.ts +0 -1
- package/dist/declarations/cli/platform/client/platform-api.d.ts +8 -0
- package/dist/declarations/cli/platform/config.d.ts +0 -10
- package/package.json +4 -1
- package/src/app/main.tsx +0 -1
- package/src/config/v2/__fixtures__/scenario-a-docs-only.json +6 -0
- package/src/config/v2/__fixtures__/scenario-b-managed-origin.json +14 -0
- package/src/config/v2/__fixtures__/scenario-c-proxy-oidc.json +15 -0
- package/src/config/v2/__fixtures__/scenario-h-full-stack.json +28 -0
- package/src/config/v2/errors.ts +42 -0
- package/src/config/v2/index.ts +105 -0
- package/src/config/v2/init.ts +273 -0
- package/src/config/v2/layout.ts +237 -0
- package/src/config/v2/ledger.ts +219 -0
- package/src/config/v2/load.ts +70 -0
- package/src/config/v2/manifest.ts +293 -0
- package/src/config/v2/parse.ts +214 -0
- package/src/config/v2/paths.ts +54 -0
- package/src/config/v2/portal-profile.ts +88 -0
- package/src/config/v2/resolve.ts +186 -0
- package/src/config/v2/rules.ts +650 -0
- package/src/config/v2/schema.ts +211 -0
- package/src/config/v2/services.ts +644 -0
- package/src/config/v2/workspace.ts +117 -0
- package/src/config/v2/wrapper.ts +397 -0
- package/src/vite/html.ts +0 -2
- package/src/vite/plugin-theme.ts +23 -7
- package/src/app/font.geist.css +0 -73
|
@@ -3,4 +3,3 @@ export declare const VERSION_CHECK_FILE = "version.json";
|
|
|
3
3
|
export declare const SENTRY_DSN: undefined;
|
|
4
4
|
export declare const MAX_WAIT_PENDING_TIME_MS = 1000;
|
|
5
5
|
export declare const POST_HOG_CAPTURE_KEY: undefined;
|
|
6
|
-
export declare const DEPLOY_SERVER_API_KEY = "qRVLTP22TFStdN6";
|
|
@@ -318,6 +318,13 @@ export interface PageFileDto {
|
|
|
318
318
|
isFile: boolean;
|
|
319
319
|
order: number;
|
|
320
320
|
}
|
|
321
|
+
export interface StaticWebSiteDeploymentDto {
|
|
322
|
+
projectId: string;
|
|
323
|
+
environment: string;
|
|
324
|
+
byteSize: number;
|
|
325
|
+
deployServerStatus: number;
|
|
326
|
+
message?: string | null;
|
|
327
|
+
}
|
|
321
328
|
export interface DeployConfigDto {
|
|
322
329
|
envVars: Record<string, string>;
|
|
323
330
|
dockerRun: string;
|
|
@@ -380,6 +387,7 @@ export declare class PlatformApiClient {
|
|
|
380
387
|
getAzureInfrastructureDeployStatus(projectId: string, jobId: string): Promise<DeployStatusResponse>;
|
|
381
388
|
listProvisioningJobs(projectId: string): Promise<ProvisioningJobListResponse>;
|
|
382
389
|
getDeploymentToken(projectId: string, regenerate?: boolean): Promise<DeploymentTokenResponse>;
|
|
390
|
+
uploadStaticWebDeployment(projectId: string, archivePath: string, env?: string): Promise<StaticWebSiteDeploymentDto>;
|
|
383
391
|
getCustomDomain(projectId: string): Promise<UserCustomDomainResponse>;
|
|
384
392
|
setCustomDomain(projectId: string, body: SetUserCustomDomainRequest): Promise<UserCustomDomainResponse>;
|
|
385
393
|
deleteCustomDomain(projectId: string): Promise<void>;
|
|
@@ -3,19 +3,9 @@ export interface ApitogoConfig {
|
|
|
3
3
|
apiKeyId: string;
|
|
4
4
|
apiSecret: string;
|
|
5
5
|
defaultProjectId?: string;
|
|
6
|
-
devPortalDeployUrl: string;
|
|
7
|
-
devPortalDeployApiKey: string;
|
|
8
|
-
devPortalDeployEnv: string;
|
|
9
6
|
docToolPath?: string;
|
|
10
7
|
createApitogoPackage: string;
|
|
11
8
|
}
|
|
12
|
-
export interface DevPortalDeployConfig {
|
|
13
|
-
endpointUrl: string;
|
|
14
|
-
apiKey: string;
|
|
15
|
-
defaultEnv: string;
|
|
16
|
-
}
|
|
17
9
|
export declare const DEFAULT_API_URL = "https://api.apitogo.com";
|
|
18
10
|
export declare function loadConfig(): ApitogoConfig;
|
|
19
11
|
export declare function resolveProjectId(explicit?: string, config?: ApitogoConfig): string;
|
|
20
|
-
export declare function resolveDevPortalDeployConfig(config: ApitogoConfig): DevPortalDeployConfig;
|
|
21
|
-
export declare const resolveDocDeployConfig: typeof resolveDevPortalDeployConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lukoweb/apitogo",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css",
|
|
@@ -181,6 +181,9 @@
|
|
|
181
181
|
"@apidevtools/json-schema-ref-parser": "15.3.1",
|
|
182
182
|
"@apidevtools/swagger-parser": "^12.1.0",
|
|
183
183
|
"@envelop/core": "5.5.1",
|
|
184
|
+
"@fontsource/ibm-plex-sans": "^5.3.0",
|
|
185
|
+
"@fontsource/jetbrains-mono": "^5.3.0",
|
|
186
|
+
"@fontsource/space-grotesk": "^5.3.0",
|
|
184
187
|
"@graphql-typed-document-node/core": "3.2.0",
|
|
185
188
|
"@hono/node-server": "1.19.11",
|
|
186
189
|
"@lekoarts/rehype-meta-as-attributes": "3.0.3",
|
package/src/app/main.tsx
CHANGED
|
@@ -65,7 +65,6 @@ export const convertApitogoConfigToOptions = (
|
|
|
65
65
|
},
|
|
66
66
|
slots: { ...configuredModulesSlots, ...config.slots },
|
|
67
67
|
metadata: {
|
|
68
|
-
favicon: "https://cdn.zudoku.dev/logos/favicon.svg",
|
|
69
68
|
applicationName: siteTitle,
|
|
70
69
|
defaultTitle: siteTitle,
|
|
71
70
|
title: `%s - ${siteTitle}`,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"layout": "embedded",
|
|
4
|
+
"gateway": {
|
|
5
|
+
"origin": {
|
|
6
|
+
"mode": "managed",
|
|
7
|
+
"source": ".",
|
|
8
|
+
"runtime": "nodejs",
|
|
9
|
+
"runtimeVersion": "NODE_22_LTS",
|
|
10
|
+
"exclude": ["apitogo/**", "node_modules/**"]
|
|
11
|
+
},
|
|
12
|
+
"specs": ["openapi.yaml"]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"layout": "standalone",
|
|
4
|
+
"gateway": {
|
|
5
|
+
"origin": { "mode": "proxy", "url": "https://api.acme.com" },
|
|
6
|
+
"specs": ["apis/openapi.yaml"],
|
|
7
|
+
"auth": {
|
|
8
|
+
"mode": "oidc",
|
|
9
|
+
"metadataUrl": "https://idp.acme.com/.well-known/openid-configuration",
|
|
10
|
+
"issuers": ["https://idp.acme.com/"],
|
|
11
|
+
"audiences": ["api://acme-orders"]
|
|
12
|
+
},
|
|
13
|
+
"policies": { "rateLimit": { "calls": 60, "periodSeconds": 60 } }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://apitogo.com/schema/config-v2.json",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"layout": "embedded",
|
|
5
|
+
"project": { "id": "prj_123", "name": "acme-orders" },
|
|
6
|
+
"gateway": {
|
|
7
|
+
"origin": {
|
|
8
|
+
"mode": "managed",
|
|
9
|
+
"source": "server",
|
|
10
|
+
"runtime": "nodejs",
|
|
11
|
+
"runtimeVersion": "NODE_22_LTS"
|
|
12
|
+
},
|
|
13
|
+
"specs": ["server/docs/swagger.yaml"],
|
|
14
|
+
"auth": { "mode": "identity" }
|
|
15
|
+
},
|
|
16
|
+
"identity": { "native": true },
|
|
17
|
+
"plans": {
|
|
18
|
+
"billing": {
|
|
19
|
+
"provider": "stripe",
|
|
20
|
+
"apiKey": "secret:STRIPE_API_KEY",
|
|
21
|
+
"webhookSecret": "secret:STRIPE_WEBHOOK_SECRET"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"portal": {
|
|
25
|
+
"name": "Acme Orders",
|
|
26
|
+
"docs": { "content": ["docs/**/*.{md,mdx}"] }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ApitogoError } from "../../lib/util/invariant.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One problem in a config file. `code` is stable and safe to match on, `path` is the dotted config path
|
|
5
|
+
* (for example `gateway.origin.mode`), `hint` says how to fix it.
|
|
6
|
+
*/
|
|
7
|
+
export type ConfigIssue = {
|
|
8
|
+
code: string;
|
|
9
|
+
path: string;
|
|
10
|
+
message: string;
|
|
11
|
+
hint?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function formatConfigIssue(issue: ConfigIssue): string {
|
|
15
|
+
const hint = issue.hint ? ` ${issue.hint}` : "";
|
|
16
|
+
const rest = issue.message.slice(issue.path.length);
|
|
17
|
+
// Several documented messages already begin with their own path; do not print it twice.
|
|
18
|
+
const startsWithPath =
|
|
19
|
+
issue.path !== "" &&
|
|
20
|
+
issue.message.startsWith(issue.path) &&
|
|
21
|
+
(rest === "" || /^[\s:]/.test(rest));
|
|
22
|
+
const text = startsWithPath
|
|
23
|
+
? issue.message
|
|
24
|
+
: `${issue.path === "" ? "(root)" : issue.path}: ${issue.message}`;
|
|
25
|
+
return `${text}${hint} [${issue.code}]`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Thrown when an `apitogo.config.json` cannot be loaded. Carries every problem found, not just the first. */
|
|
29
|
+
export class ConfigError extends ApitogoError {
|
|
30
|
+
public readonly issues: readonly ConfigIssue[];
|
|
31
|
+
|
|
32
|
+
constructor(file: string, issues: readonly ConfigIssue[]) {
|
|
33
|
+
super(
|
|
34
|
+
`${file} has ${issues.length} problem${issues.length === 1 ? "" : "s"}:\n${issues
|
|
35
|
+
.map((issue) => ` - ${formatConfigIssue(issue)}`)
|
|
36
|
+
.join("\n")}`,
|
|
37
|
+
{ title: "Invalid apitogo config" },
|
|
38
|
+
);
|
|
39
|
+
this.name = "ConfigError";
|
|
40
|
+
this.issues = issues;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export { ConfigError, type ConfigIssue, formatConfigIssue } from "./errors.js";
|
|
2
|
+
export {
|
|
3
|
+
applyInitPlan,
|
|
4
|
+
formatInitPlan,
|
|
5
|
+
InitError,
|
|
6
|
+
type InitOptions,
|
|
7
|
+
type InitPlan,
|
|
8
|
+
type InitResult,
|
|
9
|
+
type PlannedEntry,
|
|
10
|
+
planInit,
|
|
11
|
+
} from "./init.js";
|
|
12
|
+
export {
|
|
13
|
+
type DocsCandidate,
|
|
14
|
+
detectExistingDocs,
|
|
15
|
+
detectLayout,
|
|
16
|
+
ensureStateDir,
|
|
17
|
+
type LayoutDetection,
|
|
18
|
+
} from "./layout.js";
|
|
19
|
+
export { type LoadedConfigV2, loadConfigV2 } from "./load.js";
|
|
20
|
+
export {
|
|
21
|
+
BUILD_DIR,
|
|
22
|
+
GENERATED_DIR,
|
|
23
|
+
layoutRoot,
|
|
24
|
+
STATE_FILE,
|
|
25
|
+
type StatePaths,
|
|
26
|
+
stateDirFor,
|
|
27
|
+
statePathsFor,
|
|
28
|
+
} from "./paths.js";
|
|
29
|
+
export { parseConfigV2 } from "./parse.js";
|
|
30
|
+
export {
|
|
31
|
+
type CheckContext,
|
|
32
|
+
type CheckReport,
|
|
33
|
+
checkConfigV2,
|
|
34
|
+
collectSecretRefs,
|
|
35
|
+
} from "./rules.js";
|
|
36
|
+
export {
|
|
37
|
+
type ResolvedConfigV2,
|
|
38
|
+
type ResolvedGateway,
|
|
39
|
+
type ResolvedIdentity,
|
|
40
|
+
type ResolvedPlans,
|
|
41
|
+
type ResolvedPortal,
|
|
42
|
+
resolveConfigV2,
|
|
43
|
+
} from "./resolve.js";
|
|
44
|
+
export {
|
|
45
|
+
CONFIG_VERSION,
|
|
46
|
+
type ConfigV2,
|
|
47
|
+
ConfigV2Schema,
|
|
48
|
+
type Layout,
|
|
49
|
+
} from "./schema.js";
|
|
50
|
+
export { type PortalProfile, portalProfile } from "./portal-profile.js";
|
|
51
|
+
export {
|
|
52
|
+
hasCapability,
|
|
53
|
+
type ManifestServiceName,
|
|
54
|
+
missingManifestServices,
|
|
55
|
+
parseManifest,
|
|
56
|
+
readManifest,
|
|
57
|
+
requiredServices,
|
|
58
|
+
SUPPORTED_MANIFEST_SCHEMA,
|
|
59
|
+
type ServiceManifest,
|
|
60
|
+
serviceReady,
|
|
61
|
+
stubManifest,
|
|
62
|
+
withPortalEntry,
|
|
63
|
+
writeManifest,
|
|
64
|
+
} from "./manifest.js";
|
|
65
|
+
export { MANIFEST_FILE, manifestPathFor } from "./paths.js";
|
|
66
|
+
export {
|
|
67
|
+
consoleLinks,
|
|
68
|
+
planPortal,
|
|
69
|
+
publishedManifest,
|
|
70
|
+
type PortalPlan,
|
|
71
|
+
type PreparedPortal,
|
|
72
|
+
type PrepareMode,
|
|
73
|
+
preparePortal,
|
|
74
|
+
renderWrapper,
|
|
75
|
+
requiredPackages,
|
|
76
|
+
WRAPPER_FILE,
|
|
77
|
+
} from "./wrapper.js";
|
|
78
|
+
export { materializeWorkspace, portalArtifactHash } from "./workspace.js";
|
|
79
|
+
export {
|
|
80
|
+
type AddOptions,
|
|
81
|
+
applyServiceEdit,
|
|
82
|
+
formatServiceEdit,
|
|
83
|
+
isServiceName,
|
|
84
|
+
planAddService,
|
|
85
|
+
planRemoveService,
|
|
86
|
+
SERVICES,
|
|
87
|
+
ServiceEditError,
|
|
88
|
+
setProjectId,
|
|
89
|
+
type ServiceEdit,
|
|
90
|
+
type ServiceName,
|
|
91
|
+
} from "./services.js";
|
|
92
|
+
export { checkDependencies } from "./rules.js";
|
|
93
|
+
export {
|
|
94
|
+
emptyLedger,
|
|
95
|
+
type Ledger,
|
|
96
|
+
LEDGER_VERSION,
|
|
97
|
+
LedgerError,
|
|
98
|
+
type PipelineStep,
|
|
99
|
+
readLedger,
|
|
100
|
+
runPipeline,
|
|
101
|
+
type StepOutcome,
|
|
102
|
+
type StepRecord,
|
|
103
|
+
stepOf,
|
|
104
|
+
writeLedger,
|
|
105
|
+
} from "./ledger.js";
|
|
@@ -0,0 +1,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 {
|
|
6
|
+
type DocsCandidate,
|
|
7
|
+
detectExistingDocs,
|
|
8
|
+
detectLayout,
|
|
9
|
+
ensureStateDir,
|
|
10
|
+
} from "./layout.js";
|
|
11
|
+
import { parseConfigV2 } from "./parse.js";
|
|
12
|
+
import { layoutRoot, statePathsFor } from "./paths.js";
|
|
13
|
+
import type { ConfigV2, Layout } from "./schema.js";
|
|
14
|
+
|
|
15
|
+
export class InitError extends ApitogoError {
|
|
16
|
+
constructor(message: string, developerHint?: string) {
|
|
17
|
+
super(message, { developerHint, title: "Cannot initialise the project" });
|
|
18
|
+
this.name = "InitError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type InitOptions = {
|
|
23
|
+
/** Project directory; created if it does not exist. */
|
|
24
|
+
dir: string;
|
|
25
|
+
/** Overrides layout detection. */
|
|
26
|
+
layout?: Layout;
|
|
27
|
+
/** Project and portal name; defaults to the directory name. */
|
|
28
|
+
name?: string;
|
|
29
|
+
/** Existing docs to include, as paths relative to `dir`; `false` skips docs detection. */
|
|
30
|
+
docs?: string[] | false;
|
|
31
|
+
/** When several docs candidates exist, include all of them instead of asking. */
|
|
32
|
+
acceptAllDocs?: boolean;
|
|
33
|
+
/** Replace an existing `apitogo.config.json`. It is the only existing file `init` ever replaces. */
|
|
34
|
+
force?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type PlannedEntry = {
|
|
38
|
+
kind: "dir" | "file";
|
|
39
|
+
/** Absolute path. */
|
|
40
|
+
path: string;
|
|
41
|
+
/** `state` entries are created by {@link ensureStateDir}. */
|
|
42
|
+
purpose: "config" | "state" | "portal";
|
|
43
|
+
content?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type InitPlan = {
|
|
47
|
+
dir: string;
|
|
48
|
+
configPath: string;
|
|
49
|
+
layout: Layout;
|
|
50
|
+
layoutSource: "detected" | "flag";
|
|
51
|
+
layoutReason: string;
|
|
52
|
+
/** The config that will be written; already validated against the v2 schema. */
|
|
53
|
+
config: ConfigV2;
|
|
54
|
+
docsCandidates: DocsCandidate[];
|
|
55
|
+
/** Globs written to `portal.docs.content`. */
|
|
56
|
+
docsIncluded: string[];
|
|
57
|
+
/** Several candidates were found and none was chosen yet: the caller should ask and plan again. */
|
|
58
|
+
needsDocsChoice: boolean;
|
|
59
|
+
/** Everything `init` will create. */
|
|
60
|
+
entries: PlannedEntry[];
|
|
61
|
+
/** Files that already exist and stay untouched. */
|
|
62
|
+
keep: string[];
|
|
63
|
+
/** An existing config will be replaced (only with `force`). */
|
|
64
|
+
replacesConfig: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
async function exists(target: string): Promise<boolean> {
|
|
68
|
+
try {
|
|
69
|
+
await access(target);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function starterPage(name: string): string {
|
|
77
|
+
return `---
|
|
78
|
+
title: Welcome
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# ${name}
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Turns paths the user named into globs, checking that each exists inside the project. */
|
|
88
|
+
export async function docsFromPaths(
|
|
89
|
+
dir: string,
|
|
90
|
+
requested: readonly string[],
|
|
91
|
+
): Promise<string[]> {
|
|
92
|
+
const globs: string[] = [];
|
|
93
|
+
for (const requestedPath of requested) {
|
|
94
|
+
const absolute = path.resolve(dir, requestedPath);
|
|
95
|
+
const relative = path.relative(dir, absolute).split(path.sep).join("/");
|
|
96
|
+
if (
|
|
97
|
+
relative === "" ||
|
|
98
|
+
relative.startsWith("..") ||
|
|
99
|
+
path.isAbsolute(relative)
|
|
100
|
+
) {
|
|
101
|
+
throw new InitError(
|
|
102
|
+
`Docs path "${requestedPath}" is outside the project directory.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const info = await stat(absolute).catch(() => undefined);
|
|
106
|
+
if (!info)
|
|
107
|
+
throw new InitError(
|
|
108
|
+
`Docs path "${requestedPath}" does not exist in ${dir}.`,
|
|
109
|
+
);
|
|
110
|
+
globs.push(info.isDirectory() ? `${relative}/**/*.{md,mdx}` : relative);
|
|
111
|
+
}
|
|
112
|
+
return globs;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Decides what `init` would do without touching the disk (Reference, section 1.2). Existing files are never moved,
|
|
117
|
+
* renamed or modified; existing docs are referenced in place; the root `.gitignore` is never edited.
|
|
118
|
+
*/
|
|
119
|
+
export async function planInit(options: InitOptions): Promise<InitPlan> {
|
|
120
|
+
const dir = path.resolve(options.dir);
|
|
121
|
+
const configPath = path.join(dir, APITOGO_CONFIG_FILENAME);
|
|
122
|
+
|
|
123
|
+
const replacesConfig = await exists(configPath);
|
|
124
|
+
if (replacesConfig && !options.force) {
|
|
125
|
+
throw new InitError(
|
|
126
|
+
`${configPath} already exists.`,
|
|
127
|
+
"Pass --force to replace it. Nothing else in the project is changed.",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const detection = options.layout ? undefined : await detectLayout(dir);
|
|
132
|
+
const layout = options.layout ?? detection?.layout ?? "standalone";
|
|
133
|
+
const layoutReason = options.layout
|
|
134
|
+
? `chosen with --layout ${options.layout}`
|
|
135
|
+
: (detection?.reason ?? "");
|
|
136
|
+
|
|
137
|
+
let docsCandidates: DocsCandidate[] = [];
|
|
138
|
+
let docsIncluded: string[] = [];
|
|
139
|
+
let needsDocsChoice = false;
|
|
140
|
+
if (Array.isArray(options.docs) && options.docs.length > 0) {
|
|
141
|
+
docsIncluded = await docsFromPaths(dir, options.docs);
|
|
142
|
+
} else if (options.docs !== false) {
|
|
143
|
+
docsCandidates = await detectExistingDocs(dir);
|
|
144
|
+
if (
|
|
145
|
+
docsCandidates.length === 1 ||
|
|
146
|
+
(docsCandidates.length > 1 && options.acceptAllDocs)
|
|
147
|
+
) {
|
|
148
|
+
docsIncluded = docsCandidates.map((candidate) => candidate.glob);
|
|
149
|
+
} else if (docsCandidates.length > 1) {
|
|
150
|
+
needsDocsChoice = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const name = options.name?.trim() || path.basename(dir);
|
|
155
|
+
const config = parseConfigV2({
|
|
156
|
+
version: 2,
|
|
157
|
+
layout,
|
|
158
|
+
project: { name },
|
|
159
|
+
portal: {
|
|
160
|
+
name,
|
|
161
|
+
...(docsIncluded.length > 0 ? { docs: { content: docsIncluded } } : {}),
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const root = layoutRoot(dir, layout);
|
|
166
|
+
const state = statePathsFor(dir, layout);
|
|
167
|
+
const entries: PlannedEntry[] = [
|
|
168
|
+
{
|
|
169
|
+
kind: "file",
|
|
170
|
+
path: configPath,
|
|
171
|
+
purpose: "config",
|
|
172
|
+
content: `${JSON.stringify(config, null, 2)}\n`,
|
|
173
|
+
},
|
|
174
|
+
{ kind: "file", path: state.gitignore, purpose: "state", content: "*\n" },
|
|
175
|
+
{ kind: "dir", path: state.generatedDir, purpose: "state" },
|
|
176
|
+
{ kind: "dir", path: state.buildDir, purpose: "state" },
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const keep: string[] = [];
|
|
180
|
+
if (docsIncluded.length === 0) {
|
|
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
|
+
}
|