@portosaur/cli 0.1.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 +52 -0
- package/bin/porto.mjs +71 -0
- package/package.json +36 -0
- package/src/commands/build.mjs +85 -0
- package/src/commands/dev.mjs +61 -0
- package/src/commands/init.mjs +523 -0
- package/src/commands/initCi.mjs +227 -0
- package/src/commands/providers.mjs +170 -0
- package/src/commands/schema.mjs +208 -0
- package/src/commands/serve.mjs +29 -0
- package/src/index.d.ts +49 -0
- package/src/index.mjs +8 -0
- package/src/templates/README.md +58 -0
- package/src/templates/blog/authors.yml +4 -0
- package/src/templates/blog/welcome.md +11 -0
- package/src/templates/config.yml +150 -0
- package/src/templates/gitignore +9 -0
- package/src/templates/notes/index.mdx +9 -0
- package/src/templates/notes/welcome.mdx +9 -0
- package/src/templates/package.json +14 -0
- package/src/templates/registry.yml +107 -0
- package/src/templates/static/.nojekyll +0 -0
- package/src/templates/static/README.md +1 -0
- package/src/templates/workflows/codeberg/.forgejo/workflows/deploy.yml +39 -0
- package/src/templates/workflows/github/.github/workflows/deploy.yml +55 -0
- package/src/templates/workflows/gitlab/.gitlab-ci.yml +13 -0
- package/src/templates/workflows/netlify/netlify.toml +6 -0
- package/src/templates/workflows/surge/codeberg/.forgejo/workflows/deploy.yml +23 -0
- package/src/templates/workflows/surge/github/.github/workflows/deploy.yml +23 -0
- package/src/templates/workflows/surge/gitlab/.gitlab-ci.yml +16 -0
- package/src/templates/workflows/surge/sourcehut/.build.yml +26 -0
- package/src/templates/workflows/woodpecker/.woodpecker/deploy.yml +21 -0
- package/src/utils/git.mjs +52 -0
- package/src/utils/index.mjs +7 -0
- package/src/utils/interaction.mjs +24 -0
- package/src/utils/packageManager.mjs +85 -0
- package/src/utils/paths.mjs +33 -0
- package/src/utils/platforms.mjs +130 -0
- package/src/utils/projectName.mjs +20 -0
- package/src/utils/runner.mjs +192 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
|
|
5
|
+
import { runWizard, cancel } from "@portosaur/wizard";
|
|
6
|
+
|
|
7
|
+
import { logger, colors } from "@portosaur/logger";
|
|
8
|
+
import { mirrorSync, porto } from "@portosaur/core";
|
|
9
|
+
import {
|
|
10
|
+
Paths,
|
|
11
|
+
detectVcsProvider,
|
|
12
|
+
getGitConfig,
|
|
13
|
+
getWorkflowMarkers,
|
|
14
|
+
resolvePlatformKey,
|
|
15
|
+
getPlatformUserGuess,
|
|
16
|
+
validateProject,
|
|
17
|
+
printWorkflowTips,
|
|
18
|
+
warnIfRepoNameMismatch,
|
|
19
|
+
isInteractive as getInteractivity,
|
|
20
|
+
looksLikeTestProject,
|
|
21
|
+
} from "../utils/index.mjs";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initializes CI/CD configuration for an existing Portosaur project.
|
|
25
|
+
*/
|
|
26
|
+
export async function initCiCommand(options = {}) {
|
|
27
|
+
//
|
|
28
|
+
//====================== Constants ======================
|
|
29
|
+
|
|
30
|
+
const projectDir = process.cwd();
|
|
31
|
+
|
|
32
|
+
const registry = yaml.load(fs.readFileSync(Paths.registry, "utf8"));
|
|
33
|
+
|
|
34
|
+
const gitConfig = getGitConfig();
|
|
35
|
+
const vcsProviderId = detectVcsProvider(registry);
|
|
36
|
+
|
|
37
|
+
const isInteractive = getInteractivity(options);
|
|
38
|
+
|
|
39
|
+
// Validate project existence
|
|
40
|
+
try {
|
|
41
|
+
validateProject(projectDir);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.error(error.message);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Detect existing CI configurations based on workflow templates
|
|
48
|
+
const ciMarkers = getWorkflowMarkers(registry);
|
|
49
|
+
const detectedMarkers = ciMarkers.filter((marker) =>
|
|
50
|
+
fs.existsSync(path.join(projectDir, marker)),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let state = {
|
|
54
|
+
hostingPlatform: options.hosting || null,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* ====================== Interactive mode ======================
|
|
59
|
+
*/
|
|
60
|
+
if (isInteractive) {
|
|
61
|
+
const vcs = registry.vcs_providers[vcsProviderId];
|
|
62
|
+
|
|
63
|
+
const wizardState = await runWizard({
|
|
64
|
+
initialState: state,
|
|
65
|
+
|
|
66
|
+
intro: "Setting up Portosaur CI/CD",
|
|
67
|
+
outro: false,
|
|
68
|
+
|
|
69
|
+
steps: [
|
|
70
|
+
{
|
|
71
|
+
id: "overwrite",
|
|
72
|
+
type: "confirm",
|
|
73
|
+
prompt: "Existing CI configuration detected. Overwrite?",
|
|
74
|
+
hint: `Detected: ${detectedMarkers.join(", ")}`,
|
|
75
|
+
runIf: () => detectedMarkers.length > 0,
|
|
76
|
+
initialValue: false,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "hostingPlatform",
|
|
80
|
+
type: "select",
|
|
81
|
+
prompt: "Select target hosting platform",
|
|
82
|
+
hint: "The service where your portfolio will be deployed",
|
|
83
|
+
options: Object.keys(registry.hosting_platforms).map((key) => ({
|
|
84
|
+
value: key,
|
|
85
|
+
label: registry.hosting_platforms[key].name,
|
|
86
|
+
hint: key === vcs?.default_hosting ? "recommended" : "",
|
|
87
|
+
})),
|
|
88
|
+
initialValue: () => vcs?.default_hosting || "none",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "confirm",
|
|
92
|
+
type: "confirm",
|
|
93
|
+
prompt: "Are you sure you want to proceed?",
|
|
94
|
+
hint: (state) =>
|
|
95
|
+
state.overwrite
|
|
96
|
+
? "This will DELETE existing CI files and create new ones"
|
|
97
|
+
: "This will create CI configuration files",
|
|
98
|
+
initialValue: true,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
!wizardState.confirm ||
|
|
105
|
+
(detectedMarkers.length > 0 && wizardState.overwrite === false)
|
|
106
|
+
) {
|
|
107
|
+
cancel("Setup cancelled.");
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
state = { ...state, ...wizardState };
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* ====================== Non-Interactive Mode ======================
|
|
115
|
+
*/
|
|
116
|
+
} else {
|
|
117
|
+
//
|
|
118
|
+
// Normalize hosting platform
|
|
119
|
+
if (state.hostingPlatform && state.hostingPlatform !== "none") {
|
|
120
|
+
state.hostingPlatform =
|
|
121
|
+
resolvePlatformKey(registry.hosting_platforms, state.hostingPlatform) ||
|
|
122
|
+
state.hostingPlatform;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const vcs = registry.vcs_providers[vcsProviderId];
|
|
126
|
+
|
|
127
|
+
state.hostingPlatform =
|
|
128
|
+
state.hostingPlatform || vcs?.default_hosting || "none";
|
|
129
|
+
|
|
130
|
+
if (state.hostingPlatform !== "none") {
|
|
131
|
+
logger.info(
|
|
132
|
+
`Detected ${vcs?.name || vcsProviderId} provider, defaulting to ${state.hostingPlatform}.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
* ====================== Execution ======================
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
// Final canonical resolution
|
|
142
|
+
const resolved = resolvePlatformKey(
|
|
143
|
+
registry.hosting_platforms,
|
|
144
|
+
state.hostingPlatform,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (!resolved || resolved === "none") {
|
|
148
|
+
logger.error(
|
|
149
|
+
`Unknown or invalid hosting platform: ${state.hostingPlatform || "none"}`,
|
|
150
|
+
);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
state.hostingPlatform = resolved;
|
|
155
|
+
|
|
156
|
+
//--------- Clear the console ----------------
|
|
157
|
+
logger.resetView();
|
|
158
|
+
logger.info.box("Portosaur CI/CD Setup", {
|
|
159
|
+
title: ` v${porto.version} `,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Gather variables for template replacement
|
|
163
|
+
const isTestProject = looksLikeTestProject(path.basename(projectDir));
|
|
164
|
+
const portoVer = isTestProject ? "link:portosaur" : porto.version || "0.0.0";
|
|
165
|
+
|
|
166
|
+
const userName = getPlatformUserGuess(vcsProviderId, gitConfig) || "user";
|
|
167
|
+
const fullName = gitConfig["user.name"] || "User";
|
|
168
|
+
|
|
169
|
+
const templateVars = {
|
|
170
|
+
projectName: path.basename(projectDir),
|
|
171
|
+
userName,
|
|
172
|
+
fullName,
|
|
173
|
+
portoVer,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const hConfig = registry.hosting_platforms[state.hostingPlatform];
|
|
177
|
+
|
|
178
|
+
logger.info(`Configuring CI/CD for ${hConfig.name}...`);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
let template = hConfig.template_dir;
|
|
182
|
+
if (typeof template === "object") {
|
|
183
|
+
template = template[vcsProviderId] || Object.values(template)[0];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const workflowTemplateDir = path.join(Paths.workflows, template);
|
|
187
|
+
|
|
188
|
+
if (!fs.existsSync(workflowTemplateDir)) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Platform template for "${state.hostingPlatform}" not found.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Clean up existing CI files if requested
|
|
195
|
+
if (state.overwrite) {
|
|
196
|
+
logger.info("Cleaning up existing CI configuration...");
|
|
197
|
+
|
|
198
|
+
for (const marker of detectedMarkers) {
|
|
199
|
+
const fullPath = path.join(projectDir, marker);
|
|
200
|
+
if (fs.existsSync(fullPath)) {
|
|
201
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
mirrorSync(workflowTemplateDir, projectDir, templateVars, []);
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* ====================== Final Output ======================
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
logger.resetView();
|
|
213
|
+
logger.success.box(`CI/CD successfully configured!`);
|
|
214
|
+
|
|
215
|
+
printWorkflowTips(
|
|
216
|
+
registry.hosting_platforms,
|
|
217
|
+
state.hostingPlatform,
|
|
218
|
+
logger,
|
|
219
|
+
templateVars,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
logger.newLine();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logger.error(`Configuration failed: ${error.message}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
import { colors } from "@portosaur/logger";
|
|
4
|
+
import { Paths } from "../utils/index.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Displays available VCS providers and hosting platforms.
|
|
8
|
+
*/
|
|
9
|
+
export async function providersCommand(subcommand = null) {
|
|
10
|
+
const registry = yaml.load(readFileSync(Paths.registry, "utf8"));
|
|
11
|
+
|
|
12
|
+
const vcsProviders = registry.vcs_providers || {};
|
|
13
|
+
const hostingPlatforms = registry.hosting_platforms || {};
|
|
14
|
+
|
|
15
|
+
switch (subcommand) {
|
|
16
|
+
case "vcs":
|
|
17
|
+
displayVcsProviders(vcsProviders);
|
|
18
|
+
break;
|
|
19
|
+
|
|
20
|
+
case "hosting":
|
|
21
|
+
displayHostingPlatforms(hostingPlatforms, vcsProviders);
|
|
22
|
+
break;
|
|
23
|
+
|
|
24
|
+
default:
|
|
25
|
+
displayAll(vcsProviders, hostingPlatforms);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format template variables for display (e.g., {{user}} -> <username>).
|
|
31
|
+
*/
|
|
32
|
+
function formatTemplateVars(str) {
|
|
33
|
+
if (!str) return str;
|
|
34
|
+
return str.replace(/\{\{(\w+)\}\}/g, "<$1>");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Strip ANSI color codes to get the actual display width.
|
|
39
|
+
*/
|
|
40
|
+
function stripAnsi(str) {
|
|
41
|
+
return String(str).replace(/\x1b\[[0-9;]*m/g, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a simple ASCII table.
|
|
46
|
+
*/
|
|
47
|
+
function createTable(rows, columns) {
|
|
48
|
+
const colWidths = columns.map((col) => col.length);
|
|
49
|
+
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
for (let i = 0; i < columns.length; i++) {
|
|
52
|
+
const val = stripAnsi(row[i] || "");
|
|
53
|
+
colWidths[i] = Math.max(colWidths[i], val.length);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const lines = [];
|
|
58
|
+
|
|
59
|
+
// Header
|
|
60
|
+
const headerLine = columns
|
|
61
|
+
.map((col, i) => colors.bold(col.padEnd(colWidths[i])))
|
|
62
|
+
.join(" ");
|
|
63
|
+
lines.push(headerLine);
|
|
64
|
+
|
|
65
|
+
// Separator
|
|
66
|
+
const sepLine = colWidths.map((w) => "─".repeat(w)).join("──");
|
|
67
|
+
lines.push(colors.dim(sepLine));
|
|
68
|
+
|
|
69
|
+
// Rows
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
const line = row
|
|
72
|
+
.map((val, i) => {
|
|
73
|
+
const plain = stripAnsi(val || "");
|
|
74
|
+
const padded = plain.padEnd(colWidths[i]);
|
|
75
|
+
// If the value is colored, we need to apply color to the padded version
|
|
76
|
+
const colored = val || "";
|
|
77
|
+
if (stripAnsi(colored) !== colored) {
|
|
78
|
+
// Has color codes - reapply coloring after padding
|
|
79
|
+
const colorCodes = colored.match(/\x1b\[[0-9;]*m/g) || [];
|
|
80
|
+
const resetCode = "\x1b[0m";
|
|
81
|
+
return colored + resetCode + padded.slice(plain.length);
|
|
82
|
+
}
|
|
83
|
+
return padded;
|
|
84
|
+
})
|
|
85
|
+
.join(" ");
|
|
86
|
+
lines.push(line);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Display all providers and platforms.
|
|
94
|
+
*/
|
|
95
|
+
function displayAll(vcsProviders, hostingPlatforms) {
|
|
96
|
+
console.log("📦 " + colors.bold("Available Providers"));
|
|
97
|
+
console.log("");
|
|
98
|
+
|
|
99
|
+
displayVcsProviders(vcsProviders);
|
|
100
|
+
console.log("");
|
|
101
|
+
|
|
102
|
+
displayHostingPlatforms(hostingPlatforms, vcsProviders);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Display VCS providers.
|
|
107
|
+
*/
|
|
108
|
+
function displayVcsProviders(vcsProviders) {
|
|
109
|
+
console.log(colors.bold("VCS Providers:"));
|
|
110
|
+
console.log("");
|
|
111
|
+
|
|
112
|
+
const rows = Object.entries(vcsProviders).map(([id, config]) => [
|
|
113
|
+
colors.cyan(id),
|
|
114
|
+
config.name || "N/A",
|
|
115
|
+
config.domain || "N/A",
|
|
116
|
+
config.default_hosting || "N/A",
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
console.log(createTable(rows, ["ID", "Name", "Domain", "Default Hosting"]));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Display hosting platforms with supported providers.
|
|
124
|
+
*/
|
|
125
|
+
function displayHostingPlatforms(hostingPlatforms, vcsProviders) {
|
|
126
|
+
console.log(colors.bold("Hosting Platforms:"));
|
|
127
|
+
console.log("");
|
|
128
|
+
|
|
129
|
+
const rows = Object.entries(hostingPlatforms).map(([id, config]) => {
|
|
130
|
+
const supportedProviders = getSupportedProviders(config, vcsProviders);
|
|
131
|
+
const idealName = config.repo?.ideal_name
|
|
132
|
+
? formatTemplateVars(config.repo.ideal_name)
|
|
133
|
+
: "N/A";
|
|
134
|
+
|
|
135
|
+
return [
|
|
136
|
+
colors.cyan(id),
|
|
137
|
+
config.name,
|
|
138
|
+
config.domain || "N/A",
|
|
139
|
+
supportedProviders.join(", "),
|
|
140
|
+
idealName,
|
|
141
|
+
];
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
console.log(
|
|
145
|
+
createTable(rows, [
|
|
146
|
+
"ID",
|
|
147
|
+
"Name",
|
|
148
|
+
"Domain",
|
|
149
|
+
"Supported Providers",
|
|
150
|
+
"Ideal Repo Name",
|
|
151
|
+
]),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get supported providers for a hosting platform.
|
|
157
|
+
*/
|
|
158
|
+
function getSupportedProviders(platform, vcsProviders) {
|
|
159
|
+
const supportedProviders = platform.supported_providers;
|
|
160
|
+
|
|
161
|
+
if (Array.isArray(supportedProviders)) {
|
|
162
|
+
return supportedProviders;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (supportedProviders === "all") {
|
|
166
|
+
return Object.keys(vcsProviders);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return ["none"];
|
|
170
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { logger } from "@portosaur/logger";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Portosaur Discovery-Based Schema Generator
|
|
7
|
+
*
|
|
8
|
+
* This command "discovers" the configuration schema by parsing the actual
|
|
9
|
+
* Docusaurus config building logic. Every key accessed via the `get()` helper
|
|
10
|
+
* is recorded and mapped to a JSON schema.
|
|
11
|
+
*/
|
|
12
|
+
export async function schemaCommand(options = {}) {
|
|
13
|
+
// Resolve paths for the monorepo structure
|
|
14
|
+
const pkgDir = path.resolve(import.meta.dirname, "../");
|
|
15
|
+
const coreDir = path.resolve(pkgDir, "../../core");
|
|
16
|
+
|
|
17
|
+
const SOURCE_FILE =
|
|
18
|
+
typeof options.config === "string"
|
|
19
|
+
? path.resolve(process.cwd(), options.config)
|
|
20
|
+
: path.resolve(coreDir, "src/index.mjs");
|
|
21
|
+
|
|
22
|
+
const OUTPUT_FILE =
|
|
23
|
+
typeof options.output === "string"
|
|
24
|
+
? path.resolve(process.cwd(), options.output)
|
|
25
|
+
: path.resolve(pkgDir, "../../../configSchema.json");
|
|
26
|
+
|
|
27
|
+
const discoveredSchema = {
|
|
28
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
29
|
+
title: "Portosaur Project Configuration",
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {},
|
|
32
|
+
required: [],
|
|
33
|
+
additionalProperties: true,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// State for Balanced Parenthesis Walker
|
|
37
|
+
const getStartRegex = /get\(\s*["']([^"']+)["']\s*,\s*/g;
|
|
38
|
+
let match;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (!fs.existsSync(SOURCE_FILE)) {
|
|
42
|
+
throw new Error(`Source file not found: ${SOURCE_FILE}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.info(`Discovering keys from ${SOURCE_FILE}...`);
|
|
46
|
+
const sourceCode = fs.readFileSync(SOURCE_FILE, "utf8");
|
|
47
|
+
|
|
48
|
+
// Discovery Loop
|
|
49
|
+
while ((match = getStartRegex.exec(sourceCode)) !== null) {
|
|
50
|
+
const keyPath = match[1];
|
|
51
|
+
const startIdx = match.index + match[0].length;
|
|
52
|
+
|
|
53
|
+
let braceCount = 1;
|
|
54
|
+
let currentIdx = startIdx;
|
|
55
|
+
let defaultValueRaw = "";
|
|
56
|
+
let inString = null;
|
|
57
|
+
let inComment = null;
|
|
58
|
+
let escaped = false;
|
|
59
|
+
|
|
60
|
+
// Extract raw default value using balanced parenthesis walker
|
|
61
|
+
while (braceCount > 0 && currentIdx < sourceCode.length) {
|
|
62
|
+
const char = sourceCode[currentIdx];
|
|
63
|
+
const nextChar = sourceCode[currentIdx + 1];
|
|
64
|
+
|
|
65
|
+
if (escaped) {
|
|
66
|
+
escaped = false;
|
|
67
|
+
} else if (char === "\\") {
|
|
68
|
+
escaped = true;
|
|
69
|
+
} else if (!inString && !inComment) {
|
|
70
|
+
if (char === "'" || char === '"' || char === "`") inString = char;
|
|
71
|
+
else if (char === "/" && nextChar === "/") inComment = "//";
|
|
72
|
+
else if (char === "/" && nextChar === "*") inComment = "/*";
|
|
73
|
+
else if (char === "(") braceCount++;
|
|
74
|
+
else if (char === ")") braceCount--;
|
|
75
|
+
} else if (inString) {
|
|
76
|
+
if (char === inString) inString = null;
|
|
77
|
+
} else if (inComment === "//") {
|
|
78
|
+
if (char === "\n") inComment = null;
|
|
79
|
+
} else if (inComment === "/*") {
|
|
80
|
+
if (char === "*" && nextChar === "/") {
|
|
81
|
+
inComment = null;
|
|
82
|
+
defaultValueRaw += "*/";
|
|
83
|
+
currentIdx += 2;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (braceCount > 0) defaultValueRaw += char;
|
|
89
|
+
currentIdx++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Extract Documentation Comments
|
|
93
|
+
const beforeMatch = sourceCode.slice(0, match.index);
|
|
94
|
+
const currentLineStart = beforeMatch.lastIndexOf("\n");
|
|
95
|
+
const linesBefore = sourceCode
|
|
96
|
+
.slice(0, currentLineStart === -1 ? 0 : currentLineStart)
|
|
97
|
+
.split("\n");
|
|
98
|
+
|
|
99
|
+
let i = linesBefore.length - 1;
|
|
100
|
+
let foundComment = [];
|
|
101
|
+
let inDocBlock = false;
|
|
102
|
+
|
|
103
|
+
while (i >= 0 && foundComment.length < 15) {
|
|
104
|
+
let line = linesBefore[i].trim();
|
|
105
|
+
if (!line && !inDocBlock) break;
|
|
106
|
+
if (line.endsWith("*/")) inDocBlock = true;
|
|
107
|
+
|
|
108
|
+
if (inDocBlock) {
|
|
109
|
+
const content = line.replace(/^\/\*\*?|\*\/|\*/g, "").trim();
|
|
110
|
+
if (content) foundComment.unshift(content);
|
|
111
|
+
if (line.startsWith("/*") || line.startsWith("/**"))
|
|
112
|
+
inDocBlock = false;
|
|
113
|
+
} else if (line.startsWith("//")) {
|
|
114
|
+
let content = line.replace(/^\/\/\s?/, "").trim();
|
|
115
|
+
if (!content.match(/^(TODO|FIXME|NOTE|SECTION|---)/i)) {
|
|
116
|
+
foundComment.unshift(content);
|
|
117
|
+
}
|
|
118
|
+
} else if (line && !line.startsWith("/") && !line.startsWith("*")) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
i--;
|
|
122
|
+
}
|
|
123
|
+
const description = foundComment.join("\n").trim();
|
|
124
|
+
|
|
125
|
+
// Basic Type Inference and Argument Parsing
|
|
126
|
+
let type = "string";
|
|
127
|
+
let defaultValue = undefined;
|
|
128
|
+
|
|
129
|
+
let args = [];
|
|
130
|
+
let currentArg = "";
|
|
131
|
+
let depth = 0;
|
|
132
|
+
let argInString = null;
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < defaultValueRaw.length; i++) {
|
|
135
|
+
const c = defaultValueRaw[i];
|
|
136
|
+
if (!argInString) {
|
|
137
|
+
if (c === "'" || c === '"' || c === "`") argInString = c;
|
|
138
|
+
else if (c === "(" || c === "[" || c === "{") depth++;
|
|
139
|
+
else if (c === ")" || c === "]" || c === "}") depth--;
|
|
140
|
+
else if (c === "," && depth === 0) {
|
|
141
|
+
args.push(currentArg.trim());
|
|
142
|
+
currentArg = "";
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
} else if (c === argInString && defaultValueRaw[i - 1] !== "\\") {
|
|
146
|
+
argInString = null;
|
|
147
|
+
}
|
|
148
|
+
currentArg += c;
|
|
149
|
+
}
|
|
150
|
+
if (currentArg.trim()) args.push(currentArg.trim());
|
|
151
|
+
|
|
152
|
+
let val = (args[args.length - 1] || "").trim();
|
|
153
|
+
|
|
154
|
+
if (val === "true" || val === "false") {
|
|
155
|
+
type = "boolean";
|
|
156
|
+
defaultValue = val === "true";
|
|
157
|
+
} else if (!isNaN(val) && val !== "") {
|
|
158
|
+
type = "number";
|
|
159
|
+
defaultValue = Number(val);
|
|
160
|
+
} else if (
|
|
161
|
+
val.startsWith("[") ||
|
|
162
|
+
val.includes(".map(") ||
|
|
163
|
+
val.includes(".filter(") ||
|
|
164
|
+
val.includes(".slice(")
|
|
165
|
+
) {
|
|
166
|
+
type = "array";
|
|
167
|
+
} else if (val.startsWith("{")) {
|
|
168
|
+
type = "object";
|
|
169
|
+
} else if (val) {
|
|
170
|
+
defaultValue = val.replace(/^["'`](.*)["'`]$/s, "$1").trim();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Map to Discovered Schema Object
|
|
174
|
+
const parts = keyPath.split(".");
|
|
175
|
+
let current = discoveredSchema.properties;
|
|
176
|
+
|
|
177
|
+
parts.forEach((part, index) => {
|
|
178
|
+
const isLast = index === parts.length - 1;
|
|
179
|
+
|
|
180
|
+
if (!current[part]) {
|
|
181
|
+
if (isLast) {
|
|
182
|
+
current[part] = { type };
|
|
183
|
+
if (description) current[part].description = description;
|
|
184
|
+
if (defaultValue !== undefined)
|
|
185
|
+
current[part].default = defaultValue;
|
|
186
|
+
} else {
|
|
187
|
+
current[part] = { type: "object", properties: {} };
|
|
188
|
+
}
|
|
189
|
+
} else if (!isLast && current[part].type !== "object") {
|
|
190
|
+
current[part] = { type: "object", properties: {} };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!isLast) {
|
|
194
|
+
if (!current[part].properties) current[part].properties = {};
|
|
195
|
+
current = current[part].properties;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(discoveredSchema, null, 2));
|
|
201
|
+
logger.success(
|
|
202
|
+
`Successfully discovered and wrote schema to: ${OUTPUT_FILE}`,
|
|
203
|
+
);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
logger.error(`Discovery failed: ${error.message}`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { runDocusaurus, validateProject } from "../utils/index.mjs";
|
|
3
|
+
import { logger } from "@portosaur/logger";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Serves the built Portosaur site locally.
|
|
7
|
+
*
|
|
8
|
+
* Note: Docusaurus 'serve' doesn't require a config shim,
|
|
9
|
+
* as it serves the already built static files from the output directory.
|
|
10
|
+
*/
|
|
11
|
+
export async function serveCommand(siteDir, extraArgs = []) {
|
|
12
|
+
const UserRoot = siteDir
|
|
13
|
+
? path.resolve(process.cwd(), siteDir)
|
|
14
|
+
: process.cwd();
|
|
15
|
+
|
|
16
|
+
// ------- Setup -------
|
|
17
|
+
|
|
18
|
+
validateProject(UserRoot);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
logger.info("Serving built site...");
|
|
22
|
+
|
|
23
|
+
// Docusaurus serve looks for the 'build' directory by default.
|
|
24
|
+
await runDocusaurus("serve", UserRoot, "", extraArgs);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger.error(`Failed to serve site: ${error.message}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface InitOptions {
|
|
2
|
+
/** VCS Provider ID (e.g., "github", "gitlab") */
|
|
3
|
+
vcsProvider?: string;
|
|
4
|
+
/** Hosting Platform ID (e.g., "github_pages", "surge") */
|
|
5
|
+
hosting?: string;
|
|
6
|
+
/** VCS username */
|
|
7
|
+
username?: string;
|
|
8
|
+
/** Full name for the portfolio */
|
|
9
|
+
name?: string;
|
|
10
|
+
/** Project name/directory */
|
|
11
|
+
projectName?: string;
|
|
12
|
+
/** Whether to install dependencies after initialization (default: true) */
|
|
13
|
+
install?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InitCiOptions {
|
|
17
|
+
/** Hosting Platform ID */
|
|
18
|
+
hosting?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initializes a new Portosaur project.
|
|
23
|
+
*/
|
|
24
|
+
export function initCommand(options?: InitOptions): Promise<void>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sets up CI/CD workflows for an existing project.
|
|
28
|
+
*/
|
|
29
|
+
export function initCiCommand(options?: InitCiOptions): Promise<void>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Starts the Docusaurus development server.
|
|
33
|
+
*/
|
|
34
|
+
export function devCommand(siteDir?: string, extraArgs?: string[]): void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Builds the Docusaurus static site.
|
|
38
|
+
*/
|
|
39
|
+
export function buildCommand(siteDir?: string, extraArgs?: string[]): void;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Serves the built static site locally.
|
|
43
|
+
*/
|
|
44
|
+
export function serveCommand(siteDir?: string): void;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generates the config schema.
|
|
48
|
+
*/
|
|
49
|
+
export function schemaCommand(outPath?: string, srcPath?: string): void;
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./commands/init.mjs";
|
|
2
|
+
export * from "./commands/initCi.mjs";
|
|
3
|
+
export * from "./commands/dev.mjs";
|
|
4
|
+
export * from "./commands/build.mjs";
|
|
5
|
+
export * from "./commands/serve.mjs";
|
|
6
|
+
export * from "./commands/schema.mjs";
|
|
7
|
+
export * from "./commands/providers.mjs";
|
|
8
|
+
export * from "./utils/index.mjs";
|