@agent-scope/cli 1.8.0 → 1.9.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.d.cts CHANGED
@@ -1,6 +1,94 @@
1
1
  import { Command } from 'commander';
2
2
  import { ComplexityClass } from '@agent-scope/manifest';
3
3
 
4
+ /**
5
+ * @agent-scope/cli — `scope init` command implementation
6
+ *
7
+ * Scaffolds a `reactscope.config.json` (and friends) in the current working
8
+ * directory. Supports two modes:
9
+ *
10
+ * - Interactive (default): prompts the user to confirm / override every
11
+ * auto-detected value via Node's built-in `readline`.
12
+ * - Non-interactive (`--yes`): accepts all auto-detected defaults without
13
+ * prompting and prints a summary of created files.
14
+ *
15
+ * Safety rules (locked decisions):
16
+ * - Config file format is JSON (`reactscope.config.json`).
17
+ * - Output directory is `.reactscope/` — always added to `.gitignore`.
18
+ * - Auto-detection is non-destructive; never overwrite existing config
19
+ * without `--force`.
20
+ */
21
+ interface ReactScopeConfig {
22
+ components: {
23
+ include: string[];
24
+ exclude: string[];
25
+ wrappers: {
26
+ providers: string[];
27
+ globalCSS: string[];
28
+ };
29
+ };
30
+ render: {
31
+ viewport: {
32
+ default: {
33
+ width: number;
34
+ height: number;
35
+ };
36
+ };
37
+ theme: "light" | "dark";
38
+ warmBrowser: boolean;
39
+ };
40
+ tokens: {
41
+ file: string;
42
+ compliance: {
43
+ threshold: number;
44
+ };
45
+ };
46
+ output: {
47
+ dir: string;
48
+ sprites: {
49
+ format: "png" | "webp";
50
+ cellPadding: number;
51
+ labelAxes: boolean;
52
+ };
53
+ json: {
54
+ pretty: boolean;
55
+ };
56
+ };
57
+ ci: {
58
+ complianceThreshold: number;
59
+ failOnA11yViolations: boolean;
60
+ failOnConsoleErrors: boolean;
61
+ baselinePath: string;
62
+ };
63
+ }
64
+ interface InitOptions {
65
+ /** Accept all defaults without prompting. */
66
+ yes: boolean;
67
+ /** Overwrite existing config without warning. */
68
+ force: boolean;
69
+ /** Root directory (defaults to `process.cwd()`). */
70
+ cwd?: string;
71
+ }
72
+ interface InitResult {
73
+ /** Whether the command succeeded. */
74
+ success: boolean;
75
+ /** Human-readable message summarising the outcome. */
76
+ message: string;
77
+ /** Paths of files that were created. */
78
+ created: string[];
79
+ /** Whether init was skipped (config already existed and --force was not set). */
80
+ skipped: boolean;
81
+ }
82
+ /**
83
+ * Execute `scope init` logic.
84
+ *
85
+ * Separated from the Commander action so it can be unit-tested without
86
+ * spawning a real subprocess.
87
+ */
88
+ declare function runInit(options: InitOptions): Promise<InitResult>;
89
+
90
+ declare function createInitCommand(): Command;
91
+
4
92
  /**
5
93
  * @agent-scope/cli — manifest sub-commands
6
94
  *
@@ -67,4 +155,4 @@ declare function createProgram(options?: ScopeCLIOptions): Command;
67
155
  */
68
156
  declare function createTokensCommand(): Command;
69
157
 
70
- export { type ListRow, type QueryRow, type ScopeCLIOptions, createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob };
158
+ export { type InitOptions, type InitResult, type ListRow, type QueryRow, type ReactScopeConfig, type ScopeCLIOptions, createInitCommand, createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob, runInit };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,94 @@
1
1
  import { Command } from 'commander';
2
2
  import { ComplexityClass } from '@agent-scope/manifest';
3
3
 
