@multiplatform.one/cli 5.0.26 → 6.0.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/bin/multiplatformOne.mjs +0 -21
- package/lib/bin/multiplatformOne.mjs +820 -552
- package/lib/commands/e2e.mjs +409 -0
- package/lib/commands/init.mjs +480 -0
- package/lib/generateVscode.mjs +167 -0
- package/lib/index.mjs +1 -0
- package/lib/types.mjs +1 -0
- package/package.json +37 -43
- package/scripts/clone.sh +17 -0
- package/scripts/frappe-bench.sh +9 -0
- package/scripts/frappe-bootstrap.sh +495 -0
- package/scripts/frappe-clean.sh +31 -0
- package/scripts/frappe-dev.sh +41 -0
- package/scripts/frappe-helpers.sh +94 -0
- package/scripts/frappe.sh +72 -0
- package/scripts/update.sh +29 -25
- package/src/bin/multiplatformOne.ts +604 -539
- package/src/commands/e2e.ts +515 -0
- package/src/commands/init.ts +682 -0
- package/src/generateVscode.ts +216 -0
- package/src/index.ts +0 -21
- package/src/types.ts +3 -26
- package/scripts/cookiecutter.sh +0 -20
- package/scripts/init.sh +0 -5
- package/types/.tsbuildinfo +0 -1
- package/types/bin/multiplatformOne.d.ts +0 -2
- package/types/bin/multiplatformOne.d.ts.map +0 -1
- package/types/index.d.ts +0 -2
- package/types/index.d.ts.map +0 -1
- package/types/types.d.ts +0 -28
- package/types/types.d.ts.map +0 -1
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/init.ts
|
|
7
|
+
const repoUrl = "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git";
|
|
8
|
+
const services = [
|
|
9
|
+
"frappe",
|
|
10
|
+
"solana",
|
|
11
|
+
"ethereum",
|
|
12
|
+
"sui"
|
|
13
|
+
];
|
|
14
|
+
/** Resolve a service name to its relative directory from the project root. */
|
|
15
|
+
function serviceDir(name) {
|
|
16
|
+
return `apps/${name}`;
|
|
17
|
+
}
|
|
18
|
+
const apps = [
|
|
19
|
+
"one",
|
|
20
|
+
"keycloak",
|
|
21
|
+
"storybook",
|
|
22
|
+
"storybook-expo",
|
|
23
|
+
"vscode",
|
|
24
|
+
"webext",
|
|
25
|
+
"vocs"
|
|
26
|
+
];
|
|
27
|
+
/** env variable prefixes to remove per service */
|
|
28
|
+
const serviceEnvPrefixes = {
|
|
29
|
+
frappe: ["FRAPPE_", "MARIADB_"],
|
|
30
|
+
solana: [],
|
|
31
|
+
ethereum: [],
|
|
32
|
+
sui: []
|
|
33
|
+
};
|
|
34
|
+
/** env variable prefixes to remove per app */
|
|
35
|
+
const appEnvPrefixes = {
|
|
36
|
+
one: ["ONE_"],
|
|
37
|
+
keycloak: ["KEYCLOAK_"],
|
|
38
|
+
storybook: ["STORYBOOK_", "VR_DIFFING_ENGINE="],
|
|
39
|
+
"storybook-expo": [],
|
|
40
|
+
vscode: ["VSCODE_"],
|
|
41
|
+
webext: ["WEBEXT_"],
|
|
42
|
+
vocs: ["VOCS_"]
|
|
43
|
+
};
|
|
44
|
+
/** env section headers (comments) to remove per app/service */
|
|
45
|
+
const serviceEnvSections = {
|
|
46
|
+
frappe: ["# frappe", "# mariadb"],
|
|
47
|
+
solana: [],
|
|
48
|
+
ethereum: [],
|
|
49
|
+
sui: []
|
|
50
|
+
};
|
|
51
|
+
const appEnvSections = {
|
|
52
|
+
one: ["# one"],
|
|
53
|
+
keycloak: ["# keycloak"],
|
|
54
|
+
storybook: ["# storybook"],
|
|
55
|
+
"storybook-expo": [],
|
|
56
|
+
vscode: ["# vscode"],
|
|
57
|
+
webext: ["# webext"],
|
|
58
|
+
vocs: ["# vocs"]
|
|
59
|
+
};
|
|
60
|
+
/** launch.json configuration names per service/app */
|
|
61
|
+
const serviceLaunchNames = {
|
|
62
|
+
frappe: ["frappe dev"],
|
|
63
|
+
solana: ["solana dev", "solana localnet"],
|
|
64
|
+
ethereum: ["ethereum dev"],
|
|
65
|
+
sui: ["sui dev"]
|
|
66
|
+
};
|
|
67
|
+
const appLaunchNames = {
|
|
68
|
+
one: ["one dev"],
|
|
69
|
+
keycloak: ["keycloak dev", "keycloak storybook"],
|
|
70
|
+
storybook: ["storybook dev"],
|
|
71
|
+
"storybook-expo": ["storybook-expo dev"],
|
|
72
|
+
vscode: ["vscode dev"],
|
|
73
|
+
webext: ["webext dev"],
|
|
74
|
+
vocs: ["vocs dev"]
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Shared modify step: remove unselected apps/services and rewrite configs.
|
|
78
|
+
* Used by both init (with prompted selections) and update (with getPresentApps/getPresentServices).
|
|
79
|
+
*/
|
|
80
|
+
function runModifyStep(dir, selectedServices, selectedApps) {
|
|
81
|
+
const services = selectedServices;
|
|
82
|
+
const apps = selectedApps;
|
|
83
|
+
for (const service of services) if (!services.includes(service)) {
|
|
84
|
+
const servicePath = join(dir, serviceDir(service));
|
|
85
|
+
if (existsSync(servicePath)) {
|
|
86
|
+
rmSync(servicePath, { recursive: true });
|
|
87
|
+
console.log(`Removed service: ${service}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const app of apps) if (!apps.includes(app)) {
|
|
91
|
+
const appPath = join(dir, "apps", app);
|
|
92
|
+
if (existsSync(appPath)) {
|
|
93
|
+
rmSync(appPath, { recursive: true });
|
|
94
|
+
console.log(`Removed app: ${app}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const packagesDir = join(dir, "packages");
|
|
98
|
+
if (existsSync(packagesDir)) {
|
|
99
|
+
const versionMap = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const pkg of readdirSync(packagesDir)) {
|
|
101
|
+
const pkgJsonPath = join(packagesDir, pkg, "package.json");
|
|
102
|
+
if (existsSync(pkgJsonPath)) {
|
|
103
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
104
|
+
if (pkgJson.name && pkgJson.version) versionMap.set(pkgJson.name, pkgJson.version);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const pkg of readdirSync(packagesDir)) {
|
|
108
|
+
const pkgJsonPath = join(packagesDir, pkg, "package.json");
|
|
109
|
+
if (existsSync(pkgJsonPath)) {
|
|
110
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
111
|
+
if (!pkgJson.private) {
|
|
112
|
+
rmSync(join(packagesDir, pkg), { recursive: true });
|
|
113
|
+
console.log(`Removed public package: ${pkgJson.name}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
convertWorkspaceVersions(dir, versionMap);
|
|
118
|
+
}
|
|
119
|
+
cleanupPnpmWorkspace(dir, services, apps);
|
|
120
|
+
cleanupLaunchJson(dir, services, apps);
|
|
121
|
+
cleanupEnvFiles(dir, services, apps);
|
|
122
|
+
cleanupTsconfigs(dir);
|
|
123
|
+
cleanupMisc(dir);
|
|
124
|
+
removeLicenseHeaders(dir);
|
|
125
|
+
}
|
|
126
|
+
async function init(nameArg, options) {
|
|
127
|
+
let projectName = nameArg;
|
|
128
|
+
if (!projectName) projectName = (await inquirer.prompt([{
|
|
129
|
+
message: "What is the project name?",
|
|
130
|
+
name: "name",
|
|
131
|
+
type: "input",
|
|
132
|
+
default: "my-app"
|
|
133
|
+
}])).name;
|
|
134
|
+
let selectedServices;
|
|
135
|
+
if (options.services) selectedServices = options.services.split(",").map((s) => s.trim());
|
|
136
|
+
else selectedServices = (await inquirer.prompt([{
|
|
137
|
+
message: "Select services:",
|
|
138
|
+
name: "services",
|
|
139
|
+
type: "checkbox",
|
|
140
|
+
choices: services.map((s) => ({
|
|
141
|
+
name: s,
|
|
142
|
+
value: s
|
|
143
|
+
}))
|
|
144
|
+
}])).services;
|
|
145
|
+
let selectedApps;
|
|
146
|
+
if (options.apps) selectedApps = options.apps.split(",").map((s) => s.trim());
|
|
147
|
+
else selectedApps = (await inquirer.prompt([{
|
|
148
|
+
message: "Select apps:",
|
|
149
|
+
name: "apps",
|
|
150
|
+
type: "checkbox",
|
|
151
|
+
choices: apps.map((a) => ({
|
|
152
|
+
name: a,
|
|
153
|
+
value: a,
|
|
154
|
+
checked: a === "one"
|
|
155
|
+
}))
|
|
156
|
+
}])).apps;
|
|
157
|
+
const targetDir = resolve(projectName);
|
|
158
|
+
if (existsSync(targetDir)) {
|
|
159
|
+
if (readdirSync(targetDir).length > 0) throw new Error(`Directory "${targetDir}" already exists and is not empty`);
|
|
160
|
+
}
|
|
161
|
+
const branch = options.checkout || "main";
|
|
162
|
+
console.log("\nCloning repository...");
|
|
163
|
+
if (options.cloneScript) execSync(`sh "${options.cloneScript}" "${repoUrl}" "${branch}" "${targetDir}"`, { stdio: "inherit" });
|
|
164
|
+
else execSync(`git clone --depth 1 --branch "${branch}" ${repoUrl} "${targetDir}"`, { stdio: "inherit" });
|
|
165
|
+
rmSync(join(targetDir, ".git"), {
|
|
166
|
+
recursive: true,
|
|
167
|
+
force: true
|
|
168
|
+
});
|
|
169
|
+
runModifyStep(targetDir, selectedServices, selectedApps);
|
|
170
|
+
const rootPkgPath = join(targetDir, "package.json");
|
|
171
|
+
if (existsSync(rootPkgPath)) {
|
|
172
|
+
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8"));
|
|
173
|
+
rootPkg.name = projectName;
|
|
174
|
+
rootPkg.packageManager = void 0;
|
|
175
|
+
writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`);
|
|
176
|
+
}
|
|
177
|
+
const envDefaultPath = join(targetDir, ".env.example");
|
|
178
|
+
const envPath = join(targetDir, ".env");
|
|
179
|
+
if (existsSync(envDefaultPath) && !existsSync(envPath)) writeFileSync(envPath, readFileSync(envDefaultPath, "utf-8"));
|
|
180
|
+
const lostpixelBaseline = join(targetDir, "apps", "storybook", ".lostpixel", "baseline");
|
|
181
|
+
if (existsSync(lostpixelBaseline)) rmSync(lostpixelBaseline, { recursive: true });
|
|
182
|
+
const lockfile = join(targetDir, "pnpm-lock.yaml");
|
|
183
|
+
if (existsSync(lockfile)) rmSync(lockfile);
|
|
184
|
+
for (const artifact of [
|
|
185
|
+
"agent-os",
|
|
186
|
+
"public/cli/scripts/init.sh",
|
|
187
|
+
"public/cli/scripts/update.sh",
|
|
188
|
+
".changeset"
|
|
189
|
+
]) {
|
|
190
|
+
const p = join(targetDir, artifact);
|
|
191
|
+
if (existsSync(p)) rmSync(p, {
|
|
192
|
+
recursive: true,
|
|
193
|
+
force: true
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
console.log("\nInstalling dependencies...");
|
|
197
|
+
execSync("pnpm install", {
|
|
198
|
+
cwd: targetDir,
|
|
199
|
+
stdio: "inherit"
|
|
200
|
+
});
|
|
201
|
+
console.log("\nInitializing git repository...");
|
|
202
|
+
execSync("git init", {
|
|
203
|
+
cwd: targetDir,
|
|
204
|
+
stdio: "inherit"
|
|
205
|
+
});
|
|
206
|
+
execSync("git add -A", {
|
|
207
|
+
cwd: targetDir,
|
|
208
|
+
stdio: "inherit"
|
|
209
|
+
});
|
|
210
|
+
execSync("git commit -m \"Initial commit from multiplatform.one\"", {
|
|
211
|
+
cwd: targetDir,
|
|
212
|
+
stdio: "inherit"
|
|
213
|
+
});
|
|
214
|
+
console.log(`\n✅ Project created at ${targetDir}`);
|
|
215
|
+
console.log("\nNext steps:");
|
|
216
|
+
console.log(` cd ${projectName}`);
|
|
217
|
+
console.log(" pnpm dev");
|
|
218
|
+
}
|
|
219
|
+
function convertWorkspaceVersions(dir, versionMap) {
|
|
220
|
+
const files = findPackageJsonFiles(dir);
|
|
221
|
+
for (const file of files) {
|
|
222
|
+
const content = JSON.parse(readFileSync(file, "utf-8"));
|
|
223
|
+
let changed = false;
|
|
224
|
+
for (const depType of [
|
|
225
|
+
"dependencies",
|
|
226
|
+
"devDependencies",
|
|
227
|
+
"peerDependencies"
|
|
228
|
+
]) {
|
|
229
|
+
const deps = content[depType];
|
|
230
|
+
if (!deps) continue;
|
|
231
|
+
for (const [depName, version] of Object.entries(deps)) if (typeof version === "string" && version.startsWith("workspace:")) {
|
|
232
|
+
const realVersion = versionMap.get(depName);
|
|
233
|
+
if (realVersion) {
|
|
234
|
+
const workspaceRange = version.replace("workspace:", "");
|
|
235
|
+
deps[depName] = workspaceRange === "*" || workspaceRange === "^" ? `^${realVersion}` : workspaceRange;
|
|
236
|
+
changed = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (changed) writeFileSync(file, `${JSON.stringify(content, null, 2)}\n`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function findPackageJsonFiles(dir) {
|
|
244
|
+
const results = [];
|
|
245
|
+
function walk(d) {
|
|
246
|
+
let entries;
|
|
247
|
+
try {
|
|
248
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
249
|
+
} catch {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
254
|
+
const full = join(d, entry.name);
|
|
255
|
+
if (entry.isDirectory()) walk(full);
|
|
256
|
+
else if (entry.name === "package.json") results.push(full);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
walk(dir);
|
|
260
|
+
return results;
|
|
261
|
+
}
|
|
262
|
+
function cleanupPnpmWorkspace(dir, _services, _apps) {
|
|
263
|
+
const wsPath = join(dir, "pnpm-workspace.yaml");
|
|
264
|
+
if (!existsSync(wsPath)) return;
|
|
265
|
+
let content = readFileSync(wsPath, "utf-8");
|
|
266
|
+
const appsDir = join(dir, "apps");
|
|
267
|
+
if (!existsSync(appsDir) || readdirSync(appsDir).length === 0) content = content.replace(/^\s*-\s*['"]?apps\/\*['"]?\s*$/gm, "");
|
|
268
|
+
const pkgDir = join(dir, "packages");
|
|
269
|
+
if (!existsSync(pkgDir) || readdirSync(pkgDir).length === 0) content = content.replace(/^\s*-\s*['"]?packages\/\*['"]?\s*$/gm, "");
|
|
270
|
+
content = `${content.replace(/\n{3,}/g, "\n\n").trim()}\n`;
|
|
271
|
+
writeFileSync(wsPath, content);
|
|
272
|
+
}
|
|
273
|
+
function cleanupLaunchJson(dir, services, apps) {
|
|
274
|
+
const launchPath = join(dir, ".vscode", "launch.json");
|
|
275
|
+
if (!existsSync(launchPath)) return;
|
|
276
|
+
const launch = JSON.parse(readFileSync(launchPath, "utf-8"));
|
|
277
|
+
const removedNames = /* @__PURE__ */ new Set();
|
|
278
|
+
for (const service of services) if (!services.includes(service)) for (const configName of serviceLaunchNames[service]) removedNames.add(configName);
|
|
279
|
+
for (const app of apps) if (!apps.includes(app)) for (const configName of appLaunchNames[app]) removedNames.add(configName);
|
|
280
|
+
if (launch.configurations) launch.configurations = launch.configurations.filter((c) => !removedNames.has(c.name));
|
|
281
|
+
if (launch.compounds) {
|
|
282
|
+
for (const compound of launch.compounds) if (compound.configurations) compound.configurations = compound.configurations.filter((configName) => !removedNames.has(configName));
|
|
283
|
+
launch.compounds = launch.compounds.filter((c) => c.configurations && c.configurations.length > 0);
|
|
284
|
+
}
|
|
285
|
+
writeFileSync(launchPath, `${JSON.stringify(launch, null, 2)}\n`);
|
|
286
|
+
}
|
|
287
|
+
function cleanupEnvFiles(dir, services, apps) {
|
|
288
|
+
for (const envFile of [".env.example", ".env"]) {
|
|
289
|
+
const filePath = join(dir, envFile);
|
|
290
|
+
if (!existsSync(filePath)) continue;
|
|
291
|
+
let lines = readFileSync(filePath, "utf-8").split("\n");
|
|
292
|
+
for (const service of services) if (!services.includes(service)) {
|
|
293
|
+
const prefixes = serviceEnvPrefixes[service];
|
|
294
|
+
const sections = serviceEnvSections[service];
|
|
295
|
+
lines = filterEnvLines(lines, prefixes, sections);
|
|
296
|
+
}
|
|
297
|
+
for (const app of apps) if (!apps.includes(app)) {
|
|
298
|
+
const prefixes = appEnvPrefixes[app];
|
|
299
|
+
const sections = appEnvSections[app];
|
|
300
|
+
lines = filterEnvLines(lines, prefixes, sections);
|
|
301
|
+
}
|
|
302
|
+
writeFileSync(filePath, `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trim()}\n`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function filterEnvLines(lines, prefixes, sections) {
|
|
306
|
+
return lines.filter((line) => {
|
|
307
|
+
const trimmed = line.trim();
|
|
308
|
+
for (const section of sections) if (trimmed === section) return false;
|
|
309
|
+
for (const prefix of prefixes) if (prefix.endsWith("=")) {
|
|
310
|
+
if (trimmed.startsWith(prefix)) return false;
|
|
311
|
+
} else if (trimmed.startsWith(prefix) || trimmed.startsWith(`# ${prefix}`)) return false;
|
|
312
|
+
return true;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function cleanupTsconfigs(dir) {
|
|
316
|
+
const tsconfigFiles = findTsconfigFiles(dir);
|
|
317
|
+
for (const file of tsconfigFiles) try {
|
|
318
|
+
const content = readFileSync(file, "utf-8");
|
|
319
|
+
const json = JSON.parse(stripJsonComments(content));
|
|
320
|
+
if (!json.references) continue;
|
|
321
|
+
let changed = false;
|
|
322
|
+
json.references = json.references.filter((ref) => {
|
|
323
|
+
const refPath = resolve(join(file, "..", ref.path));
|
|
324
|
+
const pathExists = existsSync(refPath) || existsSync(`${refPath}.json`) || existsSync(join(refPath, "tsconfig.json"));
|
|
325
|
+
if (!pathExists) changed = true;
|
|
326
|
+
return pathExists;
|
|
327
|
+
});
|
|
328
|
+
if (changed) writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
|
|
329
|
+
} catch {}
|
|
330
|
+
}
|
|
331
|
+
function findTsconfigFiles(dir) {
|
|
332
|
+
const results = [];
|
|
333
|
+
function walk(d) {
|
|
334
|
+
let entries;
|
|
335
|
+
try {
|
|
336
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
337
|
+
} catch {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
342
|
+
const full = join(d, entry.name);
|
|
343
|
+
if (entry.isDirectory()) walk(full);
|
|
344
|
+
else if (entry.name.startsWith("tsconfig") && entry.name.endsWith(".json")) results.push(full);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
walk(dir);
|
|
348
|
+
return results;
|
|
349
|
+
}
|
|
350
|
+
function stripJsonComments(str) {
|
|
351
|
+
return str.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
352
|
+
}
|
|
353
|
+
function cleanupMisc(dir) {
|
|
354
|
+
for (const subdir of ["docker/dns"]) {
|
|
355
|
+
const p = join(dir, subdir);
|
|
356
|
+
if (existsSync(p)) rmSync(p, { recursive: true });
|
|
357
|
+
}
|
|
358
|
+
const settingsPath = join(dir, ".vscode", "settings.json");
|
|
359
|
+
if (existsSync(settingsPath)) try {
|
|
360
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
361
|
+
const psiKeys = Object.keys(settings).filter((k) => k.startsWith("psi-header"));
|
|
362
|
+
const cleaned = { ...settings };
|
|
363
|
+
for (const key of psiKeys) cleaned[key] = void 0;
|
|
364
|
+
const filtered = JSON.parse(JSON.stringify(cleaned));
|
|
365
|
+
writeFileSync(settingsPath, `${JSON.stringify(filtered, null, 2)}\n`);
|
|
366
|
+
} catch {}
|
|
367
|
+
const extPath = join(dir, ".vscode", "extensions.json");
|
|
368
|
+
if (existsSync(extPath)) try {
|
|
369
|
+
const ext = JSON.parse(readFileSync(extPath, "utf-8"));
|
|
370
|
+
if (ext.recommendations) ext.recommendations = ext.recommendations.filter((r) => r !== "psioniq.psi-header");
|
|
371
|
+
writeFileSync(extPath, `${JSON.stringify(ext, null, 2)}\n`);
|
|
372
|
+
} catch {}
|
|
373
|
+
}
|
|
374
|
+
const licenseIndicators = [
|
|
375
|
+
"File:",
|
|
376
|
+
"Project:",
|
|
377
|
+
"File Created:",
|
|
378
|
+
"Author:",
|
|
379
|
+
"Licensed under"
|
|
380
|
+
];
|
|
381
|
+
function removeLicenseHeaders(dir) {
|
|
382
|
+
const files = findSourceFiles(dir);
|
|
383
|
+
for (const file of files) try {
|
|
384
|
+
const content = readFileSync(file, "utf-8");
|
|
385
|
+
const match = content.match(/^\s*\/\*[\s\S]*?\*\//);
|
|
386
|
+
if (match) {
|
|
387
|
+
const comment = match[0];
|
|
388
|
+
if (licenseIndicators.some((indicator) => comment.includes(indicator)) && match.index !== void 0) writeFileSync(file, content.slice(match.index + comment.length).replace(/^\s*\n/, ""));
|
|
389
|
+
}
|
|
390
|
+
} catch {}
|
|
391
|
+
const hashFiles = findHashCommentFiles(dir);
|
|
392
|
+
for (const file of hashFiles) try {
|
|
393
|
+
const lines = readFileSync(file, "utf-8").split("\n");
|
|
394
|
+
let headerEnd = 0;
|
|
395
|
+
let inHeader = false;
|
|
396
|
+
for (let i = 0; i < lines.length; i++) {
|
|
397
|
+
const line = lines[i];
|
|
398
|
+
if (i === 0 && line.startsWith("#!")) continue;
|
|
399
|
+
if (line.startsWith("# File:") || line.startsWith("# Project:") || line.startsWith("# File Created:") || line.startsWith("# Author:")) {
|
|
400
|
+
inHeader = true;
|
|
401
|
+
headerEnd = i + 1;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (inHeader && line.startsWith("#")) {
|
|
405
|
+
headerEnd = i + 1;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (inHeader && line.trim() === "") {
|
|
409
|
+
headerEnd = i + 1;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
if (inHeader) break;
|
|
413
|
+
}
|
|
414
|
+
if (inHeader && headerEnd > 0) writeFileSync(file, `${lines[0].startsWith("#!") ? `${lines[0]}\n\n` : ""}${lines.slice(headerEnd).join("\n").replace(/^\s*\n/, "")}`);
|
|
415
|
+
} catch {}
|
|
416
|
+
}
|
|
417
|
+
function findSourceFiles(dir) {
|
|
418
|
+
const results = [];
|
|
419
|
+
const extensions = new Set([
|
|
420
|
+
".ts",
|
|
421
|
+
".tsx",
|
|
422
|
+
".js",
|
|
423
|
+
".jsx",
|
|
424
|
+
".mts",
|
|
425
|
+
".mjs",
|
|
426
|
+
".cts",
|
|
427
|
+
".cjs"
|
|
428
|
+
]);
|
|
429
|
+
function walk(d) {
|
|
430
|
+
let entries;
|
|
431
|
+
try {
|
|
432
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
433
|
+
} catch {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
for (const entry of entries) {
|
|
437
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
438
|
+
const full = join(d, entry.name);
|
|
439
|
+
if (entry.isDirectory()) walk(full);
|
|
440
|
+
else {
|
|
441
|
+
const ext = `.${entry.name.split(".").pop()}`;
|
|
442
|
+
if (extensions.has(ext)) results.push(full);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
walk(dir);
|
|
447
|
+
return results;
|
|
448
|
+
}
|
|
449
|
+
function findHashCommentFiles(dir) {
|
|
450
|
+
const results = [];
|
|
451
|
+
const extensions = new Set([
|
|
452
|
+
".sh",
|
|
453
|
+
".mk",
|
|
454
|
+
".yaml",
|
|
455
|
+
".yml",
|
|
456
|
+
".py"
|
|
457
|
+
]);
|
|
458
|
+
function walk(d) {
|
|
459
|
+
let entries;
|
|
460
|
+
try {
|
|
461
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
462
|
+
} catch {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
for (const entry of entries) {
|
|
466
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
467
|
+
const full = join(d, entry.name);
|
|
468
|
+
if (entry.isDirectory()) walk(full);
|
|
469
|
+
else {
|
|
470
|
+
const ext = `.${entry.name.split(".").pop()}`;
|
|
471
|
+
if (extensions.has(ext) || entry.name === "Makefile") results.push(full);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
walk(dir);
|
|
476
|
+
return results;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
//#endregion
|
|
480
|
+
export { init, runModifyStep };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/generateVscode.ts
|
|
5
|
+
/**
|
|
6
|
+
* Generate .vscode/launch.json and .vscode/tasks.json from discovered apps.
|
|
7
|
+
* Invoked by `mpo update` after merging upstream.
|
|
8
|
+
* Safe for subset projects: only includes configs for workspaces that exist.
|
|
9
|
+
*/
|
|
10
|
+
async function readJson(filePath, fallback) {
|
|
11
|
+
try {
|
|
12
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
} catch {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function getPresentApps(root) {
|
|
19
|
+
const appsDir = path.join(root, "apps");
|
|
20
|
+
try {
|
|
21
|
+
const entries = (await fs.readdir(appsDir, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const e of entries) {
|
|
24
|
+
const pkgPath = path.join(appsDir, e.name, "package.json");
|
|
25
|
+
try {
|
|
26
|
+
await fs.access(pkgPath);
|
|
27
|
+
const pkg = await readJson(pkgPath, {});
|
|
28
|
+
out.push({
|
|
29
|
+
name: e.name,
|
|
30
|
+
path: path.join("apps", e.name),
|
|
31
|
+
scripts: pkg.scripts || {}
|
|
32
|
+
});
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function launchConfigForApp(app) {
|
|
41
|
+
const { name, path: relPath, scripts } = app;
|
|
42
|
+
const configs = [];
|
|
43
|
+
const wf = "${workspaceFolder}";
|
|
44
|
+
if (name === "vscode" && scripts.dev) configs.push({
|
|
45
|
+
name: "vscode dev",
|
|
46
|
+
type: "extensionHost",
|
|
47
|
+
request: "launch",
|
|
48
|
+
args: ["--disable-extensions", `--extensionDevelopmentPath=${wf}/${relPath}`],
|
|
49
|
+
cwd: `${wf}/${relPath}`,
|
|
50
|
+
outFiles: [`${wf}/${relPath}/dist/extension/*.js`],
|
|
51
|
+
preLaunchTask: "npm: dev"
|
|
52
|
+
});
|
|
53
|
+
if (scripts.dev) configs.push({
|
|
54
|
+
name: `${name} dev`,
|
|
55
|
+
type: "node-terminal",
|
|
56
|
+
request: "launch",
|
|
57
|
+
cwd: `${wf}/${relPath}`,
|
|
58
|
+
command: "pnpm dev"
|
|
59
|
+
});
|
|
60
|
+
if (scripts.storybook && name === "keycloak") configs.push({
|
|
61
|
+
name: "keycloak storybook",
|
|
62
|
+
type: "node-terminal",
|
|
63
|
+
request: "launch",
|
|
64
|
+
cwd: `${wf}/${relPath}`,
|
|
65
|
+
command: "pnpm storybook"
|
|
66
|
+
});
|
|
67
|
+
if (scripts.localnet && name === "solana") configs.push({
|
|
68
|
+
name: "solana localnet",
|
|
69
|
+
type: "node-terminal",
|
|
70
|
+
request: "launch",
|
|
71
|
+
cwd: `${wf}/${relPath}`,
|
|
72
|
+
command: "pnpm localnet"
|
|
73
|
+
});
|
|
74
|
+
return configs;
|
|
75
|
+
}
|
|
76
|
+
async function generateLaunchJson(root) {
|
|
77
|
+
const configurations = (await getPresentApps(root)).flatMap(launchConfigForApp);
|
|
78
|
+
const compounds = [];
|
|
79
|
+
const frappeDev = configurations.find((c) => c.name === "frappe dev");
|
|
80
|
+
const keycloakDev = configurations.find((c) => c.name === "keycloak dev");
|
|
81
|
+
const oneDev = configurations.find((c) => c.name === "one dev");
|
|
82
|
+
if (frappeDev && keycloakDev) compounds.push({
|
|
83
|
+
name: "Backend",
|
|
84
|
+
configurations: ["frappe dev", "keycloak dev"]
|
|
85
|
+
});
|
|
86
|
+
if (oneDev && keycloakDev) compounds.push({
|
|
87
|
+
name: "Frontend",
|
|
88
|
+
configurations: ["one dev", "keycloak dev"]
|
|
89
|
+
});
|
|
90
|
+
if (frappeDev && oneDev && keycloakDev) compounds.push({
|
|
91
|
+
name: "Web",
|
|
92
|
+
configurations: [
|
|
93
|
+
"frappe dev",
|
|
94
|
+
"one dev",
|
|
95
|
+
"keycloak dev"
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
const storybookConfigs = configurations.filter((c) => [
|
|
99
|
+
"storybook dev",
|
|
100
|
+
"storybook-expo dev",
|
|
101
|
+
"keycloak dev",
|
|
102
|
+
"keycloak storybook"
|
|
103
|
+
].includes(c.name));
|
|
104
|
+
if (storybookConfigs.length) compounds.push({
|
|
105
|
+
name: "Storybook",
|
|
106
|
+
configurations: storybookConfigs.map((c) => c.name)
|
|
107
|
+
});
|
|
108
|
+
const solanaConfigs = configurations.filter((c) => c.name?.startsWith("solana"));
|
|
109
|
+
if (solanaConfigs.length) compounds.push({
|
|
110
|
+
name: "Solana",
|
|
111
|
+
configurations: solanaConfigs.map((c) => c.name)
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
version: "0.2.0",
|
|
115
|
+
configurations,
|
|
116
|
+
...compounds.length ? { compounds } : {}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function generateTasksJson(apps) {
|
|
120
|
+
if (!apps.find((a) => a.name === "vscode")?.scripts?.dev) return null;
|
|
121
|
+
return {
|
|
122
|
+
version: "2.0.0",
|
|
123
|
+
tasks: [{
|
|
124
|
+
type: "npm",
|
|
125
|
+
script: "dev",
|
|
126
|
+
problemMatcher: {
|
|
127
|
+
owner: "typescript",
|
|
128
|
+
fileLocation: "relative",
|
|
129
|
+
pattern: {
|
|
130
|
+
regexp: "^([a-zA-Z]\\:/?([\\w\\-]/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$",
|
|
131
|
+
file: 1,
|
|
132
|
+
line: 3,
|
|
133
|
+
column: 4,
|
|
134
|
+
code: 5,
|
|
135
|
+
message: 6
|
|
136
|
+
},
|
|
137
|
+
background: {
|
|
138
|
+
activeOnStart: true,
|
|
139
|
+
beginsPattern: ".*",
|
|
140
|
+
endsPattern: ".*Local: *http://localhost[^\\s]*"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
isBackground: true,
|
|
144
|
+
presentation: { reveal: "never" },
|
|
145
|
+
group: {
|
|
146
|
+
kind: "build",
|
|
147
|
+
isDefault: true
|
|
148
|
+
},
|
|
149
|
+
options: { cwd: "${workspaceFolder}/apps/vscode" }
|
|
150
|
+
}]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function generateVscodeConfig(root) {
|
|
154
|
+
const launch = await generateLaunchJson(root);
|
|
155
|
+
const tasks = await generateTasksJson(await getPresentApps(root));
|
|
156
|
+
const vsCodeDir = path.join(root, ".vscode");
|
|
157
|
+
await fs.mkdir(vsCodeDir, { recursive: true });
|
|
158
|
+
await fs.writeFile(path.join(vsCodeDir, "launch.json"), JSON.stringify(launch, null, 2), "utf8");
|
|
159
|
+
console.log("Wrote .vscode/launch.json (%d configurations)", launch.configurations.length);
|
|
160
|
+
if (tasks) {
|
|
161
|
+
await fs.writeFile(path.join(vsCodeDir, "tasks.json"), JSON.stringify(tasks, null, 2), "utf8");
|
|
162
|
+
console.log("Wrote .vscode/tasks.json");
|
|
163
|
+
} else console.log("Skipped .vscode/tasks.json (no apps/vscode with dev script)");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
export { generateVscodeConfig };
|
package/lib/index.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/lib/types.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|