@marwes-ui/cli 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from 'util';
2
- import { resolve, join } from 'path';
2
+ import { readFile, readdir, writeFile, access } from 'fs/promises';
3
+ import { basename, resolve, join, extname } from 'path';
3
4
  import { spawn } from 'child_process';
4
- import { readFile, access, readdir, writeFile } from 'fs/promises';
5
5
 
6
6
  // src/index.ts
7
7
  var defaultCommandRunner = (command, options) => {
@@ -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 (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") {
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
- const extension = entry.name.slice(entry.name.lastIndexOf("."));
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
- async function sourceContains(cwd, needle) {
208
- const sourceFiles = await collectSourceFiles(join(cwd, "src"));
209
- for (const file of sourceFiles) {
210
- const source = await readFile(file, "utf8");
211
- if (source.includes(needle)) {
212
- return true;
213
- }
214
- }
215
- return false;
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 hasProvider = await sourceContains(cwd, "MarwesProvider");
326
+ const providerFile = providerSetupFile(sources, adapter);
327
+ const providerMentioned = sourceContains(sources, "MarwesProvider");
249
328
  items.push(
250
- hasProvider ? { level: "pass", message: "MarwesProvider is referenced in app source." } : {
251
- level: "warn",
252
- message: "MarwesProvider was not found in app source.",
253
- fix: `Wrap the app root with MarwesProvider from ${recipe.packageName}.`
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 (await sourceContains(cwd, "@marwes-ui/presets/firstEdition/styles.css")) {
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
- const scriptName = packageJson.scripts?.typecheck ? "typecheck" : packageJson.scripts?.build ? "build" : void 0;
276
- if (!scriptName) {
277
- items.push({ level: "warn", message: "No typecheck or build script found to run." });
278
- } else {
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 exitCode2 = await runner(command, { cwd });
380
+ if (exitCode2 !== 0) {
381
+ commandFailureCode = exitCode2;
382
+ }
282
383
  items.push(
283
- exitCode2 === 0 ? { level: "pass", message: `${scriptName} completed successfully.` } : { level: "fail", message: `${scriptName} failed.` }
384
+ exitCode2 === 0 ? { level: "pass", message: `${scriptName} completed successfully.` } : { level: "fail", message: `${scriptName} failed with exit code ${exitCode2}.` }
284
385
  );
386
+ if (exitCode2 !== 0) break;
285
387
  }
286
388
  }
287
389
  for (const item of items) {
288
390
  write(formatItem(item));
289
391
  }
290
- const exitCode = items.some((item) => item.level === "fail") ? 1 : 0;
392
+ const exitCode = commandFailureCode ?? (items.some((item) => item.level === "fail") ? 1 : 0);
291
393
  return { ...adapter ? { adapter } : {}, items, exitCode };
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.includes("MarwesProvider")) {
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.includes("<MarwesProvider")) {
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.includes("<MarwesProvider")) {
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/init.ts
547
- function providerGuidance(adapter) {
548
- const recipe = getAdapterRecipe(adapter);
549
- return `Manual follow-up: wrap the app root with ${recipe.providerImport} from ${recipe.packageName}.`;
550
- }
551
- function shouldPrintProviderGuidance(patch) {
552
- if (!patch || patch.changed) {
553
- return false;
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 !patch.message.toLowerCase().includes("already");
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 writeAgenticBoundaryRules(adapter, write) {
729
+ function setupGuidance(adapter, fullExample) {
558
730
  const recipe = getAdapterRecipe(adapter);
559
- write("Agentic rules:");
560
- write(`- Import Marwes APIs only from ${recipe.packageName}.`);
561
- write("- Do not install @marwes-ui/core or @marwes-ui/presets directly.");
562
- write("- Do not add a separate Marwes stylesheet import.");
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
  exitCode = await runner(command, { cwd });
588
792
  if (exitCode !== 0) {
793
+ write("Marwes init status: failed.");
794
+ write(
795
+ `Marwes init failed: installation command exited with code ${exitCode}. 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 (agentic) {
609
- if (shouldPrintProviderGuidance(patch) || noPatch) {
610
- write(providerGuidance(options.adapter));
611
- }
612
- writeAgenticBoundaryRules(options.adapter, write);
613
- if (dryRun) {
614
- write(`[dry-run] marwes doctor --adapter ${options.adapter}`);
615
- } else {
616
- write("Marwes doctor:");
617
- doctor = await runDoctor({
618
- adapter: options.adapter,
619
- cwd,
620
- packageManager,
621
- runner,
622
- write
623
- });
624
- exitCode = doctor.exitCode;
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
- write("Marwes init complete.");
849
+ exitCode = 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: {