create-mercato-app 0.6.6-develop.6338.1.c32cd17821 → 0.6.6-develop.6339.1.193c6c7c71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mercato-app",
3
- "version": "0.6.6-develop.6338.1.c32cd17821",
3
+ "version": "0.6.6-develop.6339.1.193c6c7c71",
4
4
  "type": "module",
5
5
  "description": "Create a new Open Mercato application",
6
6
  "main": "./dist/index.js",
@@ -35,6 +35,7 @@ import {
35
35
  resolveSplashUrl as resolveSplashAccessUrl,
36
36
  } from './dev-splash-url.mjs'
37
37
  import { resolveDatabaseNameOverride } from './dev-database-url.mjs'
38
+ import { parseWatchScopeArgs, resolveWatchScope } from './watch-scope.mjs'
38
39
 
39
40
  function detectDevRuntimeMode() {
40
41
  const cwd = process.cwd()
@@ -191,6 +192,10 @@ const classic = args.includes('--classic') || isEnabledEnvFlag(process.env.OM_DE
191
192
  const verbose = args.includes('--verbose') || process.env.MERCATO_DEV_OUTPUT === 'verbose'
192
193
  const greenfield = isMonorepo && args.includes('--greenfield')
193
194
  const appOnly = args.includes('--app-only')
195
+ // Watch-scope CLI flags (e.g. `--watch=auto-optimized`, `--watch-popular`) are
196
+ // translated into env vars and forwarded to the spawned package watcher, which
197
+ // reads `OM_WATCH_SCOPE` / `OM_WATCH_PACKAGES`. CLI flags win over a pre-set env.
198
+ const watchScopeArgs = parseWatchScopeArgs(args)
194
199
  const setupMode = !isMonorepo && args.includes('--setup')
195
200
  const reinstall = setupMode && args.includes('--reinstall')
196
201
  const standaloneLocalRegistryRefresh = !isMonorepo && shouldRefreshStandaloneRegistryPackages()
@@ -1557,14 +1562,29 @@ function resolveWatchPackagesScript() {
1557
1562
  return raw === 'legacy' ? 'watch:packages:legacy' : 'watch:packages'
1558
1563
  }
1559
1564
 
1565
+ function buildWatchScopeEnv() {
1566
+ const env = {}
1567
+ if (watchScopeArgs.mode) env.OM_WATCH_SCOPE = watchScopeArgs.mode
1568
+ if (watchScopeArgs.packages?.length) env.OM_WATCH_PACKAGES = watchScopeArgs.packages.join(',')
1569
+ if (watchScopeArgs.popularLimit) env.OM_WATCH_POPULAR_LIMIT = String(watchScopeArgs.popularLimit)
1570
+ return env
1571
+ }
1572
+
1573
+ function resolveActiveWatchScope(watchScopeEnv = buildWatchScopeEnv()) {
1574
+ return resolveWatchScope({ env: { ...process.env, ...watchScopeEnv }, argv: [] }).mode
1575
+ }
1576
+
1560
1577
  function startPackageWatch() {
1561
1578
  const watchScript = resolveWatchPackagesScript()
1579
+ const watchScopeEnv = buildWatchScopeEnv()
1580
+ const activeScope = resolveActiveWatchScope(watchScopeEnv)
1562
1581
 
1563
1582
  if (classic) {
1564
1583
  const child = spawnCommand(yarnCommand, [watchScript], {
1565
1584
  label: watchScript,
1566
1585
  logFile: getDevRunnerLog(),
1567
1586
  mirrorOutput: true,
1587
+ env: watchScopeEnv,
1568
1588
  })
1569
1589
 
1570
1590
  child.on('close', (code, signal) => {
@@ -1586,9 +1606,14 @@ function startPackageWatch() {
1586
1606
  const stageCurrent = greenfield ? 5 : 2
1587
1607
  const stageTotal = greenfield ? 5 : 3
1588
1608
  console.log(`👀 ${formatProgressLine('Watching workspace packages', stageCurrent, stageTotal, resolveProgressPercent(stageCurrent, stageTotal))}`)
1609
+ if (activeScope !== 'all') {
1610
+ console.log(` ↳ watch scope: ${activeScope} (set OM_WATCH_SCOPE=all or pass --watch=all to watch every package)`)
1611
+ }
1589
1612
  updateSplashState({
1590
1613
  phase: 'Watching workspace packages',
1591
- detail: 'Package watchers are running in the background',
1614
+ detail: activeScope === 'all'
1615
+ ? 'Package watchers are running in the background'
1616
+ : `Package watchers are running (watch scope: ${activeScope})`,
1592
1617
  progressCurrent: stageCurrent,
1593
1618
  progressTotal: stageTotal,
1594
1619
  progressPercent: resolveProgressPercent(stageCurrent, stageTotal),
@@ -1600,6 +1625,7 @@ function startPackageWatch() {
1600
1625
  label: 'Watching workspace packages',
1601
1626
  logFile: getDevRunnerLog(),
1602
1627
  mirrorOutput: verbose,
1628
+ env: watchScopeEnv,
1603
1629
  })
1604
1630
 
1605
1631
  if (verbose) {
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+ // Dev-mode watch-scope resolution.
3
+ //
4
+ // Lets developers narrow which workspace packages the consolidated package
5
+ // watcher (`scripts/watch-packages.mjs`) tracks, instead of always watching
6
+ // every package. Four modes are supported (see `apps/docs/docs/appendix/
7
+ // troubleshooting.mdx`):
8
+ //
9
+ // - `all` watch every discovered package (default, unchanged).
10
+ // - `auto-optimized` watch only packages touched recently (git working tree
11
+ // + current-branch diff), re-checking every 2 minutes and
12
+ // expanding watchers to newly-touched packages.
13
+ // - `popular` watch only the most frequently changed packages (ranked
14
+ // from recent git history; static fallback when no git).
15
+ // - `env` watch exactly the packages named in `OM_WATCH_PACKAGES`
16
+ // (or the interactive picker's persisted selection).
17
+ //
18
+ // Mode is selected via `OM_WATCH_SCOPE` or `--watch=<mode>` CLI flags forwarded
19
+ // by `scripts/dev.mjs`. CLI flags win over env. This module is deliberately
20
+ // pure: every git call goes through an injectable `runGit` so the logic stays
21
+ // unit-testable without a real repository.
22
+
23
+ import { execFileSync } from 'node:child_process'
24
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
25
+ import { basename, dirname, join, relative, sep } from 'node:path'
26
+
27
+ export const WATCH_SCOPE_ALL = 'all'
28
+ export const WATCH_SCOPE_AUTO = 'auto-optimized'
29
+ export const WATCH_SCOPE_POPULAR = 'popular'
30
+ export const WATCH_SCOPE_ENV = 'env'
31
+
32
+ export const WATCH_SCOPES = [
33
+ WATCH_SCOPE_ALL,
34
+ WATCH_SCOPE_AUTO,
35
+ WATCH_SCOPE_POPULAR,
36
+ WATCH_SCOPE_ENV,
37
+ ]
38
+
39
+ // Re-check + expand interval for `auto-optimized`.
40
+ export const AUTO_EXPAND_INTERVAL_MS = 120000
41
+
42
+ // Default popular packages when git history cannot rank anything.
43
+ export const DEFAULT_POPULAR_PACKAGES = ['core', 'ui', 'shared']
44
+
45
+ export const DEFAULT_POPULAR_LIMIT = 6
46
+
47
+ // Candidate base refs for the current-branch diff, in priority order.
48
+ const DEFAULT_BASE_REFS = ['origin/develop', 'develop', 'origin/main', 'main']
49
+
50
+ const PERSISTED_SELECTION_RELATIVE = join('.mercato', 'watch-packages.local.json')
51
+
52
+ function safeReadPackageJson(packageDir) {
53
+ const pkgPath = join(packageDir, 'package.json')
54
+ if (!existsSync(pkgPath)) return null
55
+ try {
56
+ return JSON.parse(readFileSync(pkgPath, 'utf8'))
57
+ } catch {
58
+ return null
59
+ }
60
+ }
61
+
62
+ // Discover workspace packages that the consolidated watcher can track: any
63
+ // package with a `watch` script and a `src/` directory under `packages/` or
64
+ // `external/official-modules/packages/`. This is the single discovery source
65
+ // shared by the watcher and the interactive picker; it is intentionally
66
+ // dependency-light (node built-ins only) so it can run inside standalone apps
67
+ // that do not install esbuild/glob.
68
+ export function discoverWatchTargets(root) {
69
+ const roots = [
70
+ join(root, 'packages'),
71
+ join(root, 'external', 'official-modules', 'packages'),
72
+ ]
73
+ const discovered = []
74
+
75
+ for (const parent of roots) {
76
+ if (!existsSync(parent)) continue
77
+ let entries
78
+ try {
79
+ entries = readdirSync(parent, { withFileTypes: true })
80
+ } catch {
81
+ continue
82
+ }
83
+
84
+ for (const entry of entries) {
85
+ if (!entry.isDirectory()) continue
86
+ const packageDir = join(parent, entry.name)
87
+ const pkg = safeReadPackageJson(packageDir)
88
+ if (!pkg) continue
89
+ if (!pkg.scripts?.watch) continue
90
+ const srcDir = join(packageDir, 'src')
91
+ if (!existsSync(srcDir)) continue
92
+
93
+ discovered.push({
94
+ name: pkg.name ?? basename(packageDir),
95
+ packageDir,
96
+ srcDir,
97
+ shortLabel: basename(packageDir),
98
+ })
99
+ }
100
+ }
101
+
102
+ discovered.sort((a, b) => a.shortLabel.localeCompare(b.shortLabel))
103
+ return discovered
104
+ }
105
+
106
+ function normalizeMode(value) {
107
+ if (!value) return null
108
+ const raw = String(value).trim().toLowerCase()
109
+ if (!raw) return null
110
+ // Accept a few friendly aliases.
111
+ if (raw === 'auto' || raw === 'optimized' || raw === 'auto-optimised') return WATCH_SCOPE_AUTO
112
+ if (raw === 'popular' || raw === 'frequent' || raw === 'hot') return WATCH_SCOPE_POPULAR
113
+ if (raw === 'env' || raw === 'list' || raw === 'select' || raw === 'manual') return WATCH_SCOPE_ENV
114
+ if (raw === 'all' || raw === 'full' || raw === 'everything') return WATCH_SCOPE_ALL
115
+ return WATCH_SCOPES.includes(raw) ? raw : null
116
+ }
117
+
118
+ export function parsePackageList(value) {
119
+ if (!value) return []
120
+ const seen = new Set()
121
+ const result = []
122
+ for (const token of String(value).split(/[\s,]+/)) {
123
+ const trimmed = token.trim()
124
+ if (!trimmed) continue
125
+ const key = trimmed.toLowerCase()
126
+ if (seen.has(key)) continue
127
+ seen.add(key)
128
+ result.push(trimmed)
129
+ }
130
+ return result
131
+ }
132
+
133
+ function parsePositiveInt(value, fallback) {
134
+ const parsed = Number.parseInt(String(value ?? '').trim(), 10)
135
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
136
+ }
137
+
138
+ function isDisabledFlag(value) {
139
+ if (value === undefined || value === null) return false
140
+ const raw = String(value).trim().toLowerCase()
141
+ return raw === '0' || raw === 'false' || raw === 'off' || raw === 'no'
142
+ }
143
+
144
+ // Parse the watch-scope-related argv flags. Recognized forms:
145
+ // --watch=<mode> (mode = all|auto-optimized|popular|env + aliases)
146
+ // --watch-auto shorthand for --watch=auto-optimized
147
+ // --watch-popular shorthand for --watch=popular
148
+ // --watch-all shorthand for --watch=all
149
+ // --watch-env shorthand for --watch=env
150
+ // --watch-packages=a,b,c explicit package list (implies env mode)
151
+ // --watch-popular-limit=N popular cap
152
+ export function parseWatchScopeArgs(argv = []) {
153
+ const out = {}
154
+ for (const arg of argv) {
155
+ if (typeof arg !== 'string') continue
156
+ const eq = arg.indexOf('=')
157
+ const key = eq === -1 ? arg : arg.slice(0, eq)
158
+ const value = eq === -1 ? '' : arg.slice(eq + 1)
159
+ switch (key) {
160
+ case '--watch':
161
+ case '--watch-scope': {
162
+ const mode = normalizeMode(value)
163
+ if (mode) out.mode = mode
164
+ break
165
+ }
166
+ case '--watch-auto':
167
+ case '--watch-optimized':
168
+ out.mode = WATCH_SCOPE_AUTO
169
+ break
170
+ case '--watch-popular':
171
+ out.mode = WATCH_SCOPE_POPULAR
172
+ break
173
+ case '--watch-env':
174
+ case '--watch-select':
175
+ out.mode = WATCH_SCOPE_ENV
176
+ break
177
+ case '--watch-all':
178
+ out.mode = WATCH_SCOPE_ALL
179
+ break
180
+ case '--watch-packages': {
181
+ const list = parsePackageList(value)
182
+ if (list.length) {
183
+ out.packages = list
184
+ if (!out.mode) out.mode = WATCH_SCOPE_ENV
185
+ }
186
+ break
187
+ }
188
+ case '--watch-popular-limit': {
189
+ const limit = parsePositiveInt(value, 0)
190
+ if (limit) out.popularLimit = limit
191
+ break
192
+ }
193
+ default:
194
+ break
195
+ }
196
+ }
197
+ return out
198
+ }
199
+
200
+ // Resolve the effective scope config from env + argv. Argv wins over env.
201
+ export function resolveWatchScope({ env = {}, argv = [] } = {}) {
202
+ const fromArgs = parseWatchScopeArgs(argv)
203
+ const mode = fromArgs.mode || normalizeMode(env.OM_WATCH_SCOPE) || WATCH_SCOPE_ALL
204
+ const explicitPackages = fromArgs.packages?.length
205
+ ? fromArgs.packages
206
+ : parsePackageList(env.OM_WATCH_PACKAGES)
207
+ const popularLimit = fromArgs.popularLimit
208
+ || parsePositiveInt(env.OM_WATCH_POPULAR_LIMIT, DEFAULT_POPULAR_LIMIT)
209
+ const popularOverride = parsePackageList(env.OM_WATCH_POPULAR)
210
+ return {
211
+ mode,
212
+ explicitPackages,
213
+ popularLimit,
214
+ popularOverride,
215
+ gitStatusEnabled: !isDisabledFlag(env.OM_WATCH_GIT_STATUS),
216
+ gitBranchEnabled: !isDisabledFlag(env.OM_WATCH_GIT_BRANCH),
217
+ baseRef: typeof env.OM_WATCH_BASE_REF === 'string' && env.OM_WATCH_BASE_REF.trim()
218
+ ? env.OM_WATCH_BASE_REF.trim()
219
+ : null,
220
+ }
221
+ }
222
+
223
+ function packageKeySet(pkg) {
224
+ const keys = new Set()
225
+ if (pkg.shortLabel) keys.add(pkg.shortLabel.toLowerCase())
226
+ if (pkg.name) {
227
+ const name = pkg.name.toLowerCase()
228
+ keys.add(name)
229
+ const slash = name.lastIndexOf('/')
230
+ if (slash !== -1) keys.add(name.slice(slash + 1))
231
+ }
232
+ return keys
233
+ }
234
+
235
+ // Resolve label/name tokens against the discovered package list. Matches on
236
+ // short label and on package name (with or without the npm scope).
237
+ export function matchPackagesByLabels(packages, labels) {
238
+ if (!labels?.length) return []
239
+ const wanted = new Set(labels.map((label) => String(label).trim().toLowerCase()).filter(Boolean))
240
+ return packages.filter((pkg) => {
241
+ for (const key of packageKeySet(pkg)) {
242
+ if (wanted.has(key)) return true
243
+ }
244
+ return false
245
+ })
246
+ }
247
+
248
+ function toRelativeDir(root, dir) {
249
+ const rel = relative(root, dir)
250
+ if (!rel || rel.startsWith('..')) return null
251
+ return rel.split(sep).join('/')
252
+ }
253
+
254
+ // Map repo-relative changed file paths to the packages that own them.
255
+ export function mapChangedPathsToPackages(packages, root, changedPaths) {
256
+ if (!changedPaths?.length) return []
257
+ const prefixes = packages
258
+ .map((pkg) => {
259
+ const relDir = toRelativeDir(root, pkg.packageDir)
260
+ return relDir ? { pkg, prefix: `${relDir}/` } : null
261
+ })
262
+ .filter(Boolean)
263
+ const matched = new Map()
264
+ for (const rawPath of changedPaths) {
265
+ const normalized = String(rawPath).trim().replace(/\\/g, '/')
266
+ if (!normalized) continue
267
+ for (const { pkg, prefix } of prefixes) {
268
+ if (normalized.startsWith(prefix)) {
269
+ matched.set(pkg.shortLabel, pkg)
270
+ break
271
+ }
272
+ }
273
+ }
274
+ return [...matched.values()]
275
+ }
276
+
277
+ function defaultRunGit(root, gitArgs) {
278
+ return execFileSync('git', gitArgs, {
279
+ cwd: root,
280
+ encoding: 'utf8',
281
+ stdio: ['ignore', 'pipe', 'ignore'],
282
+ maxBuffer: 32 * 1024 * 1024,
283
+ })
284
+ }
285
+
286
+ function safeRunGit(runGit, root, gitArgs) {
287
+ try {
288
+ const output = runGit(root, gitArgs)
289
+ return typeof output === 'string' ? output : ''
290
+ } catch {
291
+ return ''
292
+ }
293
+ }
294
+
295
+ function parseStatusPaths(output) {
296
+ const paths = []
297
+ for (const line of output.split('\n')) {
298
+ if (!line) continue
299
+ // `git status --porcelain` lines look like `XY path` or `XY old -> new`.
300
+ const body = line.slice(3)
301
+ if (!body) continue
302
+ const arrow = body.indexOf(' -> ')
303
+ paths.push(arrow === -1 ? body.trim() : body.slice(arrow + 4).trim())
304
+ }
305
+ return paths
306
+ }
307
+
308
+ function resolveBaseRef(runGit, root, preferredBaseRef) {
309
+ const candidates = preferredBaseRef ? [preferredBaseRef, ...DEFAULT_BASE_REFS] : DEFAULT_BASE_REFS
310
+ for (const ref of candidates) {
311
+ const verified = safeRunGit(runGit, root, ['rev-parse', '--verify', '--quiet', ref]).trim()
312
+ if (verified) return ref
313
+ }
314
+ return null
315
+ }
316
+
317
+ // Discover packages "touched recently" via git: uncommitted working-tree
318
+ // changes (status) and the current branch's diff against a base ref.
319
+ export function detectTouchedPackages({
320
+ packages,
321
+ root,
322
+ config = {},
323
+ runGit = defaultRunGit,
324
+ }) {
325
+ const changedPaths = new Set()
326
+
327
+ if (config.gitStatusEnabled !== false) {
328
+ for (const path of parseStatusPaths(safeRunGit(runGit, root, ['status', '--porcelain']))) {
329
+ changedPaths.add(path)
330
+ }
331
+ }
332
+
333
+ if (config.gitBranchEnabled !== false) {
334
+ const baseRef = resolveBaseRef(runGit, root, config.baseRef)
335
+ if (baseRef) {
336
+ const diff = safeRunGit(runGit, root, ['diff', '--name-only', `${baseRef}...HEAD`])
337
+ for (const path of diff.split('\n')) {
338
+ const trimmed = path.trim()
339
+ if (trimmed) changedPaths.add(trimmed)
340
+ }
341
+ }
342
+ }
343
+
344
+ return mapChangedPathsToPackages(packages, root, [...changedPaths])
345
+ }
346
+
347
+ function parseLogFrequencies(output) {
348
+ const counts = new Map()
349
+ for (const line of output.split('\n')) {
350
+ const path = line.trim()
351
+ if (!path) continue
352
+ counts.set(path, (counts.get(path) ?? 0) + 1)
353
+ }
354
+ return counts
355
+ }
356
+
357
+ // Rank packages by how often their files changed in recent git history.
358
+ export function resolvePopularPackages({
359
+ packages,
360
+ root,
361
+ limit = DEFAULT_POPULAR_LIMIT,
362
+ override = [],
363
+ runGit = defaultRunGit,
364
+ }) {
365
+ if (override.length) {
366
+ const matched = matchPackagesByLabels(packages, override)
367
+ if (matched.length) return matched.slice(0, Math.max(limit, matched.length))
368
+ }
369
+
370
+ const log = safeRunGit(runGit, root, [
371
+ 'log',
372
+ '-n',
373
+ '400',
374
+ '--no-merges',
375
+ '--name-only',
376
+ '--pretty=format:',
377
+ ])
378
+ const counts = parseLogFrequencies(log)
379
+
380
+ if (counts.size) {
381
+ const prefixes = packages
382
+ .map((pkg) => {
383
+ const relDir = toRelativeDir(root, pkg.packageDir)
384
+ return relDir ? { pkg, prefix: `${relDir}/` } : null
385
+ })
386
+ .filter(Boolean)
387
+ const score = new Map()
388
+ for (const [path, count] of counts) {
389
+ const normalized = path.replace(/\\/g, '/')
390
+ for (const { pkg, prefix } of prefixes) {
391
+ if (normalized.startsWith(prefix)) {
392
+ score.set(pkg.shortLabel, (score.get(pkg.shortLabel) ?? 0) + count)
393
+ break
394
+ }
395
+ }
396
+ }
397
+ if (score.size) {
398
+ const ranked = packages
399
+ .filter((pkg) => score.has(pkg.shortLabel))
400
+ .sort((a, b) => (score.get(b.shortLabel) ?? 0) - (score.get(a.shortLabel) ?? 0))
401
+ return ranked.slice(0, limit)
402
+ }
403
+ }
404
+
405
+ // Fallback: the canonical high-churn packages that actually exist here.
406
+ const fallback = matchPackagesByLabels(packages, DEFAULT_POPULAR_PACKAGES)
407
+ return (fallback.length ? fallback : packages).slice(0, limit)
408
+ }
409
+
410
+ export function persistedSelectionPath(root) {
411
+ return join(root, PERSISTED_SELECTION_RELATIVE)
412
+ }
413
+
414
+ export function readPersistedSelection(root) {
415
+ const file = persistedSelectionPath(root)
416
+ if (!existsSync(file)) return []
417
+ try {
418
+ const parsed = JSON.parse(readFileSync(file, 'utf8'))
419
+ if (Array.isArray(parsed)) return parsePackageList(parsed.join(','))
420
+ if (parsed && Array.isArray(parsed.packages)) return parsePackageList(parsed.packages.join(','))
421
+ return []
422
+ } catch {
423
+ return []
424
+ }
425
+ }
426
+
427
+ export function writePersistedSelection(root, labels) {
428
+ const file = persistedSelectionPath(root)
429
+ mkdirSync(dirname(file), { recursive: true })
430
+ const payload = { packages: parsePackageList((labels ?? []).join(',')) }
431
+ writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`)
432
+ return file
433
+ }
434
+
435
+ // Resolve the final set of packages to watch for a given scope config.
436
+ // Returns { selected, mode, autoExpand, reason }.
437
+ export function selectWatchedPackages({
438
+ packages,
439
+ config,
440
+ root,
441
+ runGit = defaultRunGit,
442
+ }) {
443
+ const mode = config?.mode ?? WATCH_SCOPE_ALL
444
+
445
+ if (mode === WATCH_SCOPE_ALL || !packages?.length) {
446
+ return { selected: packages ?? [], mode: WATCH_SCOPE_ALL, autoExpand: false, reason: 'watching all packages' }
447
+ }
448
+
449
+ if (mode === WATCH_SCOPE_ENV) {
450
+ const labels = config.explicitPackages?.length
451
+ ? config.explicitPackages
452
+ : readPersistedSelection(root)
453
+ const selected = matchPackagesByLabels(packages, labels)
454
+ if (!selected.length) {
455
+ return {
456
+ selected: packages,
457
+ mode: WATCH_SCOPE_ALL,
458
+ autoExpand: false,
459
+ reason: 'env scope requested but no packages matched OM_WATCH_PACKAGES / persisted selection — watching all',
460
+ }
461
+ }
462
+ return { selected, mode, autoExpand: false, reason: `watching ${selected.length} package(s) from explicit selection` }
463
+ }
464
+
465
+ if (mode === WATCH_SCOPE_POPULAR) {
466
+ const selected = resolvePopularPackages({
467
+ packages,
468
+ root,
469
+ limit: config.popularLimit ?? DEFAULT_POPULAR_LIMIT,
470
+ override: config.popularOverride ?? [],
471
+ runGit,
472
+ })
473
+ return { selected, mode, autoExpand: false, reason: `watching ${selected.length} most-popular package(s)` }
474
+ }
475
+
476
+ // auto-optimized
477
+ const touched = detectTouchedPackages({ packages, root, config, runGit })
478
+ if (touched.length) {
479
+ return { selected: touched, mode, autoExpand: true, reason: `watching ${touched.length} recently-touched package(s), expanding every 2m` }
480
+ }
481
+ // Nothing touched yet — seed with the popular fallback and grow from there.
482
+ const seed = resolvePopularPackages({
483
+ packages,
484
+ root,
485
+ limit: config.popularLimit ?? DEFAULT_POPULAR_LIMIT,
486
+ override: config.popularOverride ?? [],
487
+ runGit,
488
+ })
489
+ return { selected: seed, mode, autoExpand: true, reason: `no recent changes detected — seeding ${seed.length} package(s), expanding every 2m` }
490
+ }