4
+ /**
5
+ * @agent-scope/cli — `scope init` command implementation
6
+ *
7
+ * Scaffolds a `reactscope.config.json` (and friends) in the current working
8
+ * directory. Supports two modes:
9
+ *
10
+ * - Interactive (default): prompts the user to confirm / override every
11
+ * auto-detected value via Node's built-in `readline`.
12
+ * - Non-interactive (`--yes`): accepts all auto-detected defaults without
13
+ * prompting and prints a summary of created files.
14
+ *
15
+ * Safety rules (locked decisions):
16
+ * - Config file format is JSON (`reactscope.config.json`).
17
+ * - Output directory is `.reactscope/` — always added to `.gitignore`.
18
+ * - Auto-detection is non-destructive; never overwrite existing config
19
+ * without `--force`.
20
+ */
21
+ interface ReactScopeConfig {
22
+ components: {
23
+ include: string[];
24
+ exclude: string[];
25
+ wrappers: {
26
+ providers: string[];
27
+ globalCSS: string[];
28
+ };
29
+ };
30
+ render: {
31
+ viewport: {
32
+ default: {
33
+ width: number;
34
+ height: number;
35
+ };
36
+ };
37
+ theme: "light" | "dark";
38
+ warmBrowser: boolean;
39
+ };
40
+ tokens: {
41
+ file: string;
42
+ compliance: {
43
+ threshold: number;
44
+ };
45
+ };
46
+ output: {
47
+ dir: string;
48
+ sprites: {
49
+ format: "png" | "webp";
50
+ cellPadding: number;
51
+ labelAxes: boolean;
52
+ };
53
+ json: {
54
+ pretty: boolean;
55
+ };
56
+ };
57
+ ci: {
58
+ complianceThreshold: number;
59
+ failOnA11yViolations: boolean;
60
+ failOnConsoleErrors: boolean;
61
+ baselinePath: string;
62
+ };
63
+ }
64
+ interface InitOptions {
65
+ /** Accept all defaults without prompting. */
66
+ yes: boolean;
67
+ /** Overwrite existing config without warning. */
68
+ force: boolean;
69
+ /** Root directory (defaults to `process.cwd()`). */
70
+ cwd?: string;
71
+ }
72
+ interface InitResult {
73
+ /** Whether the command succeeded. */
74
+ success: boolean;
75
+ /** Human-readable message summarising the outcome. */
76
+ message: string;
77
+ /** Paths of files that were created. */
78
+ created: string[];
79
+ /** Whether init was skipped (config already existed and --force was not set). */
80
+ skipped: boolean;
81
+ }
82
+ /**
83
+ * Execute `scope init` logic.
84
+ *
85
+ * Separated from the Commander action so it can be unit-tested without
86
+ * spawning a real subprocess.
87
+ */
88
+ declare function runInit(options: InitOptions): Promise<InitResult>;
89
+
90
+ declare function createInitCommand(): Command;
91
+
4
92
  /**
5
93
  * @agent-scope/cli — manifest sub-commands
6
94
  *
@@ -67,4 +155,4 @@ declare function createProgram(options?: ScopeCLIOptions): Command;
67
155
  */
68
156
  declare function createTokensCommand(): Command;
69
157
 
70
- export { type ListRow, type QueryRow, type ScopeCLIOptions, createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob };
158
+ export { type InitOptions, type InitResult, type ListRow, type QueryRow, type ReactScopeConfig, type ScopeCLIOptions, createInitCommand, createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob, runInit };
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
- import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
2
- import { resolve, dirname } from 'path';
3
- import { generateManifest } from '@agent-scope/manifest';
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, readdirSync } from 'fs';
2
+ import { join, resolve, dirname } from 'path';
3
+ import * as readline from 'readline';
4
4
  import { Command } from 'commander';
5
+ import { generateManifest } from '@agent-scope/manifest';
5
6
  import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
6
7
  import { chromium } from 'playwright';
7
8
  import { safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer, BrowserPool } from '@agent-scope/render';
@@ -9,7 +10,350 @@ import * as esbuild from 'esbuild';
9
10
  import { createRequire } from 'module';
10
11
  import { TokenResolver, validateTokenFile, TokenValidationError, parseTokenFileSync, TokenParseError } from '@agent-scope/tokens';
11
12
 
