@salty-css/core 0.1.0-alpha.9 → 0.1.0-feat-define-import.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.
Files changed (41) hide show
  1. package/README.md +44 -0
  2. package/bin/confirm-install.d.ts +34 -0
  3. package/bin/context.d.ts +3 -0
  4. package/bin/integrations/index.d.ts +11 -3
  5. package/bin/integrations/types.d.ts +14 -2
  6. package/bin/main.cjs +149 -48
  7. package/bin/main.js +149 -48
  8. package/{class-name-generator-YeSQe_Ik.js → class-name-generator-CMWY5KTJ.js} +1 -1
  9. package/{class-name-generator-B2Pb2obX.cjs → class-name-generator-DB5aQwC_.cjs} +1 -1
  10. package/compiler/resolve-import.d.ts +17 -0
  11. package/compiler/salty-compiler.cjs +85 -26
  12. package/compiler/salty-compiler.d.ts +7 -1
  13. package/compiler/salty-compiler.js +86 -26
  14. package/config/index.cjs +2 -0
  15. package/config/index.js +3 -1
  16. package/css/dynamic-styles.cjs +1 -1
  17. package/css/dynamic-styles.js +1 -1
  18. package/css/keyframes.cjs +1 -1
  19. package/css/keyframes.js +1 -1
  20. package/factories/define-import.d.ts +14 -0
  21. package/factories/index.cjs +19 -0
  22. package/factories/index.d.ts +1 -0
  23. package/factories/index.js +19 -0
  24. package/generators/index.cjs +1 -1
  25. package/generators/index.js +2 -2
  26. package/instances/classname-instance.cjs +1 -1
  27. package/instances/classname-instance.js +1 -1
  28. package/package.json +1 -1
  29. package/{parse-styles-CA3TP5n1.cjs → parse-styles-C54MOrPg.cjs} +106 -7
  30. package/{parse-styles-BTIoYnBr.js → parse-styles-CLMTHo2H.js} +107 -8
  31. package/parsers/index.cjs +2 -1
  32. package/parsers/index.d.ts +1 -0
  33. package/parsers/index.js +4 -3
  34. package/parsers/parser-regexes.d.ts +3 -0
  35. package/parsers/strict.d.ts +2 -0
  36. package/runtime/index.cjs +1 -1
  37. package/runtime/index.js +1 -1
  38. package/{salty.config-cqavVm2t.cjs → salty.config-DogY_sSQ.cjs} +1 -1
  39. package/salty.config-GV37Q-D2.js +4 -0
  40. package/types/config-types.d.ts +9 -0
  41. package/salty.config-DjosWdPw.js +0 -4
