@marwes-ui/cli 0.1.0 → 0.3.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/CHANGELOG.md +16 -0
- package/README.md +35 -2
- package/dist/cli.js +392 -63
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +392 -63
- package/dist/index.js.map +1 -1
- package/package.json +22 -2
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'util';
|
|
3
|
-
import {
|
|
3
|
+
import { readFile, readdir, access, writeFile } from 'fs/promises';
|
|
4
|
+
import { basename, join, resolve, extname } from 'path';
|
|
4
5
|
import { spawn } from 'child_process';
|
|
5
|
-
import { readFile, access, readdir, writeFile } from 'fs/promises';
|
|
6
6
|
|
|
7
7
|
var defaultCommandRunner = (command, options) => {
|
|
8
8
|
return new Promise((resolve2, reject) => {
|
|
@@ -78,6 +78,62 @@ function formatShellCommand(command) {
|
|
|
78
78
|
return [command.command, ...command.args].join(" ");
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// src/provider-detection.ts
|
|
82
|
+
function escapeRegExp(value) {
|
|
83
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
84
|
+
}
|
|
85
|
+
function maskNonCode(source, maskStrings = true) {
|
|
86
|
+
return source.replace(
|
|
87
|
+
/<!--[\s\S]*?-->|\/\*[\s\S]*?\*\/|\/\/[^\r\n]*|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`/g,
|
|
88
|
+
(match) => {
|
|
89
|
+
const isString = ['"', "'", "`"].includes(match[0] ?? "");
|
|
90
|
+
return !maskStrings && isString ? match : match.replace(/[^\r\n]/g, " ");
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
function runtimeBindings(source, code, packageName, exported) {
|
|
95
|
+
const names = [];
|
|
96
|
+
const namespaces = [];
|
|
97
|
+
const importSource = maskNonCode(source, false);
|
|
98
|
+
const pattern = /^import\s+([^;]*?)\s+from\s*(["'])([^"']+)\2/;
|
|
99
|
+
for (const position of code.matchAll(/\bimport\b/g)) {
|
|
100
|
+
const match = importSource.slice(position.index).match(pattern);
|
|
101
|
+
if (!match || match[3] !== packageName) continue;
|
|
102
|
+
const bindings = maskNonCode(match[1] ?? "").trim();
|
|
103
|
+
if (/^type\b/.test(bindings)) continue;
|
|
104
|
+
const namespace = bindings.match(/\*\s+as\s+([A-Za-z_$][\w$]*)/);
|
|
105
|
+
if (namespace?.[1]) namespaces.push(namespace[1]);
|
|
106
|
+
for (const binding of (bindings.match(/\{([^}]*)\}/)?.[1] ?? "").split(",")) {
|
|
107
|
+
const named = binding.trim().match(new RegExp(`^${escapeRegExp(exported)}(?:\\s+as\\s+([A-Za-z_$][\\w$]*))?$`));
|
|
108
|
+
if (named) names.push(named[1] ?? exported);
|
|
109
|
+
}
|
|
110
|
+
if (packageName === "react") {
|
|
111
|
+
const defaultBinding = bindings.match(/^([A-Za-z_$][\w$]*)\s*(?:,|$)/);
|
|
112
|
+
if (defaultBinding?.[1]) namespaces.push(defaultBinding[1]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...names, ...namespaces.map((name) => `${name}.${exported}`)];
|
|
116
|
+
}
|
|
117
|
+
function hasConfiguredProvider(source, adapter) {
|
|
118
|
+
const code = maskNonCode(source);
|
|
119
|
+
const providers = runtimeBindings(source, code, `@marwes-ui/${adapter}`, "MarwesProvider");
|
|
120
|
+
const renderers = adapter === "react" ? runtimeBindings(source, code, "react", "createElement") : adapter === "vue" ? runtimeBindings(source, code, "vue", "h") : [];
|
|
121
|
+
return providers.some((provider) => {
|
|
122
|
+
const tags = [provider];
|
|
123
|
+
if (adapter === "vue" && !provider.includes(".")) {
|
|
124
|
+
tags.push(provider.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase());
|
|
125
|
+
}
|
|
126
|
+
if (tags.some((name) => new RegExp(`<\\s*${escapeRegExp(name)}(?:\\s|/?>)`).test(code))) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return renderers.some(
|
|
130
|
+
(renderer) => new RegExp(
|
|
131
|
+
`(?:^|[^\\w$.])${escapeRegExp(renderer)}\\s*\\(\\s*${escapeRegExp(provider)}\\s*[,)]`
|
|
132
|
+
).test(code)
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
81
137
|
// src/recipes.ts
|
|
82
138
|
var adapters = ["react", "vue", "svelte"];
|
|
83
139
|
var packageManagers = ["pnpm", "npm", "yarn", "bun"];
|
|
@@ -173,6 +229,8 @@ function supportedTemplatesLabel() {
|
|
|
173
229
|
|
|
174
230
|
// src/doctor.ts
|
|
175
231
|
var sourceExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte", ".css"]);
|
|
232
|
+
var sourceDirectories = ["src", "app", "pages"];
|
|
233
|
+
var knownRootFile = /^(?:main|app|layout|index|root)\.(?:ts|tsx|js|jsx|vue|svelte)$/i;
|
|
176
234
|
function dependencyVersion(packageJson, packageName) {
|
|
177
235
|
return packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
|
|
178
236
|
}
|
|
@@ -186,33 +244,51 @@ async function collectSourceFiles(directory) {
|
|
|
186
244
|
for (const entry of entries) {
|
|
187
245
|
const path = join(directory, entry.name);
|
|
188
246
|
if (entry.isDirectory()) {
|
|
189
|
-
if (
|
|
247
|
+
if (["node_modules", "dist", ".git", ".next", ".svelte-kit"].includes(entry.name)) {
|
|
190
248
|
continue;
|
|
191
249
|
}
|
|
192
250
|
files.push(...await collectSourceFiles(path));
|
|
193
251
|
continue;
|
|
194
252
|
}
|
|
195
|
-
|
|
196
|
-
if (sourceExtensions.has(extension)) {
|
|
253
|
+
if (sourceExtensions.has(extname(entry.name))) {
|
|
197
254
|
files.push(path);
|
|
198
255
|
}
|
|
199
256
|
}
|
|
200
257
|
return files;
|
|
201
258
|
}
|
|
259
|
+
async function readProjectSources(cwd) {
|
|
260
|
+
const paths = /* @__PURE__ */ new Set();
|
|
261
|
+
for (const directory of sourceDirectories) {
|
|
262
|
+
for (const file of await collectSourceFiles(join(cwd, directory))) {
|
|
263
|
+
paths.add(file);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const rootEntries = await readdir(cwd, { withFileTypes: true }).catch(() => []);
|
|
267
|
+
for (const entry of rootEntries) {
|
|
268
|
+
if (entry.isFile() && knownRootFile.test(entry.name)) {
|
|
269
|
+
paths.add(join(cwd, entry.name));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return Promise.all(
|
|
273
|
+
[...paths].map(async (path) => ({ path, source: await readFile(path, "utf8") }))
|
|
274
|
+
);
|
|
275
|
+
}
|
|
202
276
|
function detectInstalledAdapter(packageJson) {
|
|
203
277
|
return adapters.find(
|
|
204
278
|
(adapter) => dependencyVersion(packageJson, getAdapterRecipe(adapter).packageName)
|
|
205
279
|
);
|
|
206
280
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
281
|
+
function escapeRegExp2(value) {
|
|
282
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
283
|
+
}
|
|
284
|
+
function providerSetupFile(sources, adapter) {
|
|
285
|
+
return sources.find((file) => hasConfiguredProvider(file.source, adapter));
|
|
286
|
+
}
|
|
287
|
+
function sourceContains(sources, needle) {
|
|
288
|
+
return sources.some((file) => file.source.includes(needle));
|
|
289
|
+
}
|
|
290
|
+
function sourceMatches(sources, pattern) {
|
|
291
|
+
return sources.some((file) => pattern.test(file.source));
|
|
216
292
|
}
|
|
217
293
|
function formatItem(item) {
|
|
218
294
|
const prefix = item.level === "pass" ? "[pass]" : item.level === "warn" ? "[warn]" : "[fail]";
|
|
@@ -225,7 +301,9 @@ async function runDoctor(options = {}) {
|
|
|
225
301
|
const runner = options.runner ?? defaultCommandRunner;
|
|
226
302
|
const packageJson = await readPackageJson(cwd);
|
|
227
303
|
const adapter = options.adapter ?? detectInstalledAdapter(packageJson);
|
|
304
|
+
const sources = await readProjectSources(cwd);
|
|
228
305
|
const items = [];
|
|
306
|
+
let commandFailureCode;
|
|
229
307
|
if (!adapter) {
|
|
230
308
|
items.push({
|
|
231
309
|
level: "fail",
|
|
@@ -245,12 +323,16 @@ async function runDoctor(options = {}) {
|
|
|
245
323
|
});
|
|
246
324
|
}
|
|
247
325
|
}
|
|
248
|
-
const
|
|
326
|
+
const providerFile = providerSetupFile(sources, adapter);
|
|
327
|
+
const providerMentioned = sourceContains(sources, "MarwesProvider");
|
|
249
328
|
items.push(
|
|
250
|
-
|
|
251
|
-
level: "
|
|
252
|
-
message:
|
|
253
|
-
|
|
329
|
+
providerFile ? {
|
|
330
|
+
level: "pass",
|
|
331
|
+
message: `MarwesProvider is imported from ${recipe.packageName} and rendered in ${basename(providerFile.path)}.`
|
|
332
|
+
} : {
|
|
333
|
+
level: "fail",
|
|
334
|
+
message: providerMentioned ? `MarwesProvider is not both imported from ${recipe.packageName} and rendered in the same app entry file.` : "MarwesProvider was not rendered in app source.",
|
|
335
|
+
fix: `Wrap the app root with MarwesProvider from ${recipe.packageName}. Checked src/, app/, pages/, and known root entry files.`
|
|
254
336
|
}
|
|
255
337
|
);
|
|
256
338
|
}
|
|
@@ -262,8 +344,16 @@ async function runDoctor(options = {}) {
|
|
|
262
344
|
fix: "App projects should import from the public adapter package instead."
|
|
263
345
|
});
|
|
264
346
|
}
|
|
347
|
+
const escapedPackage = escapeRegExp2(internalPackage);
|
|
348
|
+
if (sourceMatches(sources, new RegExp(`(?:from\\s*|import\\s*)["']${escapedPackage}(?:["'/])`))) {
|
|
349
|
+
items.push({
|
|
350
|
+
level: "warn",
|
|
351
|
+
message: `Direct source import from ${internalPackage} found.`,
|
|
352
|
+
fix: "Import consumer APIs from the framework adapter package instead."
|
|
353
|
+
});
|
|
354
|
+
}
|
|
265
355
|
}
|
|
266
|
-
if (
|
|
356
|
+
if (sourceMatches(sources, /["']@marwes-ui\/presets\/[^"']*styles\.css["']/)) {
|
|
267
357
|
items.push({
|
|
268
358
|
level: "warn",
|
|
269
359
|
message: "Manual Marwes preset stylesheet import found.",
|
|
@@ -272,22 +362,34 @@ async function runDoctor(options = {}) {
|
|
|
272
362
|
}
|
|
273
363
|
if (options.runBuild) {
|
|
274
364
|
const packageManager = options.packageManager ?? await detectPackageManager(cwd);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
365
|
+
if (!packageJson.scripts?.build) {
|
|
366
|
+
items.push({
|
|
367
|
+
level: "fail",
|
|
368
|
+
message: "No build script found to run.",
|
|
369
|
+
fix: "Add a production build script to package.json, then rerun marwes doctor --run-build."
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
const scripts = [
|
|
373
|
+
...packageJson.scripts?.typecheck ? ["typecheck"] : [],
|
|
374
|
+
...packageJson.scripts?.build ? ["build"] : []
|
|
375
|
+
];
|
|
376
|
+
for (const scriptName of scripts) {
|
|
279
377
|
const command = runScriptCommand(packageManager, scriptName);
|
|
280
378
|
write(`Running ${formatShellCommand(command)}`);
|
|
281
379
|
const exitCode3 = await runner(command, { cwd });
|
|
380
|
+
if (exitCode3 !== 0) {
|
|
381
|
+
commandFailureCode = exitCode3;
|
|
382
|
+
}
|
|
282
383
|
items.push(
|
|
283
|
-
exitCode3 === 0 ? { level: "pass", message: `${scriptName} completed successfully.` } : { level: "fail", message: `${scriptName} failed.` }
|
|
384
|
+
exitCode3 === 0 ? { level: "pass", message: `${scriptName} completed successfully.` } : { level: "fail", message: `${scriptName} failed with exit code ${exitCode3}.` }
|
|
284
385
|
);
|
|
386
|
+
if (exitCode3 !== 0) break;
|
|
285
387
|
}
|
|
286
388
|
}
|
|
287
389
|
for (const item of items) {
|
|
288
390
|
write(formatItem(item));
|
|
289
391
|
}
|
|
290
|
-
const exitCode2 = items.some((item) => item.level === "fail") ? 1 : 0;
|
|
392
|
+
const exitCode2 = commandFailureCode ?? (items.some((item) => item.level === "fail") ? 1 : 0);
|
|
291
393
|
return { ...adapter ? { adapter } : {}, items, exitCode: exitCode2 };
|
|
292
394
|
}
|
|
293
395
|
function adapterFromString(value) {
|
|
@@ -344,25 +446,39 @@ async function patchReact(cwd, dryRun) {
|
|
|
344
446
|
if (!relativeFile) {
|
|
345
447
|
return {
|
|
346
448
|
adapter: "react",
|
|
449
|
+
status: "manual-action-required",
|
|
347
450
|
changed: false,
|
|
348
451
|
dryRun,
|
|
452
|
+
searchedFiles: candidates,
|
|
349
453
|
message: "No React Vite entrypoint found. Expected src/main.tsx or src/main.jsx."
|
|
350
454
|
};
|
|
351
455
|
}
|
|
352
456
|
const file = join(cwd, relativeFile);
|
|
353
457
|
const source = await readFile(file, "utf8");
|
|
354
|
-
if (source
|
|
458
|
+
if (hasConfiguredProvider(source, "react")) {
|
|
355
459
|
return {
|
|
356
460
|
adapter: "react",
|
|
461
|
+
status: "already-configured",
|
|
357
462
|
changed: false,
|
|
358
463
|
dryRun,
|
|
359
464
|
file: relativeFile,
|
|
360
465
|
message: "React entrypoint already references MarwesProvider."
|
|
361
466
|
};
|
|
362
467
|
}
|
|
468
|
+
if (source.includes("MarwesProvider")) {
|
|
469
|
+
return {
|
|
470
|
+
adapter: "react",
|
|
471
|
+
status: "manual-action-required",
|
|
472
|
+
changed: false,
|
|
473
|
+
dryRun,
|
|
474
|
+
file: relativeFile,
|
|
475
|
+
message: "React entrypoint references MarwesProvider but not with a verified @marwes-ui/react import and render."
|
|
476
|
+
};
|
|
477
|
+
}
|
|
363
478
|
if (!source.includes("<App />")) {
|
|
364
479
|
return {
|
|
365
480
|
adapter: "react",
|
|
481
|
+
status: "manual-action-required",
|
|
366
482
|
changed: false,
|
|
367
483
|
dryRun,
|
|
368
484
|
file: relativeFile,
|
|
@@ -377,6 +493,7 @@ async function patchReact(cwd, dryRun) {
|
|
|
377
493
|
const changed = await writeIfNeeded(file, next, dryRun);
|
|
378
494
|
return {
|
|
379
495
|
adapter: "react",
|
|
496
|
+
status: changed ? "applied" : "already-configured",
|
|
380
497
|
changed,
|
|
381
498
|
dryRun,
|
|
382
499
|
file: relativeFile,
|
|
@@ -406,25 +523,39 @@ async function patchVue(cwd, dryRun) {
|
|
|
406
523
|
if (!await fileExists(file)) {
|
|
407
524
|
return {
|
|
408
525
|
adapter: "vue",
|
|
526
|
+
status: "manual-action-required",
|
|
409
527
|
changed: false,
|
|
410
528
|
dryRun,
|
|
529
|
+
searchedFiles: [relativeFile],
|
|
411
530
|
message: "No Vue starter component found. Expected src/App.vue."
|
|
412
531
|
};
|
|
413
532
|
}
|
|
414
533
|
const source = await readFile(file, "utf8");
|
|
415
|
-
if (source
|
|
534
|
+
if (hasConfiguredProvider(source, "vue")) {
|
|
416
535
|
return {
|
|
417
536
|
adapter: "vue",
|
|
537
|
+
status: "already-configured",
|
|
418
538
|
changed: false,
|
|
419
539
|
dryRun,
|
|
420
540
|
file: relativeFile,
|
|
421
541
|
message: "Vue starter component already contains MarwesProvider."
|
|
422
542
|
};
|
|
423
543
|
}
|
|
544
|
+
if (source.includes("MarwesProvider")) {
|
|
545
|
+
return {
|
|
546
|
+
adapter: "vue",
|
|
547
|
+
status: "manual-action-required",
|
|
548
|
+
changed: false,
|
|
549
|
+
dryRun,
|
|
550
|
+
file: relativeFile,
|
|
551
|
+
message: "Vue starter references MarwesProvider but not with a verified @marwes-ui/vue import and render."
|
|
552
|
+
};
|
|
553
|
+
}
|
|
424
554
|
const templateMatch = source.match(/<template>([\s\S]*?)<\/template>/);
|
|
425
555
|
if (!templateMatch) {
|
|
426
556
|
return {
|
|
427
557
|
adapter: "vue",
|
|
558
|
+
status: "manual-action-required",
|
|
428
559
|
changed: false,
|
|
429
560
|
dryRun,
|
|
430
561
|
file: relativeFile,
|
|
@@ -435,6 +566,7 @@ async function patchVue(cwd, dryRun) {
|
|
|
435
566
|
if (!innerTemplate) {
|
|
436
567
|
return {
|
|
437
568
|
adapter: "vue",
|
|
569
|
+
status: "manual-action-required",
|
|
438
570
|
changed: false,
|
|
439
571
|
dryRun,
|
|
440
572
|
file: relativeFile,
|
|
@@ -453,6 +585,7 @@ async function patchVue(cwd, dryRun) {
|
|
|
453
585
|
const changed = await writeIfNeeded(file, next, dryRun);
|
|
454
586
|
return {
|
|
455
587
|
adapter: "vue",
|
|
588
|
+
status: changed ? "applied" : "already-configured",
|
|
456
589
|
changed,
|
|
457
590
|
dryRun,
|
|
458
591
|
file: relativeFile,
|
|
@@ -485,27 +618,41 @@ async function patchSvelte(cwd, dryRun) {
|
|
|
485
618
|
if (!await fileExists(file)) {
|
|
486
619
|
return {
|
|
487
620
|
adapter: "svelte",
|
|
621
|
+
status: "manual-action-required",
|
|
488
622
|
changed: false,
|
|
489
623
|
dryRun,
|
|
624
|
+
searchedFiles: [relativeFile],
|
|
490
625
|
message: "No Svelte starter component found. Expected src/App.svelte."
|
|
491
626
|
};
|
|
492
627
|
}
|
|
493
628
|
const source = await readFile(file, "utf8");
|
|
494
|
-
if (source
|
|
629
|
+
if (hasConfiguredProvider(source, "svelte")) {
|
|
495
630
|
return {
|
|
496
631
|
adapter: "svelte",
|
|
632
|
+
status: "already-configured",
|
|
497
633
|
changed: false,
|
|
498
634
|
dryRun,
|
|
499
635
|
file: relativeFile,
|
|
500
636
|
message: "Svelte starter component already contains MarwesProvider."
|
|
501
637
|
};
|
|
502
638
|
}
|
|
639
|
+
if (source.includes("MarwesProvider")) {
|
|
640
|
+
return {
|
|
641
|
+
adapter: "svelte",
|
|
642
|
+
status: "manual-action-required",
|
|
643
|
+
changed: false,
|
|
644
|
+
dryRun,
|
|
645
|
+
file: relativeFile,
|
|
646
|
+
message: "Svelte starter references MarwesProvider but not with a verified @marwes-ui/svelte import and render."
|
|
647
|
+
};
|
|
648
|
+
}
|
|
503
649
|
const scriptBlocks = blockMatches(source, "script");
|
|
504
650
|
const styleBlocks = blockMatches(source, "style");
|
|
505
651
|
const body = removeBlocks(source, [...scriptBlocks, ...styleBlocks]).trim();
|
|
506
652
|
if (!body) {
|
|
507
653
|
return {
|
|
508
654
|
adapter: "svelte",
|
|
655
|
+
status: "manual-action-required",
|
|
509
656
|
changed: false,
|
|
510
657
|
dryRun,
|
|
511
658
|
file: relativeFile,
|
|
@@ -527,6 +674,7 @@ ${styles}` : ""
|
|
|
527
674
|
`, dryRun);
|
|
528
675
|
return {
|
|
529
676
|
adapter: "svelte",
|
|
677
|
+
status: changed ? "applied" : "already-configured",
|
|
530
678
|
changed,
|
|
531
679
|
dryRun,
|
|
532
680
|
file: relativeFile,
|
|
@@ -543,23 +691,79 @@ async function patchProject(cwd, adapter, dryRun) {
|
|
|
543
691
|
return patchSvelte(cwd, dryRun);
|
|
544
692
|
}
|
|
545
693
|
|
|
546
|
-
// src/
|
|
547
|
-
function
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
694
|
+
// src/setup-guidance.ts
|
|
695
|
+
function providerExample(adapter) {
|
|
696
|
+
const packageName = getAdapterRecipe(adapter).packageName;
|
|
697
|
+
const providerImport = `import { MarwesProvider, type ThemeInput } from "${packageName}"`;
|
|
698
|
+
const theme = 'const brandTheme = { color: { primary: "#2457FF" } } satisfies ThemeInput';
|
|
699
|
+
if (adapter === "react") {
|
|
700
|
+
return [
|
|
701
|
+
'import { createRoot } from "react-dom/client"',
|
|
702
|
+
providerImport,
|
|
703
|
+
'import App from "./App"',
|
|
704
|
+
"",
|
|
705
|
+
theme,
|
|
706
|
+
"",
|
|
707
|
+
'createRoot(document.getElementById("root")!).render(',
|
|
708
|
+
" <MarwesProvider theme={brandTheme}>",
|
|
709
|
+
" <App />",
|
|
710
|
+
" </MarwesProvider>,",
|
|
711
|
+
")"
|
|
712
|
+
].join("\n");
|
|
554
713
|
}
|
|
555
|
-
return
|
|
714
|
+
return [
|
|
715
|
+
adapter === "vue" ? '<script setup lang="ts">' : '<script lang="ts">',
|
|
716
|
+
` ${providerImport}`,
|
|
717
|
+
` ${theme}`,
|
|
718
|
+
"</script>",
|
|
719
|
+
"",
|
|
720
|
+
...adapter === "vue" ? [
|
|
721
|
+
"<template>",
|
|
722
|
+
' <MarwesProvider :theme="brandTheme">',
|
|
723
|
+
" <main>Your app</main>",
|
|
724
|
+
" </MarwesProvider>",
|
|
725
|
+
"</template>"
|
|
726
|
+
] : ["<MarwesProvider theme={brandTheme}>", " <main>Your app</main>", "</MarwesProvider>"]
|
|
727
|
+
].join("\n");
|
|
556
728
|
}
|
|
557
|
-
function
|
|
729
|
+
function setupGuidance(adapter, fullExample) {
|
|
558
730
|
const recipe = getAdapterRecipe(adapter);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
731
|
+
const ssrIntegration = { react: "next", vue: "nuxt", svelte: "sveltekit" }[adapter];
|
|
732
|
+
const hookGuidance = {
|
|
733
|
+
react: "useTheme() returns resolved concrete values: const theme = useTheme(); read theme.color.primary.base.",
|
|
734
|
+
vue: "useTheme() returns a resolved snapshot directly, not a ref: const theme = useTheme(); read theme.color.primary.base (no .value).",
|
|
735
|
+
svelte: "useTheme() returns a .theme getter: const themeState = useTheme(); read themeState.theme.color.primary.base. Keep the getter object; destructuring .theme takes a snapshot."
|
|
736
|
+
}[adapter];
|
|
737
|
+
return [
|
|
738
|
+
"Marwes setup guidance:",
|
|
739
|
+
`- Import Marwes APIs only from ${recipe.packageName}.`,
|
|
740
|
+
"- Do not install or import @marwes-ui/core or @marwes-ui/presets directly.",
|
|
741
|
+
"- Default preset CSS loads automatically from the adapter; do not add a separate Marwes stylesheet import.",
|
|
742
|
+
"- Wrap the app once with MarwesProvider; pass a small typed ThemeInput override. Unspecified values retain defaults.",
|
|
743
|
+
"- --mw-* CSS variables are scoped to the provider and its descendants, not :root. Keep app styles and runtime consumers below the provider.",
|
|
744
|
+
"- Use mwThemeVars for CSS var(...) references, not concrete JavaScript values. Call useTheme() only in a child component below MarwesProvider.",
|
|
745
|
+
`- ${hookGuidance}`,
|
|
746
|
+
...fullExample ? [
|
|
747
|
+
`Complete ${recipe.displayName} provider example:`,
|
|
748
|
+
providerExample(adapter),
|
|
749
|
+
`Component example: import { PrimaryButton } from "${recipe.packageName}"; render <PrimaryButton>Save</PrimaryButton> below the provider. Use public components, not invented mw-* replacement classes.`,
|
|
750
|
+
`Token example: import { mwThemeVars } from "${recipe.packageName}"; use mwThemeVars.color.text and mwThemeVars.spacing.sp24 in app-owned styles below the provider.`,
|
|
751
|
+
"For SSR, follow the framework guide for matching style/script helpers and provider options before hydration; changing html classes alone is not no-flash setup."
|
|
752
|
+
] : [],
|
|
753
|
+
`Setup guide: https://marwes.io/docs/get-started/${adapter}/`,
|
|
754
|
+
"Theming guide: https://marwes.io/docs/theming/",
|
|
755
|
+
`SSR guide: https://marwes.io/docs/integrations/${ssrIntegration}/`,
|
|
756
|
+
...fullExample ? [
|
|
757
|
+
"AI discovery: https://marwes.io/llms.txt",
|
|
758
|
+
`AI framework guide: https://marwes.io/ai/${adapter}.md`,
|
|
759
|
+
"Public export inventory: https://marwes.io/ai/v1/public-api.json"
|
|
760
|
+
] : []
|
|
761
|
+
];
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/init.ts
|
|
765
|
+
function shouldPrintProviderGuidance(patch) {
|
|
766
|
+
return patch?.status === "manual-action-required";
|
|
563
767
|
}
|
|
564
768
|
async function runInit(options) {
|
|
565
769
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -586,7 +790,12 @@ async function runInit(options) {
|
|
|
586
790
|
} else {
|
|
587
791
|
exitCode2 = await runner(command, { cwd });
|
|
588
792
|
if (exitCode2 !== 0) {
|
|
793
|
+
write("Marwes init status: failed.");
|
|
794
|
+
write(
|
|
795
|
+
`Marwes init failed: installation command exited with code ${exitCode2}. Provider patching and doctor were not run.`
|
|
796
|
+
);
|
|
589
797
|
return {
|
|
798
|
+
status: "failed",
|
|
590
799
|
adapter: options.adapter,
|
|
591
800
|
packageManager,
|
|
592
801
|
installCommand: commandLabel,
|
|
@@ -603,29 +812,47 @@ async function runInit(options) {
|
|
|
603
812
|
patch = await patchProject(cwd, options.adapter, dryRun);
|
|
604
813
|
const patchLabel = patch.file ? `${patch.file}: ${patch.message}` : patch.message;
|
|
605
814
|
write(dryRun && patch.changed ? `[dry-run] ${patchLabel}` : patchLabel);
|
|
815
|
+
if (patch.searchedFiles?.length) {
|
|
816
|
+
write(`Files searched: ${patch.searchedFiles.join(", ")}`);
|
|
817
|
+
}
|
|
606
818
|
}
|
|
607
819
|
let doctor;
|
|
608
|
-
if (
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
820
|
+
if (dryRun) {
|
|
821
|
+
write(`[dry-run] marwes doctor --adapter ${options.adapter}`);
|
|
822
|
+
} else {
|
|
823
|
+
write("Marwes doctor:");
|
|
824
|
+
doctor = await runDoctor({
|
|
825
|
+
adapter: options.adapter,
|
|
826
|
+
cwd,
|
|
827
|
+
packageManager,
|
|
828
|
+
runner,
|
|
829
|
+
write
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
const providerFailure = doctor?.items.some(
|
|
833
|
+
(item) => item.level === "fail" && item.message.startsWith("MarwesProvider")
|
|
834
|
+
) === true;
|
|
835
|
+
const otherFailure = doctor?.items.some(
|
|
836
|
+
(item) => item.level === "fail" && !item.message.startsWith("MarwesProvider")
|
|
837
|
+
) === true;
|
|
838
|
+
const needsManualAction = providerFailure && (shouldPrintProviderGuidance(patch) || noPatch);
|
|
839
|
+
const status = otherFailure ? "failed" : needsManualAction ? "manual-action-required" : doctor && doctor.exitCode !== 0 ? "failed" : "complete";
|
|
840
|
+
if (needsManualAction) {
|
|
841
|
+
write(
|
|
842
|
+
`Manual follow-up: wrap the app root with ${recipe.providerImport} from ${recipe.packageName}.`
|
|
843
|
+
);
|
|
844
|
+
write("Automatic patching currently supports the standard Vite app layout.");
|
|
845
|
+
}
|
|
846
|
+
if (agentic || needsManualAction || status === "complete") {
|
|
847
|
+
for (const line of setupGuidance(options.adapter, agentic || needsManualAction)) write(line);
|
|
626
848
|
}
|
|
627
|
-
|
|
849
|
+
exitCode2 = status === "manual-action-required" ? 2 : status === "failed" ? doctor?.exitCode ?? 1 : 0;
|
|
850
|
+
write(dryRun ? `[dry-run] Planned init status: ${status}.` : `Marwes init status: ${status}.`);
|
|
851
|
+
write(
|
|
852
|
+
dryRun ? "[dry-run] Marwes init plan complete. No installation, file changes, or doctor checks performed." : status === "complete" ? "Marwes init complete." : status === "manual-action-required" ? "Marwes provider wiring requires manual action (exit code 2)." : "Marwes init failed."
|
|
853
|
+
);
|
|
628
854
|
return {
|
|
855
|
+
status,
|
|
629
856
|
adapter: options.adapter,
|
|
630
857
|
packageManager,
|
|
631
858
|
installCommand: commandLabel,
|
|
@@ -638,10 +865,35 @@ async function runInit(options) {
|
|
|
638
865
|
}
|
|
639
866
|
|
|
640
867
|
// src/create.ts
|
|
868
|
+
var safeProjectPathPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]*(?:\/[a-zA-Z0-9][a-zA-Z0-9._-]*)*$/;
|
|
869
|
+
function isSafeProjectPath(projectName) {
|
|
870
|
+
return safeProjectPathPattern.test(projectName);
|
|
871
|
+
}
|
|
872
|
+
async function stageAdapterDependency(cwd, adapter) {
|
|
873
|
+
const packageJsonPath = resolve(cwd, "package.json");
|
|
874
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
875
|
+
const packageName = getAdapterRecipe(adapter).packageName;
|
|
876
|
+
const dependencies = packageJson.dependencies ?? {};
|
|
877
|
+
if (!(packageName in dependencies) && !(packageName in (packageJson.devDependencies ?? {}))) {
|
|
878
|
+
packageJson.dependencies = { ...dependencies, [packageName]: "latest" };
|
|
879
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
|
|
880
|
+
`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
641
883
|
async function runCreate(options) {
|
|
642
884
|
const cwd = options.cwd ?? process.cwd();
|
|
643
885
|
const write = options.write ?? ((message) => console.log(message));
|
|
644
886
|
const runner = options.runner ?? defaultCommandRunner;
|
|
887
|
+
if (!isSafeProjectPath(options.projectName)) {
|
|
888
|
+
write(
|
|
889
|
+
"Invalid project name. Use a relative path made from letters, numbers, dots, dashes, underscores, and forward slashes."
|
|
890
|
+
);
|
|
891
|
+
return {
|
|
892
|
+
projectName: options.projectName,
|
|
893
|
+
template: options.template,
|
|
894
|
+
exitCode: 1
|
|
895
|
+
};
|
|
896
|
+
}
|
|
645
897
|
const packageManager = options.packageManager ?? await detectPackageManager(cwd);
|
|
646
898
|
const command = createViteCommand(packageManager, options.projectName, options.template);
|
|
647
899
|
const adapter = adapterFromTemplate(options.template);
|
|
@@ -665,6 +917,12 @@ async function runCreate(options) {
|
|
|
665
917
|
exitCode: createExitCode
|
|
666
918
|
};
|
|
667
919
|
}
|
|
920
|
+
if (options.noInstall) {
|
|
921
|
+
await stageAdapterDependency(targetCwd, adapter);
|
|
922
|
+
write(
|
|
923
|
+
`Recorded ${getAdapterRecipe(adapter).packageName} in package.json. Run ${packageManager} install before starting the app.`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
668
926
|
const initResult = await runInit({
|
|
669
927
|
adapter,
|
|
670
928
|
cwd: targetCwd,
|
|
@@ -694,10 +952,65 @@ Options:
|
|
|
694
952
|
--pm <${supportedPackageManagersLabel()}>
|
|
695
953
|
--agentic
|
|
696
954
|
--dry-run
|
|
697
|
-
--no-install
|
|
955
|
+
--no-install Record dependencies but skip installation
|
|
698
956
|
--no-patch
|
|
699
957
|
`);
|
|
700
958
|
}
|
|
959
|
+
function wantsHelp(args) {
|
|
960
|
+
return args.includes("--help") || args.includes("-h") || args[0] === "help";
|
|
961
|
+
}
|
|
962
|
+
function writeInitHelp(write) {
|
|
963
|
+
write(`marwes init --adapter <${supportedAdaptersLabel()}>
|
|
964
|
+
|
|
965
|
+
Install a framework adapter, wire MarwesProvider into a standard Vite app, then run doctor.
|
|
966
|
+
|
|
967
|
+
Options:
|
|
968
|
+
--adapter <${supportedAdaptersLabel()}> Required framework adapter
|
|
969
|
+
--pm <${supportedPackageManagersLabel()}> Package manager override
|
|
970
|
+
--agentic Print AI-agent boundary rules
|
|
971
|
+
--dry-run Show changes without writing or installing
|
|
972
|
+
--no-install Skip dependency installation
|
|
973
|
+
--no-patch Skip automatic provider wiring
|
|
974
|
+
--yes Accept defaults
|
|
975
|
+
--help Show this help
|
|
976
|
+
`);
|
|
977
|
+
}
|
|
978
|
+
function writeDoctorHelp(write) {
|
|
979
|
+
write(`marwes doctor [--adapter <${supportedAdaptersLabel()}>] [--run-build]
|
|
980
|
+
|
|
981
|
+
Verify dependencies, adapter imports, provider rendering, and optional project compilation.
|
|
982
|
+
|
|
983
|
+
Options:
|
|
984
|
+
--adapter <${supportedAdaptersLabel()}> Framework adapter override
|
|
985
|
+
--pm <${supportedPackageManagersLabel()}> Package manager override
|
|
986
|
+
--run-build Run optional typecheck, then the required production build
|
|
987
|
+
--help Show this help
|
|
988
|
+
`);
|
|
989
|
+
}
|
|
990
|
+
function writeAiPromptHelp(write) {
|
|
991
|
+
write(`marwes ai-prompt --adapter <${supportedAdaptersLabel()}>
|
|
992
|
+
|
|
993
|
+
Print a framework-specific setup prompt for an AI coding agent.
|
|
994
|
+
|
|
995
|
+
Options:
|
|
996
|
+
--adapter <${supportedAdaptersLabel()}> Required framework adapter
|
|
997
|
+
--pm <${supportedPackageManagersLabel()}> Package manager used in the prompt
|
|
998
|
+
--help Show this help
|
|
999
|
+
`);
|
|
1000
|
+
}
|
|
1001
|
+
function writeCreateHelp(write) {
|
|
1002
|
+
write(`create-marwes <name> --template <${supportedTemplatesLabel()}>
|
|
1003
|
+
|
|
1004
|
+
Create a new Vite app and configure its Marwes adapter.
|
|
1005
|
+
|
|
1006
|
+
Options:
|
|
1007
|
+
--template <${supportedTemplatesLabel()}>
|
|
1008
|
+
--pm <${supportedPackageManagersLabel()}>
|
|
1009
|
+
--dry-run
|
|
1010
|
+
--no-install
|
|
1011
|
+
--help
|
|
1012
|
+
`);
|
|
1013
|
+
}
|
|
701
1014
|
function parsePackageManager(value) {
|
|
702
1015
|
return typeof value === "string" && isPackageManager(value) ? value : void 0;
|
|
703
1016
|
}
|
|
@@ -709,6 +1022,10 @@ async function runMarwesCli(argv, options = {}) {
|
|
|
709
1022
|
return 0;
|
|
710
1023
|
}
|
|
711
1024
|
if (command === "init") {
|
|
1025
|
+
if (wantsHelp(args)) {
|
|
1026
|
+
writeInitHelp(write);
|
|
1027
|
+
return 0;
|
|
1028
|
+
}
|
|
712
1029
|
const parsed = parseArgs({
|
|
713
1030
|
args,
|
|
714
1031
|
options: {
|
|
@@ -747,6 +1064,10 @@ async function runMarwesCli(argv, options = {}) {
|
|
|
747
1064
|
return result.exitCode;
|
|
748
1065
|
}
|
|
749
1066
|
if (command === "doctor") {
|
|
1067
|
+
if (wantsHelp(args)) {
|
|
1068
|
+
writeDoctorHelp(write);
|
|
1069
|
+
return 0;
|
|
1070
|
+
}
|
|
750
1071
|
const parsed = parseArgs({
|
|
751
1072
|
args,
|
|
752
1073
|
options: {
|
|
@@ -775,6 +1096,10 @@ async function runMarwesCli(argv, options = {}) {
|
|
|
775
1096
|
return result.exitCode;
|
|
776
1097
|
}
|
|
777
1098
|
if (command === "ai-prompt") {
|
|
1099
|
+
if (wantsHelp(args)) {
|
|
1100
|
+
writeAiPromptHelp(write);
|
|
1101
|
+
return 0;
|
|
1102
|
+
}
|
|
778
1103
|
const parsed = parseArgs({
|
|
779
1104
|
args,
|
|
780
1105
|
options: {
|
|
@@ -800,6 +1125,10 @@ async function runMarwesCli(argv, options = {}) {
|
|
|
800
1125
|
}
|
|
801
1126
|
async function runCreateMarwesCli(argv, options = {}) {
|
|
802
1127
|
const write = options.write ?? ((message) => console.log(message));
|
|
1128
|
+
if (wantsHelp(argv)) {
|
|
1129
|
+
writeCreateHelp(write);
|
|
1130
|
+
return 0;
|
|
1131
|
+
}
|
|
803
1132
|
const parsed = parseArgs({
|
|
804
1133
|
args: argv,
|
|
805
1134
|
options: {
|