12
- // src/manifest-commands.ts
13
+ // src/init/index.ts
14
+ function hasConfigFile(dir, stem) {
15
+ if (!existsSync(dir)) return false;
16
+ try {
17
+ const entries = readdirSync(dir);
18
+ return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+ function readSafe(path) {
24
+ try {
25
+ return readFileSync(path, "utf-8");
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ function detectFramework(rootDir, packageDeps) {
31
+ if (hasConfigFile(rootDir, "next.config")) return "next";
32
+ if (hasConfigFile(rootDir, "vite.config")) return "vite";
33
+ if (hasConfigFile(rootDir, "remix.config")) return "remix";
34
+ if ("react-scripts" in packageDeps) return "cra";
35
+ return "unknown";
36
+ }
37
+ function detectPackageManager(rootDir) {
38
+ if (existsSync(join(rootDir, "bun.lock"))) return "bun";
39
+ if (existsSync(join(rootDir, "yarn.lock"))) return "yarn";
40
+ if (existsSync(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
41
+ if (existsSync(join(rootDir, "package-lock.json"))) return "npm";
42
+ return "npm";
43
+ }
44
+ function detectTypeScript(rootDir) {
45
+ const candidate = join(rootDir, "tsconfig.json");
46
+ if (existsSync(candidate)) {
47
+ return { typescript: true, tsconfigPath: candidate };
48
+ }
49
+ return { typescript: false, tsconfigPath: null };
50
+ }
51
+ var COMPONENT_DIRS = ["src/components", "src/app", "src/pages", "src/ui", "src/features", "src"];
52
+ var COMPONENT_EXTS = [".tsx", ".jsx"];
53
+ function detectComponentPatterns(rootDir, typescript) {
54
+ const patterns = [];
55
+ const ext = typescript ? "tsx" : "jsx";
56
+ const altExt = typescript ? "jsx" : "jsx";
57
+ for (const dir of COMPONENT_DIRS) {
58
+ const absDir = join(rootDir, dir);
59
+ if (!existsSync(absDir)) continue;
60
+ let hasComponents = false;
61
+ try {
62
+ const entries = readdirSync(absDir, { withFileTypes: true });
63
+ hasComponents = entries.some(
64
+ (e) => e.isFile() && COMPONENT_EXTS.some((x) => e.name.endsWith(x))
65
+ );
66
+ if (!hasComponents) {
67
+ hasComponents = entries.some(
68
+ (e) => e.isDirectory() && (() => {
69
+ try {
70
+ return readdirSync(join(absDir, e.name)).some(
71
+ (f) => COMPONENT_EXTS.some((x) => f.endsWith(x))
72
+ );
73
+ } catch {
74
+ return false;
75
+ }
76
+ })()
77
+ );
78
+ }
79
+ } catch {
80
+ continue;
81
+ }
82
+ if (hasComponents) {
83
+ patterns.push(`${dir}/**/*.${ext}`);
84
+ if (altExt !== ext) {
85
+ patterns.push(`${dir}/**/*.${altExt}`);
86
+ }
87
+ }
88
+ }
89
+ const unique = [...new Set(patterns)];
90
+ if (unique.length === 0) {
91
+ return [`**/*.${ext}`];
92
+ }
93
+ return unique;
94
+ }
95
+ var TAILWIND_STEMS = ["tailwind.config"];
96
+ var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
97
+ var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
98
+ var CSS_CUSTOM_PROPS_RE = /:root\s*\{[^}]*--[a-zA-Z]/;
99
+ function detectTokenSources(rootDir) {
100
+ const sources = [];
101
+ for (const stem of TAILWIND_STEMS) {
102
+ if (hasConfigFile(rootDir, stem)) {
103
+ try {
104
+ const entries = readdirSync(rootDir);
105
+ const match = entries.find((f) => f === stem || f.startsWith(`${stem}.`));
106
+ if (match) {
107
+ sources.push({ kind: "tailwind-config", path: join(rootDir, match) });
108
+ }
109
+ } catch {
110
+ }
111
+ }
112
+ }
113
+ const srcDir = join(rootDir, "src");
114
+ const dirsToScan = existsSync(srcDir) ? [srcDir] : [];
115
+ for (const scanDir of dirsToScan) {
116
+ try {
117
+ const entries = readdirSync(scanDir, { withFileTypes: true });
118
+ for (const entry of entries) {
119
+ if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
120
+ const filePath = join(scanDir, entry.name);
121
+ const content = readSafe(filePath);
122
+ if (content !== null && CSS_CUSTOM_PROPS_RE.test(content)) {
123
+ sources.push({ kind: "css-custom-properties", path: filePath });
124
+ }
125
+ }
126
+ }
127
+ } catch {
128
+ }
129
+ }
130
+ if (existsSync(srcDir)) {
131
+ try {
132
+ const entries = readdirSync(srcDir);
133
+ for (const entry of entries) {
134
+ if (THEME_SUFFIXES.some((s) => entry.endsWith(s))) {
135
+ sources.push({ kind: "theme-file", path: join(srcDir, entry) });
136
+ }
137
+ }
138
+ } catch {
139
+ }
140
+ }
141
+ return sources;
142
+ }
143
+ function detectProject(rootDir) {
144
+ const pkgPath = join(rootDir, "package.json");
145
+ let packageDeps = {};
146
+ const pkgContent = readSafe(pkgPath);
147
+ if (pkgContent !== null) {
148
+ try {
149
+ const pkg = JSON.parse(pkgContent);
150
+ packageDeps = {
151
+ ...pkg.dependencies,
152
+ ...pkg.devDependencies
153
+ };
154
+ } catch {
155
+ }
156
+ }
157
+ const framework = detectFramework(rootDir, packageDeps);
158
+ const { typescript, tsconfigPath } = detectTypeScript(rootDir);
159
+ const packageManager = detectPackageManager(rootDir);
160
+ const componentPatterns = detectComponentPatterns(rootDir, typescript);
161
+ const tokenSources = detectTokenSources(rootDir);
162
+ return {
163
+ framework,
164
+ typescript,
165
+ tsconfigPath,
166
+ componentPatterns,
167
+ tokenSources,
168
+ packageManager
169
+ };
170
+ }
171
+ function buildDefaultConfig(detected, tokenFile, outputDir) {
172
+ const include = detected.componentPatterns.length > 0 ? detected.componentPatterns : ["src/**/*.tsx"];
173
+ return {
174
+ components: {
175
+ include,
176
+ exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
177
+ wrappers: { providers: [], globalCSS: [] }
178
+ },
179
+ render: {
180
+ viewport: { default: { width: 1280, height: 800 } },
181
+ theme: "light",
182
+ warmBrowser: true
183
+ },
184
+ tokens: {
185
+ file: tokenFile,
186
+ compliance: { threshold: 90 }
187
+ },
188
+ output: {
189
+ dir: outputDir,
190
+ sprites: { format: "png", cellPadding: 8, labelAxes: true },
191
+ json: { pretty: true }
192
+ },
193
+ ci: {
194
+ complianceThreshold: 90,
195
+ failOnA11yViolations: true,
196
+ failOnConsoleErrors: false,
197
+ baselinePath: `${outputDir}baseline/`
198
+ }
199
+ };
200
+ }
201
+ function createRL() {
202
+ return readline.createInterface({
203
+ input: process.stdin,
204
+ output: process.stdout
205
+ });
206
+ }
207
+ async function ask(rl, question) {
208
+ return new Promise((resolve6) => {
209
+ rl.question(question, (answer) => {
210
+ resolve6(answer.trim());
211
+ });
212
+ });
213
+ }
214
+ async function askWithDefault(rl, label, defaultValue) {
215
+ const answer = await ask(rl, ` ${label} [${defaultValue}]: `);
216
+ return answer.length > 0 ? answer : defaultValue;
217
+ }
218
+ function ensureGitignoreEntry(rootDir, entry) {
219
+ const gitignorePath = join(rootDir, ".gitignore");
220
+ if (existsSync(gitignorePath)) {
221
+ const content = readFileSync(gitignorePath, "utf-8");
222
+ const normalised = entry.replace(/\/$/, "");
223
+ const lines = content.split("\n").map((l) => l.trim());
224
+ if (lines.includes(entry) || lines.includes(normalised)) {
225
+ return;
226
+ }
227
+ const suffix = content.endsWith("\n") ? "" : "\n";
228
+ appendFileSync(gitignorePath, `${suffix}${entry}
229
+ `);
230
+ } else {
231
+ writeFileSync(gitignorePath, `${entry}
232
+ `);
233
+ }
234
+ }
235
+ function scaffoldConfig(rootDir, config) {
236
+ const path = join(rootDir, "reactscope.config.json");
237
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
238
+ `);
239
+ return path;
240
+ }
241
+ function scaffoldTokenFile(rootDir, tokenFile) {
242
+ const path = join(rootDir, tokenFile);
243
+ if (!existsSync(path)) {
244
+ const stub = {
245
+ $schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
246
+ tokens: {}
247
+ };
248
+ writeFileSync(path, `${JSON.stringify(stub, null, 2)}
249
+ `);
250
+ }
251
+ return path;
252
+ }
253
+ function scaffoldOutputDir(rootDir, outputDir) {
254
+ const dirPath = join(rootDir, outputDir);
255
+ mkdirSync(dirPath, { recursive: true });
256
+ const keepPath = join(dirPath, ".gitkeep");
257
+ if (!existsSync(keepPath)) {
258
+ writeFileSync(keepPath, "");
259
+ }
260
+ return dirPath;
261
+ }
262
+ async function runInit(options) {
263
+ const rootDir = options.cwd ?? process.cwd();
264
+ const configPath = join(rootDir, "reactscope.config.json");
265
+ const created = [];
266
+ if (existsSync(configPath) && !options.force) {
267
+ const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
268
+ process.stderr.write(`\u26A0\uFE0F ${msg}
269
+ `);
270
+ return { success: false, message: msg, created: [], skipped: true };
271
+ }
272
+ const detected = detectProject(rootDir);
273
+ const defaultTokenFile = "reactscope.tokens.json";
274
+ const defaultOutputDir = ".reactscope/";
275
+ let config = buildDefaultConfig(detected, defaultTokenFile, defaultOutputDir);
276
+ if (options.yes) {
277
+ process.stdout.write("\n\u{1F50D} Detected project settings:\n");
278
+ process.stdout.write(` Framework : ${detected.framework}
279
+ `);
280
+ process.stdout.write(` TypeScript : ${detected.typescript}
281
+ `);
282
+ process.stdout.write(` Include globs : ${config.components.include.join(", ")}
283
+ `);
284
+ process.stdout.write(` Token file : ${config.tokens.file}
285
+ `);
286
+ process.stdout.write(` Output dir : ${config.output.dir}
287
+
288
+ `);
289
+ } else {
290
+ const rl = createRL();
291
+ process.stdout.write("\n\u{1F680} scope init \u2014 project configuration\n");
292
+ process.stdout.write(" Press Enter to accept the detected value shown in brackets.\n\n");
293
+ try {
294
+ process.stdout.write(` Detected framework: ${detected.framework}
295
+ `);
296
+ const includeRaw = await askWithDefault(
297
+ rl,
298
+ "Component include patterns (comma-separated)",
299
+ config.components.include.join(", ")
300
+ );
301
+ config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
302
+ const excludeRaw = await askWithDefault(
303
+ rl,
304
+ "Component exclude patterns (comma-separated)",
305
+ config.components.exclude.join(", ")
306
+ );
307
+ config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
308
+ const tokenFile = await askWithDefault(rl, "Token file location", config.tokens.file);
309
+ config.tokens.file = tokenFile;
310
+ config.ci.baselinePath = `${config.output.dir}baseline/`;
311
+ const outputDir = await askWithDefault(rl, "Output directory", config.output.dir);
312
+ config.output.dir = outputDir.endsWith("/") ? outputDir : `${outputDir}/`;
313
+ config.ci.baselinePath = `${config.output.dir}baseline/`;
314
+ config = buildDefaultConfig(detected, config.tokens.file, config.output.dir);
315
+ config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
316
+ config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
317
+ } finally {
318
+ rl.close();
319
+ }
320
+ process.stdout.write("\n");
321
+ }
322
+ const cfgPath = scaffoldConfig(rootDir, config);
323
+ created.push(cfgPath);
324
+ const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
325
+ created.push(tokPath);
326
+ const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
327
+ created.push(outDirPath);
328
+ ensureGitignoreEntry(rootDir, config.output.dir);
329
+ process.stdout.write("\u2705 Scope project initialised!\n\n");
330
+ process.stdout.write(" Created files:\n");
331
+ for (const p of created) {
332
+ process.stdout.write(` ${p}
333
+ `);
334
+ }
335
+ process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
336
+ return {
337
+ success: true,
338
+ message: "Project initialised successfully.",
339
+ created,
340
+ skipped: false
341
+ };
342
+ }
343
+ function createInitCommand() {
344
+ return new Command("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
345
+ try {
346
+ const result = await runInit({ yes: opts.yes, force: opts.force });
347
+ if (!result.success && !result.skipped) {
348
+ process.exit(1);
349
+ }
350
+ } catch (err) {
351
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
352
+ `);
353
+ process.exit(1);
354
+ }
355
+ });
356
+ }
13
357
 
14
358
  // src/manifest-formatter.ts
15
359
  function isTTY() {
@@ -2362,9 +2706,10 @@ function createProgram(options = {}) {
2362
2706
  program.addCommand(createRenderCommand());
2363
2707
  program.addCommand(createTokensCommand());
2364
2708
  program.addCommand(createInstrumentCommand());
2709
+ program.addCommand(createInitCommand());
2365
2710
  return program;
2366
2711
  }
2367
2712
 
2368
- export { createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob };
2713
+ export { createInitCommand, createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob, runInit };
2369
2714
  //# sourceMappingURL=index.js.map
2370
2715
  //# sourceMappingURL=index.js.map