@multiplatform.one/cli 5.0.27 → 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 -556
- 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 +602 -541
- 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,682 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { type Dirent, existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
|
|
6
|
+
const repoUrl = "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git";
|
|
7
|
+
|
|
8
|
+
const services = ["frappe", "solana", "ethereum", "sui"] as const;
|
|
9
|
+
|
|
10
|
+
/** Resolve a service name to its relative directory from the project root. */
|
|
11
|
+
function serviceDir(name: string): string {
|
|
12
|
+
return `apps/${name}`;
|
|
13
|
+
}
|
|
14
|
+
const apps = [
|
|
15
|
+
"one",
|
|
16
|
+
"keycloak",
|
|
17
|
+
"storybook",
|
|
18
|
+
"storybook-expo",
|
|
19
|
+
"vscode",
|
|
20
|
+
"webext",
|
|
21
|
+
"vocs",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
type Service = (typeof services)[number];
|
|
25
|
+
type App = (typeof apps)[number];
|
|
26
|
+
|
|
27
|
+
/** env variable prefixes to remove per service */
|
|
28
|
+
const serviceEnvPrefixes: Record<Service, string[]> = {
|
|
29
|
+
frappe: ["FRAPPE_", "MARIADB_"],
|
|
30
|
+
solana: [],
|
|
31
|
+
ethereum: [],
|
|
32
|
+
sui: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** env variable prefixes to remove per app */
|
|
36
|
+
const appEnvPrefixes: Record<App, string[]> = {
|
|
37
|
+
one: ["ONE_"],
|
|
38
|
+
keycloak: ["KEYCLOAK_"],
|
|
39
|
+
storybook: ["STORYBOOK_", "VR_DIFFING_ENGINE="],
|
|
40
|
+
"storybook-expo": [],
|
|
41
|
+
vscode: ["VSCODE_"],
|
|
42
|
+
webext: ["WEBEXT_"],
|
|
43
|
+
vocs: ["VOCS_"],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** env section headers (comments) to remove per app/service */
|
|
47
|
+
const serviceEnvSections: Record<Service, string[]> = {
|
|
48
|
+
frappe: ["# frappe", "# mariadb"],
|
|
49
|
+
solana: [],
|
|
50
|
+
ethereum: [],
|
|
51
|
+
sui: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const appEnvSections: Record<App, string[]> = {
|
|
55
|
+
one: ["# one"],
|
|
56
|
+
keycloak: ["# keycloak"],
|
|
57
|
+
storybook: ["# storybook"],
|
|
58
|
+
"storybook-expo": [],
|
|
59
|
+
vscode: ["# vscode"],
|
|
60
|
+
webext: ["# webext"],
|
|
61
|
+
vocs: ["# vocs"],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** launch.json configuration names per service/app */
|
|
65
|
+
const serviceLaunchNames: Record<Service, string[]> = {
|
|
66
|
+
frappe: ["frappe dev"],
|
|
67
|
+
solana: ["solana dev", "solana localnet"],
|
|
68
|
+
ethereum: ["ethereum dev"],
|
|
69
|
+
sui: ["sui dev"],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const appLaunchNames: Record<App, string[]> = {
|
|
73
|
+
one: ["one dev"],
|
|
74
|
+
keycloak: ["keycloak dev", "keycloak storybook"],
|
|
75
|
+
storybook: ["storybook dev"],
|
|
76
|
+
"storybook-expo": ["storybook-expo dev"],
|
|
77
|
+
vscode: ["vscode dev"],
|
|
78
|
+
webext: ["webext dev"],
|
|
79
|
+
vocs: ["vocs dev"],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
interface InitOptions {
|
|
83
|
+
name?: string;
|
|
84
|
+
services?: string;
|
|
85
|
+
apps?: string;
|
|
86
|
+
checkout?: string;
|
|
87
|
+
/** Path to clone.sh script (from CLI); if set, clone via script instead of execSync */
|
|
88
|
+
cloneScript?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Shared modify step: remove unselected apps/services and rewrite configs.
|
|
93
|
+
* Used by both init (with prompted selections) and update (with getPresentApps/getPresentServices).
|
|
94
|
+
*/
|
|
95
|
+
export function runModifyStep(
|
|
96
|
+
dir: string,
|
|
97
|
+
selectedServices: string[],
|
|
98
|
+
selectedApps: string[],
|
|
99
|
+
): void {
|
|
100
|
+
const services = selectedServices as Service[];
|
|
101
|
+
const apps = selectedApps as App[];
|
|
102
|
+
|
|
103
|
+
for (const service of services) {
|
|
104
|
+
if (!services.includes(service)) {
|
|
105
|
+
const servicePath = join(dir, serviceDir(service));
|
|
106
|
+
if (existsSync(servicePath)) {
|
|
107
|
+
rmSync(servicePath, { recursive: true });
|
|
108
|
+
console.log(`Removed service: ${service}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const app of apps) {
|
|
114
|
+
if (!apps.includes(app)) {
|
|
115
|
+
const appPath = join(dir, "apps", app);
|
|
116
|
+
if (existsSync(appPath)) {
|
|
117
|
+
rmSync(appPath, { recursive: true });
|
|
118
|
+
console.log(`Removed app: ${app}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const packagesDir = join(dir, "packages");
|
|
124
|
+
if (existsSync(packagesDir)) {
|
|
125
|
+
const versionMap = new Map<string, string>();
|
|
126
|
+
for (const pkg of readdirSync(packagesDir)) {
|
|
127
|
+
const pkgJsonPath = join(packagesDir, pkg, "package.json");
|
|
128
|
+
if (existsSync(pkgJsonPath)) {
|
|
129
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
130
|
+
if (pkgJson.name && pkgJson.version) {
|
|
131
|
+
versionMap.set(pkgJson.name, pkgJson.version);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const pkg of readdirSync(packagesDir)) {
|
|
136
|
+
const pkgJsonPath = join(packagesDir, pkg, "package.json");
|
|
137
|
+
if (existsSync(pkgJsonPath)) {
|
|
138
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
139
|
+
if (!pkgJson.private) {
|
|
140
|
+
rmSync(join(packagesDir, pkg), { recursive: true });
|
|
141
|
+
console.log(`Removed public package: ${pkgJson.name}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
convertWorkspaceVersions(dir, versionMap);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
cleanupPnpmWorkspace(dir, services, apps);
|
|
149
|
+
cleanupLaunchJson(dir, services, apps);
|
|
150
|
+
cleanupEnvFiles(dir, services, apps);
|
|
151
|
+
cleanupTsconfigs(dir);
|
|
152
|
+
cleanupMisc(dir);
|
|
153
|
+
removeLicenseHeaders(dir);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function init(nameArg: string | undefined, options: InitOptions) {
|
|
157
|
+
let projectName = nameArg;
|
|
158
|
+
if (!projectName) {
|
|
159
|
+
const result = await inquirer.prompt([
|
|
160
|
+
{
|
|
161
|
+
message: "What is the project name?",
|
|
162
|
+
name: "name",
|
|
163
|
+
type: "input",
|
|
164
|
+
default: "my-app",
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
projectName = result.name as string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let selectedServices: Service[];
|
|
171
|
+
if (options.services) {
|
|
172
|
+
selectedServices = options.services.split(",").map((s) => s.trim()) as Service[];
|
|
173
|
+
} else {
|
|
174
|
+
const result = await inquirer.prompt([
|
|
175
|
+
{
|
|
176
|
+
message: "Select services:",
|
|
177
|
+
name: "services",
|
|
178
|
+
type: "checkbox",
|
|
179
|
+
choices: services.map((s) => ({ name: s, value: s })),
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
selectedServices = result.services as Service[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let selectedApps: App[];
|
|
186
|
+
if (options.apps) {
|
|
187
|
+
selectedApps = options.apps.split(",").map((s) => s.trim()) as App[];
|
|
188
|
+
} else {
|
|
189
|
+
const result = await inquirer.prompt([
|
|
190
|
+
{
|
|
191
|
+
message: "Select apps:",
|
|
192
|
+
name: "apps",
|
|
193
|
+
type: "checkbox",
|
|
194
|
+
choices: apps.map((a) => ({
|
|
195
|
+
name: a,
|
|
196
|
+
value: a,
|
|
197
|
+
checked: a === "one",
|
|
198
|
+
})),
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
selectedApps = result.apps as App[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Target dir: when project name is provided, use ./<name>; when not provided, we prompted above (default "my-app"). Init cannot run inside an existing git repo (enforced by CLI).
|
|
205
|
+
const targetDir = resolve(projectName);
|
|
206
|
+
|
|
207
|
+
if (existsSync(targetDir)) {
|
|
208
|
+
const entries = readdirSync(targetDir);
|
|
209
|
+
if (entries.length > 0) {
|
|
210
|
+
throw new Error(`Directory "${targetDir}" already exists and is not empty`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Clone repository (via script when provided by CLI, else inline)
|
|
215
|
+
const branch = options.checkout || "main";
|
|
216
|
+
console.log("\nCloning repository...");
|
|
217
|
+
if (options.cloneScript) {
|
|
218
|
+
execSync(`sh "${options.cloneScript}" "${repoUrl}" "${branch}" "${targetDir}"`, {
|
|
219
|
+
stdio: "inherit",
|
|
220
|
+
});
|
|
221
|
+
} else {
|
|
222
|
+
execSync(`git clone --depth 1 --branch "${branch}" ${repoUrl} "${targetDir}"`, {
|
|
223
|
+
stdio: "inherit",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Remove .git to start fresh
|
|
228
|
+
rmSync(join(targetDir, ".git"), { recursive: true, force: true });
|
|
229
|
+
|
|
230
|
+
runModifyStep(targetDir, selectedServices, selectedApps);
|
|
231
|
+
|
|
232
|
+
// Init-only: update root package.json name
|
|
233
|
+
const rootPkgPath = join(targetDir, "package.json");
|
|
234
|
+
if (existsSync(rootPkgPath)) {
|
|
235
|
+
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8"));
|
|
236
|
+
rootPkg.name = projectName;
|
|
237
|
+
rootPkg.packageManager = undefined;
|
|
238
|
+
writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Copy .env.example to .env if it exists
|
|
242
|
+
const envDefaultPath = join(targetDir, ".env.example");
|
|
243
|
+
const envPath = join(targetDir, ".env");
|
|
244
|
+
if (existsSync(envDefaultPath) && !existsSync(envPath)) {
|
|
245
|
+
writeFileSync(envPath, readFileSync(envDefaultPath, "utf-8"));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Remove storybook baselines
|
|
249
|
+
const lostpixelBaseline = join(targetDir, "apps", "storybook", ".lostpixel", "baseline");
|
|
250
|
+
if (existsSync(lostpixelBaseline)) {
|
|
251
|
+
rmSync(lostpixelBaseline, { recursive: true });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Remove pnpm-lock.yaml for fresh install
|
|
255
|
+
const lockfile = join(targetDir, "pnpm-lock.yaml");
|
|
256
|
+
if (existsSync(lockfile)) {
|
|
257
|
+
rmSync(lockfile);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Remove scaffolding artifacts
|
|
261
|
+
for (const artifact of [
|
|
262
|
+
"agent-os",
|
|
263
|
+
"public/cli/scripts/init.sh",
|
|
264
|
+
"public/cli/scripts/update.sh",
|
|
265
|
+
".changeset",
|
|
266
|
+
]) {
|
|
267
|
+
const p = join(targetDir, artifact);
|
|
268
|
+
if (existsSync(p)) {
|
|
269
|
+
rmSync(p, { recursive: true, force: true });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Initialize project
|
|
274
|
+
console.log("\nInstalling dependencies...");
|
|
275
|
+
execSync("pnpm install", { cwd: targetDir, stdio: "inherit" });
|
|
276
|
+
|
|
277
|
+
console.log("\nInitializing git repository...");
|
|
278
|
+
execSync("git init", { cwd: targetDir, stdio: "inherit" });
|
|
279
|
+
execSync("git add -A", { cwd: targetDir, stdio: "inherit" });
|
|
280
|
+
execSync('git commit -m "Initial commit from multiplatform.one"', {
|
|
281
|
+
cwd: targetDir,
|
|
282
|
+
stdio: "inherit",
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
console.log(`\n✅ Project created at ${targetDir}`);
|
|
286
|
+
console.log("\nNext steps:");
|
|
287
|
+
console.log(` cd ${projectName}`);
|
|
288
|
+
console.log(" pnpm dev");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function convertWorkspaceVersions(dir: string, versionMap: Map<string, string>) {
|
|
292
|
+
const files = findPackageJsonFiles(dir);
|
|
293
|
+
for (const file of files) {
|
|
294
|
+
const content = JSON.parse(readFileSync(file, "utf-8"));
|
|
295
|
+
let changed = false;
|
|
296
|
+
for (const depType of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
297
|
+
const deps = content[depType] as Record<string, string> | undefined;
|
|
298
|
+
if (!deps) continue;
|
|
299
|
+
for (const [depName, version] of Object.entries(deps)) {
|
|
300
|
+
if (typeof version === "string" && version.startsWith("workspace:")) {
|
|
301
|
+
const realVersion = versionMap.get(depName);
|
|
302
|
+
if (realVersion) {
|
|
303
|
+
// workspace:* -> ^version, workspace:^x -> ^x (keep the range)
|
|
304
|
+
const workspaceRange = version.replace("workspace:", "");
|
|
305
|
+
deps[depName] =
|
|
306
|
+
workspaceRange === "*" || workspaceRange === "^" ? `^${realVersion}` : workspaceRange;
|
|
307
|
+
changed = true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (changed) {
|
|
313
|
+
writeFileSync(file, `${JSON.stringify(content, null, 2)}\n`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function findPackageJsonFiles(dir: string): string[] {
|
|
319
|
+
const results: string[] = [];
|
|
320
|
+
function walk(d: string) {
|
|
321
|
+
let entries: Dirent[];
|
|
322
|
+
try {
|
|
323
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
324
|
+
} catch {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
for (const entry of entries) {
|
|
328
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
329
|
+
const full = join(d, entry.name);
|
|
330
|
+
if (entry.isDirectory()) walk(full);
|
|
331
|
+
else if (entry.name === "package.json") results.push(full);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
walk(dir);
|
|
335
|
+
return results;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function cleanupPnpmWorkspace(dir: string, _services: Service[], _apps: App[]) {
|
|
339
|
+
const wsPath = join(dir, "pnpm-workspace.yaml");
|
|
340
|
+
if (!existsSync(wsPath)) return;
|
|
341
|
+
|
|
342
|
+
let content = readFileSync(wsPath, "utf-8");
|
|
343
|
+
|
|
344
|
+
// If no apps remain, remove apps/* entry
|
|
345
|
+
const appsDir = join(dir, "apps");
|
|
346
|
+
if (!existsSync(appsDir) || readdirSync(appsDir).length === 0) {
|
|
347
|
+
content = content.replace(/^\s*-\s*['"]?apps\/\*['"]?\s*$/gm, "");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check if packages dir still has entries
|
|
351
|
+
const pkgDir = join(dir, "packages");
|
|
352
|
+
if (!existsSync(pkgDir) || readdirSync(pkgDir).length === 0) {
|
|
353
|
+
content = content.replace(/^\s*-\s*['"]?packages\/\*['"]?\s*$/gm, "");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Clean up multiple blank lines
|
|
357
|
+
content = `${content.replace(/\n{3,}/g, "\n\n").trim()}\n`;
|
|
358
|
+
writeFileSync(wsPath, content);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function cleanupLaunchJson(dir: string, services: Service[], apps: App[]) {
|
|
362
|
+
const launchPath = join(dir, ".vscode", "launch.json");
|
|
363
|
+
if (!existsSync(launchPath)) return;
|
|
364
|
+
|
|
365
|
+
const launch = JSON.parse(readFileSync(launchPath, "utf-8"));
|
|
366
|
+
const removedNames = new Set<string>();
|
|
367
|
+
|
|
368
|
+
// Collect names to remove for unselected services
|
|
369
|
+
for (const service of services) {
|
|
370
|
+
if (!services.includes(service)) {
|
|
371
|
+
for (const configName of serviceLaunchNames[service]) {
|
|
372
|
+
removedNames.add(configName);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Collect names to remove for unselected apps
|
|
378
|
+
for (const app of apps) {
|
|
379
|
+
if (!apps.includes(app)) {
|
|
380
|
+
for (const configName of appLaunchNames[app]) {
|
|
381
|
+
removedNames.add(configName);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Filter configurations
|
|
387
|
+
if (launch.configurations) {
|
|
388
|
+
launch.configurations = launch.configurations.filter(
|
|
389
|
+
(c: { name: string }) => !removedNames.has(c.name),
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Filter compound configurations and remove empty compounds
|
|
394
|
+
if (launch.compounds) {
|
|
395
|
+
for (const compound of launch.compounds) {
|
|
396
|
+
if (compound.configurations) {
|
|
397
|
+
compound.configurations = compound.configurations.filter(
|
|
398
|
+
(configName: string) => !removedNames.has(configName),
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
launch.compounds = launch.compounds.filter(
|
|
403
|
+
(c: { configurations?: string[] }) => c.configurations && c.configurations.length > 0,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
writeFileSync(launchPath, `${JSON.stringify(launch, null, 2)}\n`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function cleanupEnvFiles(dir: string, services: Service[], apps: App[]) {
|
|
411
|
+
for (const envFile of [".env.example", ".env"]) {
|
|
412
|
+
const filePath = join(dir, envFile);
|
|
413
|
+
if (!existsSync(filePath)) continue;
|
|
414
|
+
|
|
415
|
+
let lines = readFileSync(filePath, "utf-8").split("\n");
|
|
416
|
+
|
|
417
|
+
// Remove lines for unselected services
|
|
418
|
+
for (const service of services) {
|
|
419
|
+
if (!services.includes(service)) {
|
|
420
|
+
const prefixes = serviceEnvPrefixes[service];
|
|
421
|
+
const sections = serviceEnvSections[service];
|
|
422
|
+
lines = filterEnvLines(lines, prefixes, sections);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Remove lines for unselected apps
|
|
427
|
+
for (const app of apps) {
|
|
428
|
+
if (!apps.includes(app)) {
|
|
429
|
+
const prefixes = appEnvPrefixes[app];
|
|
430
|
+
const sections = appEnvSections[app];
|
|
431
|
+
lines = filterEnvLines(lines, prefixes, sections);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Clean up multiple blank lines
|
|
436
|
+
const content = lines
|
|
437
|
+
.join("\n")
|
|
438
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
439
|
+
.trim();
|
|
440
|
+
writeFileSync(filePath, `${content}\n`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function filterEnvLines(lines: string[], prefixes: string[], sections: string[]): string[] {
|
|
445
|
+
return lines.filter((line) => {
|
|
446
|
+
const trimmed = line.trim();
|
|
447
|
+
// Remove section headers
|
|
448
|
+
for (const section of sections) {
|
|
449
|
+
if (trimmed === section) return false;
|
|
450
|
+
}
|
|
451
|
+
// Remove lines with matching prefixes
|
|
452
|
+
for (const prefix of prefixes) {
|
|
453
|
+
if (prefix.endsWith("=")) {
|
|
454
|
+
// Exact match like "VR_DIFFING_ENGINE="
|
|
455
|
+
if (trimmed.startsWith(prefix)) return false;
|
|
456
|
+
} else {
|
|
457
|
+
// Prefix match like "FRAPPE_"
|
|
458
|
+
if (trimmed.startsWith(prefix) || trimmed.startsWith(`# ${prefix}`)) return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return true;
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function cleanupTsconfigs(dir: string) {
|
|
466
|
+
const tsconfigFiles = findTsconfigFiles(dir);
|
|
467
|
+
for (const file of tsconfigFiles) {
|
|
468
|
+
try {
|
|
469
|
+
const content = readFileSync(file, "utf-8");
|
|
470
|
+
// Parse JSON with comments (tsconfig allows comments)
|
|
471
|
+
const json = JSON.parse(stripJsonComments(content));
|
|
472
|
+
if (!json.references) continue;
|
|
473
|
+
|
|
474
|
+
let changed = false;
|
|
475
|
+
json.references = json.references.filter((ref: { path: string }) => {
|
|
476
|
+
const refPath = resolve(join(file, "..", ref.path));
|
|
477
|
+
// Check if the referenced path exists (could be a dir or file)
|
|
478
|
+
const pathExists =
|
|
479
|
+
existsSync(refPath) ||
|
|
480
|
+
existsSync(`${refPath}.json`) ||
|
|
481
|
+
existsSync(join(refPath, "tsconfig.json"));
|
|
482
|
+
if (!pathExists) changed = true;
|
|
483
|
+
return pathExists;
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
if (changed) {
|
|
487
|
+
writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
|
|
488
|
+
}
|
|
489
|
+
} catch {
|
|
490
|
+
// Skip files that can't be parsed
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function findTsconfigFiles(dir: string): string[] {
|
|
496
|
+
const results: string[] = [];
|
|
497
|
+
function walk(d: string) {
|
|
498
|
+
let entries: Dirent[];
|
|
499
|
+
try {
|
|
500
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
501
|
+
} catch {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
for (const entry of entries) {
|
|
505
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
506
|
+
const full = join(d, entry.name);
|
|
507
|
+
if (entry.isDirectory()) walk(full);
|
|
508
|
+
else if (entry.name.startsWith("tsconfig") && entry.name.endsWith(".json")) {
|
|
509
|
+
results.push(full);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
walk(dir);
|
|
514
|
+
return results;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function stripJsonComments(str: string): string {
|
|
518
|
+
// Simple JSON comment stripper for tsconfig files
|
|
519
|
+
return str.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function cleanupMisc(dir: string) {
|
|
523
|
+
// Remove docker dns dir (if generating from template)
|
|
524
|
+
for (const subdir of ["docker/dns"]) {
|
|
525
|
+
const p = join(dir, subdir);
|
|
526
|
+
if (existsSync(p)) {
|
|
527
|
+
rmSync(p, { recursive: true });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Remove VSCode psi-header config from settings.json
|
|
532
|
+
const settingsPath = join(dir, ".vscode", "settings.json");
|
|
533
|
+
if (existsSync(settingsPath)) {
|
|
534
|
+
try {
|
|
535
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
536
|
+
const psiKeys = Object.keys(settings).filter((k) => k.startsWith("psi-header"));
|
|
537
|
+
const cleaned = { ...settings };
|
|
538
|
+
for (const key of psiKeys) {
|
|
539
|
+
cleaned[key] = undefined;
|
|
540
|
+
}
|
|
541
|
+
// Filter out undefined keys by serializing
|
|
542
|
+
const filtered = JSON.parse(JSON.stringify(cleaned));
|
|
543
|
+
writeFileSync(settingsPath, `${JSON.stringify(filtered, null, 2)}\n`);
|
|
544
|
+
} catch {
|
|
545
|
+
// Skip if settings can't be parsed
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Remove psi-header from extensions.json recommendations
|
|
550
|
+
const extPath = join(dir, ".vscode", "extensions.json");
|
|
551
|
+
if (existsSync(extPath)) {
|
|
552
|
+
try {
|
|
553
|
+
const ext = JSON.parse(readFileSync(extPath, "utf-8"));
|
|
554
|
+
if (ext.recommendations) {
|
|
555
|
+
ext.recommendations = ext.recommendations.filter((r: string) => r !== "psioniq.psi-header");
|
|
556
|
+
}
|
|
557
|
+
writeFileSync(extPath, `${JSON.stringify(ext, null, 2)}\n`);
|
|
558
|
+
} catch {
|
|
559
|
+
// Skip if can't be parsed
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const licenseIndicators = ["File:", "Project:", "File Created:", "Author:", "Licensed under"];
|
|
565
|
+
|
|
566
|
+
function removeLicenseHeaders(dir: string) {
|
|
567
|
+
const files = findSourceFiles(dir);
|
|
568
|
+
for (const file of files) {
|
|
569
|
+
try {
|
|
570
|
+
const content = readFileSync(file, "utf-8");
|
|
571
|
+
// Check for block comment license header at start of file
|
|
572
|
+
const match = content.match(/^\s*\/\*[\s\S]*?\*\//);
|
|
573
|
+
if (match) {
|
|
574
|
+
const comment = match[0];
|
|
575
|
+
const isLicense = licenseIndicators.some((indicator) => comment.includes(indicator));
|
|
576
|
+
if (isLicense && match.index !== undefined) {
|
|
577
|
+
const cleaned = content.slice(match.index + comment.length).replace(/^\s*\n/, "");
|
|
578
|
+
writeFileSync(file, cleaned);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
582
|
+
// Skip files that can't be read
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Also handle hash-style license headers for shell/yaml/python files
|
|
587
|
+
const hashFiles = findHashCommentFiles(dir);
|
|
588
|
+
for (const file of hashFiles) {
|
|
589
|
+
try {
|
|
590
|
+
const content = readFileSync(file, "utf-8");
|
|
591
|
+
const lines = content.split("\n");
|
|
592
|
+
let headerEnd = 0;
|
|
593
|
+
let inHeader = false;
|
|
594
|
+
|
|
595
|
+
for (let i = 0; i < lines.length; i++) {
|
|
596
|
+
const line = lines[i];
|
|
597
|
+
if (i === 0 && line.startsWith("#!")) {
|
|
598
|
+
// Skip shebang
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (
|
|
602
|
+
line.startsWith("# File:") ||
|
|
603
|
+
line.startsWith("# Project:") ||
|
|
604
|
+
line.startsWith("# File Created:") ||
|
|
605
|
+
line.startsWith("# Author:")
|
|
606
|
+
) {
|
|
607
|
+
inHeader = true;
|
|
608
|
+
headerEnd = i + 1;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (inHeader && line.startsWith("#")) {
|
|
612
|
+
headerEnd = i + 1;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (inHeader && line.trim() === "") {
|
|
616
|
+
headerEnd = i + 1;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
if (inHeader) break;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (inHeader && headerEnd > 0) {
|
|
623
|
+
const shebang = lines[0].startsWith("#!") ? `${lines[0]}\n\n` : "";
|
|
624
|
+
const remaining = lines
|
|
625
|
+
.slice(headerEnd)
|
|
626
|
+
.join("\n")
|
|
627
|
+
.replace(/^\s*\n/, "");
|
|
628
|
+
writeFileSync(file, `${shebang}${remaining}`);
|
|
629
|
+
}
|
|
630
|
+
} catch {
|
|
631
|
+
// Skip files that can't be read
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function findSourceFiles(dir: string): string[] {
|
|
637
|
+
const results: string[] = [];
|
|
638
|
+
const extensions = new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs"]);
|
|
639
|
+
function walk(d: string) {
|
|
640
|
+
let entries: Dirent[];
|
|
641
|
+
try {
|
|
642
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
643
|
+
} catch {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
for (const entry of entries) {
|
|
647
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
648
|
+
const full = join(d, entry.name);
|
|
649
|
+
if (entry.isDirectory()) walk(full);
|
|
650
|
+
else {
|
|
651
|
+
const ext = `.${entry.name.split(".").pop()}`;
|
|
652
|
+
if (extensions.has(ext)) results.push(full);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
walk(dir);
|
|
657
|
+
return results;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function findHashCommentFiles(dir: string): string[] {
|
|
661
|
+
const results: string[] = [];
|
|
662
|
+
const extensions = new Set([".sh", ".mk", ".yaml", ".yml", ".py"]);
|
|
663
|
+
function walk(d: string) {
|
|
664
|
+
let entries: Dirent[];
|
|
665
|
+
try {
|
|
666
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
667
|
+
} catch {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
for (const entry of entries) {
|
|
671
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
672
|
+
const full = join(d, entry.name);
|
|
673
|
+
if (entry.isDirectory()) walk(full);
|
|
674
|
+
else {
|
|
675
|
+
const ext = `.${entry.name.split(".").pop()}`;
|
|
676
|
+
if (extensions.has(ext) || entry.name === "Makefile") results.push(full);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
walk(dir);
|
|
681
|
+
return results;
|
|
682
|
+
}
|