package/README.md CHANGED
@@ -57,6 +57,7 @@ To get help with problems, [Join Salty CSS Discord server](https://discord.gg/R6
57
57
  - [defineVariables](#variables) - create CSS variables (tokens) that can be used in any styling function
58
58
  - [defineMediaQuery](#media-queries) - create CSS media queries and use them in any styling function
59
59
  - [defineTemplates](#templates) - create reusable templates that can be applied when same styles are used over and over again
60
+ - [defineImport](#importing-additional-css) - pull in external CSS files (relative, public, node_modules, or URL)
60
61
  - [keyframes](#keyframes-animations) - create CSS keyframes animation that can be used and imported in any styling function
61
62
 
62
63
  ### Styling helpers & utility
@@ -319,6 +320,49 @@ Example usage:
319
320
  styled('div', { base: { textStyle: 'headline.large', card: '20px' } });
320
321
  ```
321
322
 
323
+ ## Importing additional CSS
324
+
325
+ Use `defineImport` to pull in CSS that lives outside of Salty's authoring API — a reset stylesheet from npm, a Google Fonts URL, an asset in your app's `public/` folder, or a sibling `.css` file. The compiler turns each spec into an `@import` rule in the generated `saltygen/index.css`, so the imported stylesheets travel with the rest of your build.
326
+
327
+ ```ts
328
+ // /styles/imports.css.ts
329
+ import { defineImport } from '@salty-css/core/factories';
330
+
331
+ export default defineImport(
332
+ // Relative to this file
333
+ './reset.css',
334
+ // From node_modules (bare specifier — same as Vite / native CSS @import)
335
+ 'modern-normalize/modern-normalize.css',
336
+ // From node_modules (~ prefix — same resolver, webpack-style)
337
+ '~normalize.css/normalize.css',
338
+ // From your app's public/ folder (served at the host root)
339
+ '/fonts/inter.css',
340
+ // External URL
341
+ 'https://fonts.googleapis.com/css2?family=Inter&display=swap',
342
+ // Object form — attach media or supports() conditions
343
+ { url: './print.css', media: 'print' },
344
+ { url: './p3.css', supports: 'color(display-p3 1 1 1)' }
345
+ );
346
+ ```
347
+
348
+ Path resolution:
349
+
350
+ | Pattern | Behaviour |
351
+ | --------------------------- | ---------------------------------------------------------------------------------------- |
352
+ | `http://`, `https://`, `//` | Emitted verbatim |
353
+ | Starts with `/` | Public-folder URL — emitted verbatim, the browser resolves it against your host |
354
+ | Starts with `./` or `../` | Resolved at build time relative to the file that called `defineImport` |
355
+ | `~package/file.css` | Stripped of the leading `~`, then resolved from `node_modules` and copied into the build |
356
+ | `package/file.css` (bare) | Same `node_modules` resolution as the `~` form |
357
+
358
+ All imports are placed inside a new `imports` cascade layer that sits **before** `reset`, `global`, `templates`, and your component styles. This means your own styles always win over third-party CSS you pull in — which is what most teams expect when they drop in something like `modern-normalize`.
359
+
360
+ The full layer order in the generated `index.css` is:
361
+
362
+ ```css
363
+ @layer imports, reset, global, templates, l0, l1, l2, l3, l4, l5, l6, l7, l8;
364
+ ```
365
+
322
366
  ## Keyframes animations
323
367
 
324
368
  ```ts
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Renders a single package spec for display. Translates the internal
3
+ * `'-D <pkg>@<ver>'` shorthand used by `npmInstall` into a `<pkg>@<ver> (dev)`
4
+ * suffix so the user-facing list reads naturally.
5
+ */
6
+ export declare const formatPackageForDisplay: (spec: string) => string;
7
+ export declare const renderPackageList: (packages: string[]) => string;
8
+ export interface ConfirmInstallOptions {
9
+ /** Streams used for the prompt — defaults to process.stdin/stdout. Allows tests to inject. */
10
+ input?: NodeJS.ReadableStream;
11
+ output?: NodeJS.WritableStream;
12
+ /** Whether the input is a TTY — defaults to process.stdin.isTTY. Allows tests to override. */
13
+ isTTY?: boolean;
14
+ }
15
+ /**
16
+ * Confirm a batched install. Resolves on success, throws on decline.
17
+ *
18
+ * - `yes=true` or empty `packages` → no prompt, resolves immediately.
19
+ * - Non-TTY without `yes` → throws, telling the user to pass `--yes`.
20
+ * - Otherwise prints the list and asks `Proceed? (y/N)`. Accepts y/yes
21
+ * (case-insensitive); anything else throws to abort the command.
22
+ */
23
+ export declare const confirmInstall: (packages: string[], yes: boolean, options?: ConfirmInstallOptions) => Promise<void>;
24
+ export interface ConfirmYesNoOptions extends ConfirmInstallOptions {
25
+ /** When true, resolves true without prompting. */
26
+ yes?: boolean;
27
+ /** When true, an empty answer counts as yes. Defaults to false (empty = no). */
28
+ defaultYes?: boolean;
29
+ }
30
+ /**
31
+ * Generic yes/no prompt. Unlike `confirmInstall`, non-TTY without `yes`
32
+ * resolves `false` instead of throwing — callers can choose policy.
33
+ */
34
+ export declare const confirmYesNo: (question: string, options?: ConfirmYesNoOptions) => Promise<boolean>;
package/bin/context.d.ts CHANGED
@@ -8,11 +8,14 @@ export interface ProjectContext {
8
8
  rcFile: RCFile;
9
9
  cliVersion: string;
10
10
  skipInstall: boolean;
11
+ yes: boolean;
11
12
  }
12
13
  export declare const resolveProjectDir: (dir: string, rootDir?: string) => string;
13
14
  export interface BuildContextOptions {
14
15
  dir: string;
15
16
  skipInstall?: boolean;
17
+ /** Skip the install confirmation prompt and install without asking. */
18
+ yes?: boolean;
16
19
  /** When false, build context even if package.json is missing (used by commands that should not require one). */
17
20
  requirePackageJson?: boolean;
18
21
  }
@@ -1,12 +1,20 @@
1
1
  import { ProjectContext } from '../context';
2
- import { BuildIntegrationAdapter } from './types';
2
+ import { BuildIntegrationAdapter, IntegrationPlan } from './types';
3
3
  export declare const buildIntegrationRegistry: BuildIntegrationAdapter[];
4
- export declare const detectAndApplyIntegrations: (ctx: ProjectContext) => Promise<{
4
+ export interface PlannedIntegration {
5
+ name: string;
6
+ configPath: string;
7
+ plan: IntegrationPlan;
8
+ }
9
+ /** Detect every integration that has work to do and compute its plan. */
10
+ export declare const planIntegrations: (ctx: ProjectContext) => Promise<PlannedIntegration[]>;
11
+ /** Execute each previously-planned integration (writes config files). */
12
+ export declare const applyIntegrationPlans: (planned: PlannedIntegration[]) => Promise<{
5
13
  name: string;
6
14
  configPath: string;
7
15
  changed: boolean;
8
16
  }[]>;
9
- export type { BuildIntegrationAdapter, ConfigEdit } from './types';
17
+ export type { BuildIntegrationAdapter, ConfigEdit, IntegrationPlan } from './types';
10
18
  export { viteIntegration, editViteConfig, vitePackage } from './vite';
11
19
  export { nextIntegration, editNextConfig, nextPackage, nextConfigFiles } from './next';
12
20
  export { astroIntegration, editAstroConfig, astroPackage } from './astro';
@@ -3,12 +3,24 @@ export interface IntegrationApplyResult {
3
3
  /** Whether any file was edited or any install was performed. */
4
4
  changed: boolean;
5
5
  }
6
+ /**
7
+ * Pending integration work — the packages it would like installed and a
8
+ * closure that writes the config edits once the install (if any) has run.
9
+ */
10
+ export interface IntegrationPlan {
11
+ /** Packages to install, using the same `npmInstall` shorthand (e.g. `'-D @salty-css/vite@1.2.3'`). */
12
+ packages: string[];
13
+ execute(): Promise<IntegrationApplyResult>;
14
+ }
6
15
  export interface BuildIntegrationAdapter {
7
16
  name: string;
8
17
  /** Returns the config file path this integration targets, or null when not applicable. */
9
18
  detect(ctx: ProjectContext): Promise<string | null> | string | null;
10
- /** Idempotently wire the integration into the project (edit configs, install dev deps). */
11
- apply(ctx: ProjectContext, configPath: string): Promise<IntegrationApplyResult>;
19
+ /**
20
+ * Idempotently produce the work needed to wire the integration in. Returns
21
+ * null when the integration is already wired (nothing to do).
22
+ */
23
+ plan(ctx: ProjectContext, configPath: string): Promise<IntegrationPlan | null>;
12
24
  }
13
25
  /** Pure transform — returned by an integration when it knows how to rewrite a config file. */
14
26
  export interface ConfigEdit {
package/bin/main.cjs CHANGED
@@ -11,6 +11,7 @@ const child_process = require("child_process");
11
11
  const ora = require("ora");
12
12
  const pascalCase = require("../pascal-case-By_l58S-.cjs");
13
13
  const ejs = require("ejs");
14
+ const promises$1 = require("readline/promises");
14
15
  var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
15
16
  const defaultPackageJsonPath = path.join(process.cwd(), "package.json");
16
17
  const readPackageJson = async (filePath = defaultPackageJsonPath) => {
@@ -141,17 +142,21 @@ const buildContext = async (opts) => {
141
142
  packageJson,
142
143
  rcFile,
143
144
  cliVersion: cliPackageJson.version || "0.0.0",
144
- skipInstall: !!opts.skipInstall
145
+ skipInstall: !!opts.skipInstall,
146
+ yes: !!opts.yes
145
147
  };
146
148
  };
147
149
  const registerBuildCommand = (program, defaultProject) => {
148
- program.command("build [directory]").alias("b").description("Build the Salty-CSS project.").option("-d, --dir <dir>", "Project directory to build the project in.").option("--watch", "Watch for changes and rebuild the project.").action(async function(_dir = defaultProject) {
150
+ program.command("build [directory]").alias("b").description("Build the Salty-CSS project.").option("-d, --dir <dir>", "Project directory to build the project in.").option("--watch", "Watch for changes and rebuild the project.").option("--mode <mode>", 'Build mode: "production" or "development". Defaults to NODE_ENV-based detection.').action(async function(_dir = defaultProject) {
149
151
  compiler_saltyCompiler.logger.info("Building the Salty-CSS project...");
150
- const { dir = _dir, watch } = this.opts();
152
+ const { dir = _dir, watch, mode } = this.opts();
153
+ if (mode !== void 0 && mode !== "production" && mode !== "development") {
154
+ return compiler_saltyCompiler.logError(`Invalid --mode "${mode}". Expected "production" or "development".`);
155
+ }
151
156
  const resolved = dir ?? await getDefaultProject();
152
157
  if (!resolved) return compiler_saltyCompiler.logError("Project directory must be provided. Add it as the first argument after build command or use the --dir option.");
153
158
  const projectDir = resolveProjectDir(resolved);
154
- const compiler = new compiler_saltyCompiler.SaltyCompiler(projectDir);
159
+ const compiler = new compiler_saltyCompiler.SaltyCompiler(projectDir, { mode });
155
160
  await compiler.generateCss();
156
161
  if (watch) {
157
162
  compiler_saltyCompiler.logger.info("Watching for changes in the project directory...");
@@ -225,7 +230,7 @@ const getFramework = (name) => {
225
230
  return frameworksByName[name];
226
231
  };
227
232
  const templateLoaders = {
228
- "salty.config.ts": () => Promise.resolve().then(() => require("../salty.config-cqavVm2t.cjs")),
233
+ "salty.config.ts": () => Promise.resolve().then(() => require("../salty.config-DogY_sSQ.cjs")),
229
234
  "saltygen/index.css": () => Promise.resolve().then(() => require("../index-ByR0nfaf.cjs")),
230
235
  "react/styled-file.ts": () => Promise.resolve().then(() => require("../styled-file-CPd_rTW2.cjs")),
231
236
  "react/vanilla-file.ts": () => Promise.resolve().then(() => require("../vanilla-file-r0fp2q_m.cjs")),
@@ -293,6 +298,52 @@ const registerGenerateCommand = (program, defaultProject) => {
293
298
  await formatWithPrettier(formattedStyledFilePath);
294
299
  });
295
300
  };
301
+ const formatPackageForDisplay = (spec) => {
302
+ const trimmed = spec.trim();
303
+ if (trimmed.startsWith("-D ")) return `${trimmed.slice(3).trim()} (dev)`;
304
+ return trimmed;
305
+ };
306
+ const renderPackageList = (packages) => {
307
+ return packages.map((p) => ` + ${formatPackageForDisplay(p)}`).join("\n");
308
+ };
309
+ const confirmInstall = async (packages, yes, options = {}) => {
310
+ if (yes) return;
311
+ if (packages.length === 0) return;
312
+ const input = options.input ?? process.stdin;
313
+ const output = options.output ?? process.stdout;
314
+ const isTTY = options.isTTY ?? process.stdin.isTTY ?? false;
315
+ if (!isTTY) {
316
+ throw new Error("Cannot prompt for install confirmation: stdin is not a TTY. Re-run with --yes to install the listed packages without prompting.");
317
+ }
318
+ output.write(`The following packages will be installed:
319
+ ${renderPackageList(packages)}
320
+ `);
321
+ const rl = promises$1.createInterface({ input, output, terminal: false });
322
+ try {
323
+ const answer = (await rl.question("Proceed? (y/N) ")).trim().toLowerCase();
324
+ if (answer !== "y" && answer !== "yes") {
325
+ throw new Error("Install cancelled by user.");
326
+ }
327
+ } finally {
328
+ rl.close();
329
+ }
330
+ };
331
+ const confirmYesNo = async (question, options = {}) => {
332
+ if (options.yes) return true;
333
+ const input = options.input ?? process.stdin;
334
+ const output = options.output ?? process.stdout;
335
+ const isTTY = options.isTTY ?? process.stdin.isTTY ?? false;
336
+ if (!isTTY) return false;
337
+ const suffix = options.defaultYes ? "(Y/n)" : "(y/N)";
338
+ const rl = promises$1.createInterface({ input, output, terminal: false });
339
+ try {
340
+ const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
341
+ if (answer === "") return !!options.defaultYes;
342
+ return answer === "y" || answer === "yes";
343
+ } finally {
344
+ rl.close();
345
+ }
346
+ };
296
347
  const CSS_FILE_FOLDERS = ["src", "public", "assets", "styles", "css", "app"];
297
348
  const CSS_SECOND_LEVEL_FOLDERS = ["styles", "css", "app", "pages"];
298
349
  const CSS_FILE_NAMES = ["index", "styles", "main", "app", "global", "globals"];
@@ -339,17 +390,22 @@ const editAstroConfig = (existing) => {
339
390
  const astroIntegration = {
340
391
  name: "astro",
341
392
  detect: (ctx) => findAstroConfig(ctx.projectDir),
342
- apply: async (ctx, configPath) => {
393
+ plan: async (ctx, configPath) => {
343
394
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
344
- if (existing === void 0) return { changed: false };
395
+ if (existing === void 0) return null;
345
396
  const result = editAstroConfig(existing);
346
397
  if (result.warning) compiler_saltyCompiler.logger.warn(result.warning);
347
- if (result.content === null) return { changed: false };
348
- if (!ctx.skipInstall) await npmInstall(`-D ${astroPackage(ctx.cliVersion)}`);
349
- compiler_saltyCompiler.logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
350
- await promises.writeFile(configPath, result.content);
351
- await formatWithPrettier(configPath);
352
- return { changed: true };
398
+ if (result.content === null) return null;
399
+ const newContent = result.content;
400
+ return {
401
+ packages: [`-D ${astroPackage(ctx.cliVersion)}`],
402
+ execute: async () => {
403
+ compiler_saltyCompiler.logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
404
+ await promises.writeFile(configPath, newContent);
405
+ await formatWithPrettier(configPath);
406
+ return { changed: true };
407
+ }
408
+ };
353
409
  }
354
410
  };
355
411
  const ESLINT_CONFIG_CANDIDATES = [
@@ -408,20 +464,25 @@ const eslintIntegration = {
408
464
  const candidates = eslintConfigCandidates(ctx.projectDir, ctx.cwd);
409
465
  return candidates.find((p) => fs.existsSync(p)) ?? null;
410
466
  },
411
- apply: async (ctx, configPath) => {
467
+ plan: async (ctx, configPath) => {
412
468
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
413
469
  if (existing === void 0) {
414
470
  compiler_saltyCompiler.logger.error("Could not read ESLint config file.");
415
- return { changed: false };
471
+ return null;
416
472
  }
417
- if (!ctx.skipInstall) await npmInstall(corePackages.eslintConfigCore(ctx.cliVersion));
418
473
  const result = editEslintConfig(existing, configPath.endsWith("js"));
419
474
  if (result.warning) compiler_saltyCompiler.logger.warn(result.warning);
420
- if (result.content === null) return { changed: false };
421
- compiler_saltyCompiler.logger.info("Edit file: " + configPath);
422
- await promises.writeFile(configPath, result.content);
423
- await formatWithPrettier(configPath);
424
- return { changed: true };
475
+ if (result.content === null) return null;
476
+ const newContent = result.content;
477
+ return {
478
+ packages: [corePackages.eslintConfigCore(ctx.cliVersion)],
479
+ execute: async () => {
480
+ compiler_saltyCompiler.logger.info("Edit file: " + configPath);
481
+ await promises.writeFile(configPath, newContent);
482
+ await formatWithPrettier(configPath);
483
+ return { changed: true };
484
+ }
485
+ };
425
486
  }
426
487
  };
427
488
  const nextConfigFiles = ["next.config.js", "next.config.cjs", "next.config.ts", "next.config.mjs"];
@@ -451,16 +512,20 @@ const nextIntegration = {
451
512
  const found = nextConfigFiles.map((file) => path.join(ctx.projectDir, file)).find((p) => fs.existsSync(p));
452
513
  return found ?? null;
453
514
  },
454
- apply: async (ctx, configPath) => {
515
+ plan: async (ctx, configPath) => {
455
516
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
456
- if (existing === void 0) return { changed: false };
517
+ if (existing === void 0) return null;
457
518
  const { content } = editNextConfig(existing);
458
- if (content === null) return { changed: false };
459
- if (!ctx.skipInstall) await npmInstall(`-D ${nextPackage(ctx.cliVersion)}`);
460
- compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Next.js config...");
461
- await promises.writeFile(configPath, content);
462
- await formatWithPrettier(configPath);
463
- return { changed: true };
519
+ if (content === null) return null;
520
+ return {
521
+ packages: [`-D ${nextPackage(ctx.cliVersion)}`],
522
+ execute: async () => {
523
+ compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Next.js config...");
524
+ await promises.writeFile(configPath, content);
525
+ await formatWithPrettier(configPath);
526
+ return { changed: true };
527
+ }
528
+ };
464
529
  }
465
530
  };
466
531
  const vitePackage = (version) => `@salty-css/vite@${version}`;
@@ -478,27 +543,40 @@ const viteIntegration = {
478
543
  const path$1 = path.join(ctx.projectDir, "vite.config.ts");
479
544
  return fs.existsSync(path$1) ? path$1 : null;
480
545
  },
481
- apply: async (ctx, configPath) => {
546
+ plan: async (ctx, configPath) => {
482
547
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
483
- if (existing === void 0) return { changed: false };
548
+ if (existing === void 0) return null;
484
549
  const { content } = editViteConfig(existing);
485
- if (content === null) return { changed: false };
486
- compiler_saltyCompiler.logger.info("Edit file: " + configPath);
487
- if (!ctx.skipInstall) await npmInstall(`-D ${vitePackage(ctx.cliVersion)}`);
488
- compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Vite config...");
489
- await promises.writeFile(configPath, content);
490
- await formatWithPrettier(configPath);
491
- return { changed: true };
550
+ if (content === null) return null;
551
+ return {
552
+ packages: [`-D ${vitePackage(ctx.cliVersion)}`],
553
+ execute: async () => {
554
+ compiler_saltyCompiler.logger.info("Edit file: " + configPath);
555
+ compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Vite config...");
556
+ await promises.writeFile(configPath, content);
557
+ await formatWithPrettier(configPath);
558
+ return { changed: true };
559
+ }
560
+ };
492
561
  }
493
562
  };
494
563
  const buildIntegrationRegistry = [eslintIntegration, viteIntegration, nextIntegration, astroIntegration];
495
- const detectAndApplyIntegrations = async (ctx) => {
496
- const results = [];
564
+ const planIntegrations = async (ctx) => {
565
+ const planned = [];
497
566
  for (const integration of buildIntegrationRegistry) {
498
567
  const configPath = await integration.detect(ctx);
499
568
  if (!configPath) continue;
500
- const result = await integration.apply(ctx, configPath);
501
- results.push({ name: integration.name, configPath, changed: result.changed });
569
+ const plan = await integration.plan(ctx, configPath);
570
+ if (!plan) continue;
571
+ planned.push({ name: integration.name, configPath, plan });
572
+ }
573
+ return planned;
574
+ };
575
+ const applyIntegrationPlans = async (planned) => {
576
+ const results = [];
577
+ for (const { name, configPath, plan } of planned) {
578
+ const result = await plan.execute();
579
+ results.push({ name, configPath, changed: result.changed });
502
580
  }
503
581
  return results;
504
582
  };
@@ -549,17 +627,24 @@ const wirePrepareScript = async () => {
549
627
  await updatePackageJson(next);
550
628
  };
551
629
  const registerInitCommand = (program) => {
552
- program.command("init [directory]").description("Initialize a new Salty-CSS project.").option("-d, --dir <dir>", "Project directory to initialize the project in.").option("--css-file <css-file>", "Existing CSS file where to import the generated CSS. Path must be relative to the given project directory.").option("--skip-install", "Skip installing dependencies.").action(async function(_dir = ".") {
630
+ program.command("init [directory]").description("Initialize a new Salty-CSS project.").option("-d, --dir <dir>", "Project directory to initialize the project in.").option("--css-file <css-file>", "Existing CSS file where to import the generated CSS. Path must be relative to the given project directory.").option("--skip-install", "Skip installing dependencies.").option("-y, --yes", "Skip the install confirmation prompt.").action(async function(_dir = ".") {
553
631
  try {
554
632
  const opts = this.opts();
555
633
  const dir = opts.dir ?? _dir;
556
634
  if (!dir) return compiler_saltyCompiler.logError("Project directory must be provided. Add it as the first argument after init command or use the --dir option.");
557
- const ctx = await buildContext({ dir, skipInstall: opts.skipInstall });
635
+ const ctx = await buildContext({ dir, skipInstall: opts.skipInstall, yes: opts.yes });
558
636
  compiler_saltyCompiler.logger.info("Initializing a new Salty-CSS project!");
559
637
  const framework = await detectFramework(ctx);
560
638
  compiler_saltyCompiler.logger.info(`Detected framework: ${framework.name}`);
639
+ const plannedIntegrations = await planIntegrations(ctx);
561
640
  if (!ctx.skipInstall) {
562
- await npmInstall(corePackages.core(ctx.cliVersion), framework.runtimePackage(ctx.cliVersion));
641
+ const packages = [
642
+ corePackages.core(ctx.cliVersion),
643
+ framework.runtimePackage(ctx.cliVersion),
644
+ ...plannedIntegrations.flatMap((p) => p.plan.packages)
645
+ ];
646
+ await confirmInstall(packages, ctx.yes);
647
+ await npmInstall(...packages);
563
648
  }
564
649
  const projectFiles = await Promise.all([readTemplate("salty.config.ts"), readTemplate("saltygen/index.css")]);
565
650
  await promises.mkdir(ctx.projectDir, { recursive: true });
@@ -567,7 +652,7 @@ const registerInitCommand = (program) => {
567
652
  await writeProjectToRc(ctx.cwd, ctx.relativeProjectPath, framework);
568
653
  await ensureGitignoreSaltygen(ctx.cwd);
569
654
  await importSaltygenIntoCss(ctx.projectDir, opts.cssFile);
570
- await detectAndApplyIntegrations(ctx);
655
+ await applyIntegrationPlans(plannedIntegrations);
571
656
  await wirePrepareScript();
572
657
  compiler_saltyCompiler.logger.info("Running the build to generate initial CSS...");
573
658
  const compiler = new compiler_saltyCompiler.SaltyCompiler(ctx.projectDir);
@@ -598,8 +683,8 @@ const getSaltyCssPackages = async () => {
598
683
  return saltyCssPackages;
599
684
  };
600
685
  const registerUpdateCommand = (program) => {
601
- program.command("update [version]").alias("up").description("Update Salty-CSS packages to the latest or specified version.").option("-v, --version <version>", "Version to update to.").option("--legacy-peer-deps <legacyPeerDeps>", "Use legacy peer dependencies (not recommended).", false).action(async function(_version = "latest") {
602
- const { legacyPeerDeps, version = _version } = this.opts();
686
+ program.command("update [version]").alias("up").description("Update Salty-CSS packages to the latest or specified version.").option("-v, --version <version>", "Version to update to.").option("--legacy-peer-deps <legacyPeerDeps>", "Use legacy peer dependencies (not recommended).", false).option("-y, --yes", "Skip confirmation prompts (install and rebuild).").option("-d, --dir <dir>", "Project directory to rebuild after updating.").action(async function(_version = "latest") {
687
+ const { legacyPeerDeps, version = _version, yes = false, dir } = this.opts();
603
688
  const saltyCssPackages = await getSaltyCssPackages();
604
689
  if (!saltyCssPackages) return compiler_saltyCompiler.logError("Could not update Salty-CSS packages as any were found in package.json.");
605
690
  const cli = await readThisPackageJson();
@@ -607,6 +692,11 @@ const registerUpdateCommand = (program) => {
607
692
  if (version === "@") return `${name}@${cli.version}`;
608
693
  return `${name}@${version.replace(/^@/, "")}`;
609
694
  });
695
+ try {
696
+ await confirmInstall(packagesToUpdate, yes);
697
+ } catch (err) {
698
+ return compiler_saltyCompiler.logError(err instanceof Error ? err.message : String(err));
699
+ }
610
700
  if (legacyPeerDeps) {
611
701
  compiler_saltyCompiler.logger.warn("Using legacy peer dependencies to update packages.");
612
702
  await npmInstall(...packagesToUpdate, "--legacy-peer-deps");
@@ -629,6 +719,17 @@ const registerUpdateCommand = (program) => {
629
719
  compiler_saltyCompiler.logger.info(`Updated to ${v.replace(/^\^/, "")}: ${names.join(", ")}`);
630
720
  }
631
721
  }
722
+ const project = dir ?? await getDefaultProject();
723
+ if (!project) {
724
+ compiler_saltyCompiler.logger.warn("Skipping rebuild: no project directory configured. Run `salty-css build [dir]` manually.");
725
+ return;
726
+ }
727
+ const shouldRebuild = await confirmYesNo("Rebuild Salty CSS now?", { yes });
728
+ if (!shouldRebuild) return;
729
+ const projectDir = resolveProjectDir(project);
730
+ compiler_saltyCompiler.logger.info("Rebuilding Salty-CSS project...");
731
+ await new compiler_saltyCompiler.SaltyCompiler(projectDir).generateCss();
732
+ compiler_saltyCompiler.logger.info("Rebuild complete.");
632
733
  });
633
734
  };
634
735
  const registerVersionOption = (program) => {