@r0hitsharma/uikit-cli 0.12.0-rohit-fork-ci.1

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/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # @r0hitsharma/uikit-cli
2
+
3
+ CLI tool for local package linking during active development with consumer repositories.
4
+
5
+ ## Setup (One-time)
6
+
7
+ ### 1. Configure npm prefix for writable global packages
8
+
9
+ If using nix-managed Node.js, configure npm to use a writable location:
10
+
11
+ ```bash
12
+ npm config set prefix ~/.npm-global
13
+ ```
14
+
15
+ Add to your shell profile (e.g., `~/.zshrc`):
16
+ ```bash
17
+ export PATH="$HOME/.npm-global/bin:$PATH"
18
+ ```
19
+
20
+ ### 2. Link CLI in uikit monorepo
21
+
22
+ From the uikit repository root:
23
+
24
+ ```bash
25
+ npm link --workspace packages/uikit-cli
26
+ ```
27
+
28
+ This makes the CLI available globally via workspace linking.
29
+
30
+ ### 3. Link CLI into consumer repository
31
+
32
+ From your consumer repository root:
33
+
34
+ ```bash
35
+ npm link @r0hitsharma/uikit-cli --workspace <workspace-name>
36
+ ```
37
+
38
+ Example for a consuming project:
39
+ ```bash
40
+ cd /path/to/your-project/ts
41
+ npm link @r0hitsharma/uikit-cli --workspace ui
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### Run lint and format tools without downstream installs
47
+
48
+ From any consumer workspace:
49
+
50
+ ```bash
51
+ ./node_modules/.bin/uikit-cli lint -c ./.oxlintrc.ts src panda.config.ts vite.config.ts
52
+ ./node_modules/.bin/uikit-cli format -c ./.oxfmtrc.ts --write "src/**/*.ts" "src/**/*.tsx" panda.config.ts vite.config.ts
53
+ ```
54
+
55
+ The CLI runs uikit-cli-managed `oxlint` and `oxfmt` versions internally (resolved via its
56
+ lockfile), so downstream workspaces do not need to declare those tool packages directly.
57
+
58
+ `format` fills in `-c ./.oxfmtrc.ts` when you omit a config flag and that file is in the
59
+ current directory, because `oxfmt` only auto-discovers the `.json` form. `lint` deliberately
60
+ does not do the same: `oxlint` resolves `oxlint.config.ts` per target file by walking up from
61
+ it, so a run spanning several packages picks up each package's own config. Injecting `-c`
62
+ would replace that with whichever config sits in the current directory, which in the git-hook
63
+ shape — cwd at the repo root, targets in several packages — silently drops those packages'
64
+ rules. Pass `-c` yourself when you do want one config to win.
65
+
66
+ `lint` applies `--max-warnings=0` by default, because `oxlint` exits 0 on warnings and the
67
+ shared presets set the `correctness` and `suspicious` categories to `warn`. Pass your own
68
+ `--max-warnings` or `--deny-warnings` to change that.
69
+
70
+ If your consumer workspace prefers to run tooling directly, it can install and invoke
71
+ `oxlint`/`oxfmt` itself. In that setup, `@r0hitsharma/oxlint-config` and
72
+ `@r0hitsharma/oxfmt-config` remain reusable config packages, while `uikit-cli` remains an
73
+ optional workflow wrapper.
74
+
75
+ ### Check a generated stylesheet for silently-dropped CSS
76
+
77
+ ```bash
78
+ ./node_modules/.bin/uikit-cli doctor # finds styled-system/styles.css
79
+ ./node_modules/.bin/uikit-cli doctor path/to/styles.css # explicit path
80
+ ./node_modules/.bin/uikit-cli doctor --codegen # runs `panda cssgen` itself
81
+ ```
82
+
83
+ Use `--codegen` when you use the Panda PostCSS plugin and never write a frozen
84
+ `styled-system/styles.css`.
85
+
86
+ Findings carry a severity. `doctor` exits non-zero on any **error**, so it gates CI;
87
+ **warnings** are printed and do not affect the exit code. Only roleless `colorPalette` can
88
+ warn — see [error or warning](#roleless-colorpalette-error-or-warning) below.
89
+
90
+ The design system's authoring model has failure modes that produce *no error anywhere* — the
91
+ build passes, the console is clean, and the style simply does not apply. `doctor` scans the
92
+ generated stylesheet for them:
93
+
94
+ | Check | Severity | What silently breaks |
95
+ | --- | --- | --- |
96
+ | `missing-static-css` | error | No design-system recipe classes were emitted at all, so runtime-selected variants (status tones, dense tables, drawer sizes) render unstyled. Fix by spreading `designSystemStaticCssRecipes` into your Panda config's `staticCss.recipes`. |
97
+ | `unresolved-token` | error | A declaration whose value is a bare `token.path` (e.g. `color: text.subtle`) rather than a `var(--…)`. Invalid CSS the browser drops. |
98
+ | `roleless-color-palette` | error or warning | A `colorPalette` value that defines no role for the property styled with it — `var(--colors-color-palette-solid-bg)` is well-formed but undefined in that scope, so the browser drops the declaration. Only `neutral`, `gray`, `green`, `red`, `amber` and `blue` carry full role sub-tokens; other hues map the 50–950 scale only. |
99
+
100
+ #### Roleless `colorPalette`: error or warning
101
+
102
+ A role reference is only a *definite* miss when every `colorPalette` its scope can be set to
103
+ lacks the role — then no combination of that recipe's variants avoids the dropped declaration.
104
+ When some assignable palette does define the role, the breaking pairing is possible but
105
+ unproven: a recipe exposing `colorPalette: violet` alongside a `solid` emphasis variant may
106
+ never combine the two in an app, and the stylesheet cannot say. That is reported as a warning —
107
+ worth printing, not worth failing a build over.
108
+
109
+ | Palettes assignable in the scope | Which lack the role | Severity | Exit code |
110
+ | --- | --- | --- | --- |
111
+ | one | it does | error | non-zero |
112
+ | several | all of them | error | non-zero |
113
+ | several | some of them | warning | 0 |
114
+ | several | none | no finding | 0 |
115
+ | none (no `colorPalette` assigned in scope) | — | no finding | 0 |
116
+
117
+ #### Roleless `colorPalette`: detection boundary
118
+
119
+ Resolving which palette applies to a declaration is the CSS cascade, and `doctor` does not
120
+ simulate it. It resolves exactly one scope — a **recipe**, keyed by the class-name stem Panda
121
+ derives from `className` — and flags a role reference only when a palette assigned somewhere in
122
+ that same recipe's classes fails to define the role. Slots share the scope, since the root slot's
123
+ palette properties cascade into the others.
124
+
125
+ These cases are *not* flagged, on purpose, because scope is ambiguous and a false pass is far
126
+ cheaper than a false failure:
127
+
128
+ - atomic utilities (`.color-palette_violet` plus a separate `.bg_colorPalette\.solid\.bg`) — the
129
+ stylesheet cannot prove the two classes land on the same element, nor that an ancestor already
130
+ supplied the role;
131
+ - a palette set on an outer recipe with the role consumed by an inner one;
132
+ - `var(--colors-color-palette-…, fallback)` — a fallback means the declaration survives.
133
+
134
+ The palette→roles map is read out of the stylesheet itself (each `colorPalette` assignment
135
+ ruleset *is* the preset's role tokens in generated form), so palettes added to the preset — or
136
+ defined in a consumer's own preset extension — are covered with no list to keep in sync.
137
+
138
+ ### Register the local uikit packages
139
+
140
+ ```bash
141
+ ./node_modules/.bin/uikit-cli register
142
+ ```
143
+
144
+ Runs `npm link` in every local `@r0hitsharma/*` package so they are available to link
145
+ from. `link` and `unlink` do this themselves, so run it directly only to register the
146
+ packages without touching a consumer. Pass `--uikit-root <path>` when the uikit checkout
147
+ cannot be auto-discovered.
148
+
149
+ ### Link uikit packages into a consumer repository
150
+
151
+ From your consumer repository:
152
+
153
+ ```bash
154
+ ./node_modules/.bin/uikit-cli link
155
+ ```
156
+
157
+ This command links all `@r0hitsharma/*` packages from your local uikit monorepo into your consumer project, allowing you to develop packages and see changes immediately.
158
+
159
+ Verify links are working:
160
+ ```bash
161
+ ./node_modules/.bin/uikit-cli link --verify
162
+ ```
163
+
164
+ Add this script to your consumer's `package.json`:
165
+
166
+ ```json
167
+ {
168
+ "scripts": {
169
+ "uikit:link": "./node_modules/.bin/uikit-cli link",
170
+ "uikit:unlink": "./node_modules/.bin/uikit-cli unlink"
171
+ }
172
+ }
173
+ ```
174
+
175
+ ### Restore registry versions
176
+
177
+ When co-development is complete, restore published versions from npm:
178
+
179
+ ```bash
180
+ ./node_modules/.bin/uikit-cli unlink
181
+ ```
182
+
183
+ ## How it works
184
+
185
+ The CLI manages local development links by:
186
+
187
+ 1. Auto-registering local `@r0hitsharma/*` packages from your uikit checkout via `npm link`
188
+ 2. Linking only the consumer workspaces that actually depend on those packages
189
+ 3. Cleaning up shadow installs and Vite caches to ensure symlinks work correctly
190
+ 4. Using `--preserve-symlinks` flag and bundling to avoid ES module resolution issues
191
+
192
+ The CLI automatically detects the consumer root and all dependent packages, working from any
193
+ directory within the project. Both consumer shapes are supported: an **npm-workspaces monorepo**
194
+ (the root with a `workspaces` field), and a **single package** that installs uikit directly (no
195
+ `workspaces` field) — in the single-package case the root package is linked directly.
196
+
197
+ The CLI auto-discovers the local uikit monorepo for typical sibling-checkout layouts.
198
+
199
+ ## Requirements
200
+
201
+ - Local clone of the uikit monorepo
202
+ - Node.js 24+ and npm installed
203
+ - Writable npm prefix configured (see Setup)
204
+
205
+ ## Troubleshooting
206
+
207
+ ### "EACCES: permission denied" when using npm link
208
+
209
+ You need to configure npm to use a writable prefix location. See Setup step 1 above.
210
+
211
+ ### "ENOENT: no such file or directory" errors
212
+
213
+ Use the workspace-based linking approach (Setup steps 2-3) instead of global npm link. The CLI bundle includes `--preserve-symlinks` to handle ES module resolution with symlinks.
214
+
215
+ ### Links not working after linking
216
+
217
+ Run with `--verify` flag to check link status:
218
+ ```bash
219
+ ./node_modules/.bin/uikit-cli link --verify
220
+ ```
221
+
222
+ ### `useTheme must be used within ThemeProvider` (duplicated React context) after linking
223
+
224
+ With `resolve.preserveSymlinks: true` (which linking relies on) Vite can serve a linked package
225
+ under **two** URLs at once — `/node_modules/…` (the root symlink) and `/@fs/…` (the real path) —
226
+ producing two module graphs and two copies of singletons like `ThemeContext`. A component and its
227
+ `ThemeProvider` then hold different context objects and the hook throws. This is a link artifact,
228
+ not a bug in the app or the design system.
229
+
230
+ To confirm: look for the same module served at both URLs in the dev server (both HTTP 200). To
231
+ recover: run `uikit-cli unlink` and restart the dev server (touch the Vite config so it
232
+ re-optimizes) — the module collapses back to a single URL. To stay linked, add the linked
233
+ package(s) plus `react`/`react-dom` to your Vite `resolve.dedupe`, or drop `preserveSymlinks`.
234
+
235
+ ### A partial link reported as success
236
+
237
+ If `link` prints that some packages did **not** link (PARTIAL), the named packages may still
238
+ resolve to a stale registry version. A consumer `.npmrc` with `min-release-age` is the usual
239
+ cause — npm rejects a fresh prerelease with `ETARGET`. Re-run with an override (e.g.
240
+ `--min-release-age=0`) or link those packages manually.
241
+
242
+ ### The CLI cannot repair a stale copy of itself
243
+
244
+ `link` links `@r0hitsharma/*` into the consumer — and that includes `uikit-cli` itself. So
245
+ `npm run uikit:link` runs whatever `uikit-cli` is currently in the consumer's `node_modules`: if
246
+ that copy is **stale**, the old code runs, and no source fix can change the run that needs it (a
247
+ stale binary can't even report its own staleness). When developing the CLI, or right after pulling
248
+ CLI changes, bypass the linked copy and invoke the monorepo's freshly-built binary directly so you
249
+ always run current code:
250
+
251
+ ```bash
252
+ npm run build --workspace packages/uikit-cli # in the uikit checkout
253
+ node <path-to-uikit>/packages/uikit-cli/dist/cli.js link --uikit-root <path-to-uikit>
254
+ ```
255
+
256
+ Or install the published version from the registry, which sidesteps linking entirely.
257
+
258
+ ## Development workflow
259
+
260
+ In a consumer workspace:
261
+
262
+ ```bash
263
+ # One-time setup (see Setup section above)
264
+ npm link @r0hitsharma/uikit-cli --workspace <workspace-name>
265
+
266
+ # Link uikit packages for local development
267
+ npm run uikit:link
268
+
269
+ # Verify links
270
+ npm run uikit:link -- --verify
271
+
272
+ # Later, restore registry versions
273
+ npm run uikit:unlink
274
+ ```
275
+
276
+ ## Debug mode
277
+
278
+ Run with debug output:
279
+ ```bash
280
+ UIKIT_DEBUG=1 ./node_modules/.bin/uikit-cli link --verify
281
+ ```
282
+
283
+ Or use the `--debug` flag:
284
+ ```bash
285
+ ./node_modules/.bin/uikit-cli link --debug --verify
286
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { NpmCommandExecutor } from './command-executor.js';
5
+ import { DoctorCommand } from './commands/doctor.js';
6
+ import { FormatCommand } from './commands/format.js';
7
+ import { LinkCommand } from './commands/link.js';
8
+ import { LintCommand } from './commands/lint.js';
9
+ import { RegisterCommand } from './commands/register.js';
10
+ import { UnlinkCommand } from './commands/unlink.js';
11
+ import { RealFileSystem } from './fs-utils.js';
12
+ import { LinkValidator } from './link-validator.js';
13
+ import { ConsoleLogger } from './logger.js';
14
+ import { PackageDiscovery } from './package-discovery.js';
15
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
16
+ /**
17
+ * Parse command line arguments
18
+ */
19
+ function parseArgs(argv) {
20
+ const args = argv.slice(2);
21
+ if (args.length === 0) {
22
+ throw new Error('Usage: uikit-cli <register|link|unlink|lint|format|doctor> [--verify] [--debug] [--uikit-root <path>] [args...]');
23
+ }
24
+ const mode = args[0];
25
+ const validModes = [
26
+ 'lint',
27
+ 'format',
28
+ 'register',
29
+ 'link',
30
+ 'unlink',
31
+ 'doctor',
32
+ ];
33
+ if (!validModes.includes(mode)) {
34
+ throw new Error(`Unknown command: ${mode}`);
35
+ }
36
+ let uikitRoot;
37
+ let verify = false;
38
+ let debug = false;
39
+ const commandArgs = [];
40
+ let i = 1;
41
+ while (i < args.length) {
42
+ const arg = args[i];
43
+ // Unreachable given the loop bound; narrows `arg` to `string`.
44
+ if (arg === undefined)
45
+ break;
46
+ if (arg === '--uikit-root' && i + 1 < args.length) {
47
+ uikitRoot = args[i + 1];
48
+ i += 2;
49
+ }
50
+ else if (arg === '--verify') {
51
+ verify = true;
52
+ i += 1;
53
+ }
54
+ else if (arg === '--debug') {
55
+ debug = true;
56
+ i += 1;
57
+ }
58
+ else {
59
+ commandArgs.push(arg);
60
+ i += 1;
61
+ }
62
+ }
63
+ // Check for UIKIT_DEBUG environment variable
64
+ if (process.env.UIKIT_DEBUG === '1') {
65
+ debug = true;
66
+ }
67
+ // Resolve uikit root
68
+ const fs = new RealFileSystem();
69
+ const discovery = new PackageDiscovery(fs);
70
+ if (debug) {
71
+ console.log('[DEBUG parseArgs] Resolving uikit root...');
72
+ }
73
+ if (!uikitRoot) {
74
+ // Try relative to script location
75
+ const relativeRoot = path.resolve(scriptDir, '../../../..');
76
+ if (debug) {
77
+ console.log('[DEBUG parseArgs] Checking relative root:', relativeRoot);
78
+ }
79
+ if (discovery.isValidUIKitRoot(relativeRoot)) {
80
+ uikitRoot = relativeRoot;
81
+ if (debug) {
82
+ console.log('[DEBUG parseArgs] Found valid uikit root at relative location');
83
+ }
84
+ }
85
+ }
86
+ if (!uikitRoot && mode !== 'lint' && mode !== 'format' && mode !== 'doctor') {
87
+ // Try to find from consumer
88
+ try {
89
+ const tempConsumerRoot = discovery.findConsumerRoot(process.cwd());
90
+ const foundRoot = discovery.findUIKitRootFromConsumer(tempConsumerRoot);
91
+ if (foundRoot) {
92
+ uikitRoot = foundRoot;
93
+ }
94
+ }
95
+ catch {
96
+ // Will throw error below if needed
97
+ }
98
+ }
99
+ if (!uikitRoot && mode !== 'lint' && mode !== 'format' && mode !== 'doctor') {
100
+ // Try walking up from cwd
101
+ const foundRoot = discovery.findUIKitRoot(process.cwd());
102
+ if (foundRoot) {
103
+ uikitRoot = foundRoot;
104
+ }
105
+ }
106
+ if (!uikitRoot && mode !== 'lint' && mode !== 'format' && mode !== 'doctor') {
107
+ throw new Error('Could not find uikit root directory.\n' +
108
+ 'Tried:\n' +
109
+ ' - Relative to script location\n' +
110
+ ' - Sibling to consumer root\n' +
111
+ ' - Walking up from cwd\n' +
112
+ 'Use --uikit-root to specify manually.');
113
+ }
114
+ let consumerRoot = null;
115
+ if (mode === 'link' || mode === 'unlink') {
116
+ consumerRoot = discovery.findConsumerRoot(process.cwd());
117
+ }
118
+ return {
119
+ mode,
120
+ consumerRoot,
121
+ uikitRoot: uikitRoot ?? '',
122
+ commandArgs,
123
+ verify,
124
+ debug,
125
+ };
126
+ }
127
+ /**
128
+ * Ensure CLI binary is built before registering
129
+ */
130
+ function ensureCliBinaryBuilt(uikitRoot, executor) {
131
+ const cliPackagePath = path.join(uikitRoot, 'packages/uikit-cli');
132
+ const distPath = path.join(cliPackagePath, 'dist/cli.js');
133
+ const fs = new RealFileSystem();
134
+ if (!fs.exists(distPath)) {
135
+ console.log('Building CLI binary...');
136
+ executor.exec('npm run build', { cwd: cliPackagePath });
137
+ }
138
+ }
139
+ /**
140
+ * Link CLI into consumer for convenience
141
+ */
142
+ function linkCliIntoConsumer(consumerRoot, executor, logger) {
143
+ const result = executor.exec('npm link "@r0hitsharma/uikit-cli" --package-lock=false --save=false --no-workspaces', { cwd: consumerRoot });
144
+ if (!result.success) {
145
+ logger.warn('Could not link the local @r0hitsharma/uikit-cli — you may be running ' +
146
+ 'the registry version, which can lag behind the local one (e.g. missing ' +
147
+ '`doctor --codegen`). A consumer .npmrc with `min-release-age` can reject ' +
148
+ 'a fresh prerelease with ETARGET here.');
149
+ }
150
+ }
151
+ /**
152
+ * Main entry point
153
+ */
154
+ try {
155
+ const parsed = parseArgs(process.argv);
156
+ const { mode, consumerRoot, uikitRoot, commandArgs, verify, debug } = parsed;
157
+ if (debug) {
158
+ console.log('[DEBUG] Parsed args:', {
159
+ mode,
160
+ consumerRoot,
161
+ uikitRoot,
162
+ verify,
163
+ });
164
+ }
165
+ // Initialize dependencies
166
+ const fs = new RealFileSystem();
167
+ const executor = new NpmCommandExecutor(debug);
168
+ const logger = new ConsoleLogger(debug);
169
+ const discovery = new PackageDiscovery(fs);
170
+ const validator = new LinkValidator(fs, logger);
171
+ // Lint and format forward the tool's verdict as the exit code — exiting 0
172
+ // unconditionally left `--check` runs unable to gate CI.
173
+ if (mode === 'lint') {
174
+ const lintCmd = new LintCommand(executor);
175
+ process.exit(lintCmd.execute(commandArgs) ? 0 : 1);
176
+ }
177
+ if (mode === 'format') {
178
+ const formatCmd = new FormatCommand(executor, fs);
179
+ process.exit(formatCmd.execute(commandArgs) ? 0 : 1);
180
+ }
181
+ // Doctor scans the consumer's generated CSS; it runs in the consumer cwd and
182
+ // needs no uikit root. Non-zero exit on findings so it gates CI.
183
+ if (mode === 'doctor') {
184
+ const doctorCmd = new DoctorCommand(fs, logger, executor);
185
+ process.exit(doctorCmd.execute(commandArgs) ? 0 : 1);
186
+ }
187
+ // Handle register command
188
+ if (mode === 'register') {
189
+ ensureCliBinaryBuilt(uikitRoot, executor);
190
+ const registerCmd = new RegisterCommand(discovery, executor, logger);
191
+ registerCmd.execute(uikitRoot);
192
+ if (verify) {
193
+ logger.info('\\nVerifying registration...');
194
+ // Could add verification logic here
195
+ logger.info('✓ Registration verified');
196
+ }
197
+ process.exit(0);
198
+ }
199
+ // Handle link/unlink commands (require consumerRoot)
200
+ if (!consumerRoot) {
201
+ throw new Error('Consumer root is required for link/unlink operations.');
202
+ }
203
+ // Register packages and link CLI before link/unlink
204
+ ensureCliBinaryBuilt(uikitRoot, executor);
205
+ const registerCmd = new RegisterCommand(discovery, executor, logger);
206
+ registerCmd.execute(uikitRoot);
207
+ linkCliIntoConsumer(consumerRoot, executor, logger);
208
+ if (mode === 'link') {
209
+ const linkCmd = new LinkCommand(discovery, executor, validator, fs, logger);
210
+ linkCmd.execute(consumerRoot, uikitRoot, verify);
211
+ process.exit(0);
212
+ }
213
+ if (mode === 'unlink') {
214
+ const unlinkCmd = new UnlinkCommand(executor, logger);
215
+ unlinkCmd.execute(consumerRoot, uikitRoot, discovery);
216
+ process.exit(0);
217
+ }
218
+ }
219
+ catch (error) {
220
+ if (process.env.UIKIT_DEBUG) {
221
+ console.error('Full error:', error);
222
+ }
223
+ console.error(error instanceof Error ? error.message : String(error));
224
+ process.exit(1);
225
+ }
package/dist/cli.sh ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env sh
2
+ # Use --preserve-symlinks to fix ES module resolution with npm link
3
+ # Follow symlink to find the actual script location
4
+ if [ -L "$0" ]; then
5
+ SCRIPT="$(readlink -f "$0" 2>/dev/null || readlink "$0")"
6
+ else
7
+ SCRIPT="$0"
8
+ fi
9
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)"
10
+ exec node --preserve-symlinks "$SCRIPT_DIR/cli.js" "$@"
@@ -0,0 +1,41 @@
1
+ import { execSync } from 'node:child_process';
2
+ /**
3
+ * Real npm command executor using execSync
4
+ */
5
+ export class NpmCommandExecutor {
6
+ debugMode;
7
+ constructor(debugMode = false) {
8
+ this.debugMode = debugMode;
9
+ }
10
+ exec(command, options) {
11
+ const { cwd = process.cwd(), silent = false } = options || {};
12
+ if (this.debugMode) {
13
+ console.log(`[DEBUG] Executing: ${command}`);
14
+ console.log(`[DEBUG] CWD: ${cwd}`);
15
+ }
16
+ try {
17
+ const stdout = execSync(command, {
18
+ cwd,
19
+ encoding: 'utf8',
20
+ stdio: silent ? 'pipe' : 'inherit',
21
+ });
22
+ return {
23
+ stdout: typeof stdout === 'string' ? stdout : '',
24
+ stderr: '',
25
+ success: true,
26
+ };
27
+ }
28
+ catch (error) {
29
+ const err = error;
30
+ return {
31
+ stdout: err.stdout?.toString('utf8') || '',
32
+ stderr: err.stderr?.toString('utf8') || err.message || 'Unknown error',
33
+ success: false,
34
+ };
35
+ }
36
+ }
37
+ execQuiet(command, options) {
38
+ const result = this.exec(command, { ...options, silent: true });
39
+ return result.success;
40
+ }
41
+ }