@skitterbyte/skitterspec-linear 3.4.0 → 5.0.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/src/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
5
  const { execFileSync } = require('child_process')
6
- const { init } = require('./init.js')
6
+ const { init, resync, reset, isExistingSetup } = require('./init.js')
7
7
  const {
8
8
  detectReleaseTooling,
9
9
  removeReleaseTooling,
@@ -21,10 +21,21 @@ const {
21
21
  resolveSpec,
22
22
  resolveBaseBranch,
23
23
  resolvePrimaryCheckout,
24
+ assertPrimaryOnMain,
24
25
  repoInfo,
25
26
  expandTokens,
26
27
  splitPrefix,
27
28
  } = require('./env/resolve.js')
29
+ const {
30
+ readReceipt,
31
+ writeReceipt,
32
+ clearReceipt,
33
+ summarizeReceipt,
34
+ migrationsHit,
35
+ planTake,
36
+ planRelease,
37
+ planAbort,
38
+ } = require('./env/live.js')
28
39
  const { ensureWorktreeDirTrusted } = require('./env/trust.js')
29
40
  const { planUp } = require('./env/provision.js')
30
41
  const { planDown } = require('./env/teardown.js')
@@ -39,9 +50,12 @@ const HELP = `skitterspec — spec-driven-development for Claude Code
39
50
 
40
51
  Usage:
41
52
  skitterspec init [dir] Install the spec lifecycle skills, rule, and specs/
42
- folders into a project
43
- skitterspec update [dir] Re-copy skills + rule (overwrites), leaves specs/
44
- and specs/.core/ config alone
53
+ folders. On an already-set-up repo it detects that
54
+ and offers resync / start-again / leave (interactive)
55
+ non-interactively it just adds anything missing.
56
+ skitterspec update [dir] Resync managed files to the latest, keeping your
57
+ edits (--force to overwrite). Leaves specs/ + live
58
+ .core config alone.
45
59
  skitterspec spec-env <cmd> Per-spec isolation engine (opt-in; needs
46
60
  specs/.core/env.config.json). Subcommands:
47
61
  up <spec> plan a worktree + Docker stack + opener
@@ -56,6 +70,9 @@ Usage:
56
70
  skitterspec --version Print version
57
71
 
58
72
  Options (init / update):
73
+ --resync (init) Update managed files to latest, keep your edits
74
+ --reset (init) Start again: reset managed scaffolding fresh
75
+ (needs --yes; never touches your specs or config)
59
76
  --force Overwrite skill/rule/script files that already exist
60
77
  --dir <path> Target project dir (default: positional arg or cwd)
61
78
  --no-claude-md Skip creating/patching CLAUDE.md
@@ -80,6 +97,8 @@ function parse(argv) {
80
97
  yes: false,
81
98
  isolation: undefined,
82
99
  removeReleaseTooling: false,
100
+ resync: false,
101
+ reset: false,
83
102
  }
84
103
  const positional = []
85
104
  for (let i = 0; i < argv.length; i++) {
@@ -90,6 +109,8 @@ function parse(argv) {
90
109
  else if (a === '--isolation') opts.isolation = true
91
110
  else if (a === '--no-isolation') opts.isolation = false
92
111
  else if (a === '--remove-release-tooling') opts.removeReleaseTooling = true
112
+ else if (a === '--resync') opts.resync = true
113
+ else if (a === '--reset') opts.reset = true
93
114
  else if (a === '--dir') opts.dir = argv[++i]
94
115
  else if (a.startsWith('--')) throw new Error(`unknown option: ${a}`)
95
116
  else positional.push(a)
@@ -348,21 +369,43 @@ function specEnvIntegrate(dir, config, specArg) {
348
369
 
349
370
  // `dir` is already anchored on the primary checkout by the dispatch, so it is
350
371
  // both where the spec resolves and the target of the fast-forward — /spec-complete
351
- // can run this from inside the worktree and still land on main.
352
- //
353
- // A spec authored entirely on its branch may not exist in the primary
354
- // checkout's specs/** (it was never committed to base) — but its worktree
355
- // does, and the worktree path is derivable from config without the folder.
356
- // Offer it as a fallback search location so integrate can still find the spec.
357
- const { slug } = splitPrefix(path.basename(specArg))
358
- const { repo, repoSlug } = repoInfo(dir)
359
- const wtTokens = { repo, repoSlug, slug }
360
- const worktreeGuess = path.resolve(
361
- dir,
362
- expandTokens(config.worktree.root, wtTokens),
363
- expandTokens(config.worktree.folderPattern, wtTokens),
364
- )
365
- const spec = resolveSpec(specArg, dir, config, { searchDirs: [worktreeGuess] })
372
+ // can run this from inside the worktree and still land on main. A spec authored
373
+ // entirely on its branch may not exist in the primary checkout's specs/** —
374
+ // resolveSpecWithWorktree offers its worktree as a fallback search location.
375
+ const spec = resolveSpecWithWorktree(dir, config, specArg)
376
+ const base = resolveBaseBranch(config, gitReader(dir))
377
+
378
+ // Live-aware: if this spec is live on the primary checkout (branch-switched by
379
+ // `live take`), end the live session first — release back to base, re-isolate the
380
+ // branch, clear the receipt so the normal rebase→ff plan below applies
381
+ // unchanged. Refuse if a *different* spec holds the primary checkout.
382
+ const primary = assertPrimaryOnMain(config, gitReader(dir))
383
+ if (!primary.onBase) {
384
+ if (primary.branch !== spec.branch) {
385
+ process.stdout.write(
386
+ `spec-env integrate: blocked another spec (${primary.branch}) holds the ` +
387
+ 'primary checkout; release it with `/spec-live main` first.\n',
388
+ )
389
+ return
390
+ }
391
+ const pstatus = gitReader(dir)(['status', '--porcelain'])
392
+ if (pstatus === null || pstatus.length > 0) {
393
+ process.stdout.write(
394
+ `spec-env integrate: blocked — commit your live fixes to ${spec.branch} first.\n`,
395
+ )
396
+ return
397
+ }
398
+ const co = runGit(dir, ['checkout', base])
399
+ if (!co.ok) {
400
+ process.stdout.write(`spec-env integrate: could not check out ${base} — ${co.err}\n`)
401
+ return
402
+ }
403
+ if (fs.existsSync(spec.worktreePath)) runGit(spec.worktreePath, ['switch', spec.branch])
404
+ clearReceipt(dir, config)
405
+ process.stdout.write(
406
+ `spec-env integrate: ended live session — ${spec.folder} released to its worktree.\n`,
407
+ )
408
+ }
366
409
 
367
410
  if (!fs.existsSync(spec.worktreePath)) {
368
411
  process.stdout.write(
@@ -371,7 +414,6 @@ function specEnvIntegrate(dir, config, specArg) {
371
414
  return
372
415
  }
373
416
 
374
- const base = resolveBaseBranch(config, gitReader(dir))
375
417
  const wtGit = gitReader(spec.worktreePath)
376
418
  const status = wtGit(['status', '--porcelain'])
377
419
  const dirty = status !== null && status.length > 0
@@ -588,6 +630,276 @@ async function specEnvConnect(dir, config, specArg) {
588
630
 
589
631
  // Dispatch `skitterspec spec-env <sub> [args] [--dir path]`. No-ops with a clear
590
632
  // message when the feature isn't enabled (no specs/.core/env.config.json).
633
+ // Run a mutating git command; return { ok, err } (stderr captured, not swallowed).
634
+ function runGit(cwd, args) {
635
+ try {
636
+ execFileSync('git', ['-C', cwd, ...args], { stdio: ['ignore', 'pipe', 'pipe'] })
637
+ return { ok: true, err: '' }
638
+ } catch (error) {
639
+ const err = (error.stderr && error.stderr.toString().trim()) || error.message
640
+ return { ok: false, err }
641
+ }
642
+ }
643
+
644
+ // Lockfiles/manifests whose change means a dev server needs a restart, not HMR.
645
+ const DEPS_RE = /(^|\/)(package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$/
646
+
647
+ // Live overlay: test a spec on the already-running instance by checking its
648
+ // branch out in the primary checkout, so the running dev server hot-reloads the
649
+ // feature. The branch that's checked out IS the lock (assertPrimaryOnMain); the
650
+ // receipt is advisory metadata. `status` is read-only; `take` performs the switch
651
+ // (release/abort land in a later phase).
652
+ async function specEnvLive(dir, config, positional) {
653
+ const action = positional[0] || 'status'
654
+ switch (action) {
655
+ case 'status':
656
+ specEnvLiveStatus(dir, config)
657
+ break
658
+ case 'take':
659
+ await specEnvLiveTake(dir, config, positional[1])
660
+ break
661
+ case 'release':
662
+ await specEnvLiveRelease(dir, config, positional[1])
663
+ break
664
+ case 'abort':
665
+ await specEnvLiveAbort(dir, config)
666
+ break
667
+ default:
668
+ process.stdout.write('Usage: skitterspec spec-env live <take|release|abort|status> [spec]\n')
669
+ }
670
+ }
671
+
672
+ // Resolve a spec, offering its worktree as a fallback search dir — a spec authored
673
+ // on its own branch may not exist in the primary checkout's specs/**.
674
+ function resolveSpecWithWorktree(dir, config, specArg) {
675
+ const { slug } = splitPrefix(path.basename(specArg))
676
+ const { repo, repoSlug } = repoInfo(dir)
677
+ const wtTokens = { repo, repoSlug, slug }
678
+ const worktreeGuess = path.resolve(
679
+ dir,
680
+ expandTokens(config.worktree.root, wtTokens),
681
+ expandTokens(config.worktree.folderPattern, wtTokens),
682
+ )
683
+ return resolveSpec(specArg, dir, config, { searchDirs: [worktreeGuess] })
684
+ }
685
+
686
+ // Take the running instance: rebase the spec's branch onto base, free it from its
687
+ // worktree, and check it out in the primary checkout so the dev server reloads it.
688
+ async function specEnvLiveTake(dir, config, specArg) {
689
+ if (!specArg) {
690
+ process.stdout.write('Usage: skitterspec spec-env live take <spec>\n')
691
+ return
692
+ }
693
+
694
+ const spec = resolveSpecWithWorktree(dir, config, specArg)
695
+
696
+ // Probe the primary checkout's git state (IO stays here; the planner is pure).
697
+ const primaryGit = gitReader(dir)
698
+ const primary = assertPrimaryOnMain(config, primaryGit)
699
+ const base = resolveBaseBranch(config, primaryGit)
700
+ const status = primaryGit(['status', '--porcelain'])
701
+ const clean = status !== null && status.length === 0
702
+ const worktreeExists = fs.existsSync(spec.worktreePath)
703
+ const baseMainCommit = primaryGit(['rev-parse', 'HEAD'])
704
+
705
+ // Diff base...branch to spot migration / dependency changes (best-effort).
706
+ const changed = primaryGit(['diff', '--name-only', `${base}...${spec.branch}`])
707
+ const files = changed ? changed.split('\n').filter(Boolean) : []
708
+ const depsChanged = files.some((f) => DEPS_RE.test(f))
709
+
710
+ // Verify-only: probe the declared canonical (frontPort) ports. None declared →
711
+ // no health gate (serverUp = null); the switch proceeds with a warning.
712
+ const canonicalPorts = config.dev.map((d) => d.frontPort).filter((p) => typeof p === 'number')
713
+ let serverUp = null
714
+ if (canonicalPorts.length) {
715
+ const up = await portsInUse(canonicalPorts, config.proxy.host)
716
+ serverUp = up.length === canonicalPorts.length
717
+ }
718
+
719
+ const plan = planTake(spec, config, {
720
+ primary,
721
+ primaryPath: dir,
722
+ clean,
723
+ worktreeExists,
724
+ base,
725
+ baseMainCommit,
726
+ serverUp,
727
+ canonicalPorts,
728
+ migrationsHit: migrationsHit(files, config.live.migrations),
729
+ depsChanged,
730
+ holder: primaryGit(['config', 'user.name']) || 'unknown',
731
+ heldSince: new Date().toISOString(),
732
+ })
733
+
734
+ if (plan.blocked) {
735
+ process.stdout.write(`spec-env live take: blocked — ${plan.reason}.\n`)
736
+ return
737
+ }
738
+
739
+ // Execute the switch. Rebase first; on conflict, abort and bail (state untouched).
740
+ const reb = runGit(spec.worktreePath, ['rebase', base])
741
+ if (!reb.ok) {
742
+ runGit(spec.worktreePath, ['rebase', '--abort'])
743
+ process.stdout.write(
744
+ `spec-env live take: rebase of ${spec.branch} onto ${base} hit conflicts — ` +
745
+ `resolve them in ${spec.worktreePath}, then retry.\n`,
746
+ )
747
+ return
748
+ }
749
+ const det = runGit(spec.worktreePath, ['switch', '--detach'])
750
+ if (!det.ok) {
751
+ process.stdout.write(`spec-env live take: could not detach the worktree — ${det.err}\n`)
752
+ return
753
+ }
754
+ const co = runGit(dir, ['checkout', spec.branch])
755
+ if (!co.ok) {
756
+ // Roll the detach back so the worktree keeps its branch.
757
+ runGit(spec.worktreePath, ['switch', spec.branch])
758
+ process.stdout.write(
759
+ `spec-env live take: could not check out ${spec.branch} in the primary ` +
760
+ `checkout — ${co.err}\n`,
761
+ )
762
+ return
763
+ }
764
+ const receipt = writeReceipt(dir, config, plan.receipt)
765
+
766
+ const out = [`spec-env live take: ${spec.folder} is live on the primary checkout`]
767
+ out.push('')
768
+ out.push(` primary: now on ${receipt.branch} (was ${base} @ ${receipt.baseMainCommit.slice(0, 7)})`)
769
+ out.push(` worktree: ${spec.worktreePath} (detached — branch handed to the primary checkout)`)
770
+ for (const w of plan.warnings) out.push(` ! ${w}`)
771
+ out.push('')
772
+ out.push(' Test at your canonical URL; release with: /spec-live main')
773
+ process.stdout.write(out.join('\n') + '\n')
774
+ }
775
+
776
+ // Release the running instance: hand the primary checkout back to base and
777
+ // re-isolate the spec's branch into its worktree. With no spec arg, the live spec
778
+ // is read from the receipt (this is what `/spec-live main` runs).
779
+ async function specEnvLiveRelease(dir, config, specArg) {
780
+ const receipt = readReceipt(dir, config)
781
+ const target = specArg || (receipt && receipt.spec)
782
+ if (!target) {
783
+ const primaryGit = gitReader(dir)
784
+ const primary = assertPrimaryOnMain(config, primaryGit)
785
+ process.stdout.write(
786
+ primary.onBase
787
+ ? `spec-env live release: nothing is live — the primary checkout is on ${primary.baseBranch}.\n`
788
+ : `spec-env live release: no receipt, but the primary checkout is on ${primary.branch} — ` +
789
+ 'use `spec-env live abort` to recover.\n',
790
+ )
791
+ return
792
+ }
793
+
794
+ const spec = resolveSpecWithWorktree(dir, config, target)
795
+ const primaryGit = gitReader(dir)
796
+ const primary = assertPrimaryOnMain(config, primaryGit)
797
+ const base = resolveBaseBranch(config, primaryGit)
798
+ const status = primaryGit(['status', '--porcelain'])
799
+ const clean = status !== null && status.length === 0
800
+ const worktreeExists = fs.existsSync(spec.worktreePath)
801
+
802
+ const plan = planRelease(spec, config, {
803
+ primary,
804
+ primaryPath: dir,
805
+ base,
806
+ clean,
807
+ worktreeExists,
808
+ })
809
+
810
+ if (plan.noop) {
811
+ process.stdout.write(`spec-env live release: ${plan.reason}.\n`)
812
+ return
813
+ }
814
+ if (plan.blocked) {
815
+ process.stdout.write(`spec-env live release: blocked — ${plan.reason}.\n`)
816
+ return
817
+ }
818
+
819
+ const co = runGit(dir, ['checkout', base])
820
+ if (!co.ok) {
821
+ process.stdout.write(`spec-env live release: could not check out ${base} — ${co.err}\n`)
822
+ return
823
+ }
824
+ if (worktreeExists) runGit(spec.worktreePath, ['switch', spec.branch])
825
+ clearReceipt(dir, config)
826
+
827
+ process.stdout.write(
828
+ `spec-env live release: ${spec.folder} released — primary back on ${base}, ` +
829
+ `${spec.branch} re-isolated to its worktree.\n`,
830
+ )
831
+ }
832
+
833
+ // Crash recovery: force the primary checkout back to base from the receipt and
834
+ // re-isolate, without discarding uncommitted work (it refuses on a dirty tree).
835
+ async function specEnvLiveAbort(dir, config) {
836
+ const receipt = readReceipt(dir, config)
837
+ const primaryGit = gitReader(dir)
838
+ const primary = assertPrimaryOnMain(config, primaryGit)
839
+ const base = resolveBaseBranch(config, primaryGit)
840
+ const status = primaryGit(['status', '--porcelain'])
841
+ const clean = status !== null && status.length === 0
842
+
843
+ // Resolve the worktree from the receipt (best-effort — may be gone/unresolvable).
844
+ let worktreePath = null
845
+ if (receipt) {
846
+ try {
847
+ worktreePath = resolveSpecWithWorktree(dir, config, receipt.spec).worktreePath
848
+ } catch {
849
+ worktreePath = null
850
+ }
851
+ }
852
+ const worktreeExists = worktreePath !== null && fs.existsSync(worktreePath)
853
+
854
+ const plan = planAbort(config, {
855
+ receipt,
856
+ primary,
857
+ primaryPath: dir,
858
+ base,
859
+ clean,
860
+ worktreeExists,
861
+ worktreePath,
862
+ })
863
+
864
+ if (plan.noop) {
865
+ process.stdout.write(`spec-env live abort: ${plan.reason}.\n`)
866
+ return
867
+ }
868
+ if (plan.blocked) {
869
+ process.stdout.write(`spec-env live abort: blocked — ${plan.reason}.\n`)
870
+ return
871
+ }
872
+
873
+ if (!primary.onBase) {
874
+ const co = runGit(dir, ['checkout', base])
875
+ if (!co.ok) {
876
+ process.stdout.write(`spec-env live abort: could not check out ${base} — ${co.err}\n`)
877
+ return
878
+ }
879
+ }
880
+ if (worktreeExists) runGit(worktreePath, ['switch', plan.branch])
881
+ clearReceipt(dir, config)
882
+
883
+ process.stdout.write(
884
+ `spec-env live abort: recovered — primary back on ${base}` +
885
+ (worktreeExists ? `, ${plan.branch} re-isolated to its worktree` : '') +
886
+ '.\n',
887
+ )
888
+ }
889
+
890
+ function specEnvLiveStatus(dir, config) {
891
+ const { onBase, branch, baseBranch } = assertPrimaryOnMain(config, gitReader(dir))
892
+ const receipt = readReceipt(dir, config)
893
+ const state = onBase
894
+ ? 'on base — free'
895
+ : `feature in control — not on ${baseBranch}`
896
+ process.stdout.write(
897
+ 'spec-env live:\n' +
898
+ ` primary: ${branch || '(detached)'} (${state})\n` +
899
+ ` receipt: ${summarizeReceipt(receipt)}\n`,
900
+ )
901
+ }
902
+
591
903
  async function specEnv(rest) {
592
904
  const [sub, ...args] = rest
593
905
  let dir = process.cwd()
@@ -635,9 +947,12 @@ async function specEnv(rest) {
635
947
  case 'resolve':
636
948
  specEnvResolve(dir, config, positional[0])
637
949
  break
950
+ case 'live':
951
+ await specEnvLive(dir, config, positional)
952
+ break
638
953
  default:
639
954
  process.stdout.write(
640
- 'Usage: skitterspec spec-env <up|down|dev|connect|integrate|status|resolve> [spec] [--keep-volumes] [--force]\n',
955
+ 'Usage: skitterspec spec-env <up|down|dev|connect|integrate|live|status|resolve> [spec] [--keep-volumes] [--force]\n',
641
956
  )
642
957
  }
643
958
  }
@@ -664,21 +979,54 @@ async function run(argv) {
664
979
 
665
980
  switch (cmd) {
666
981
  case 'init': {
667
- // Isolation defaults OFF; a flag or an interactive "yes" opts in.
668
- let isolation = opts.isolation === true
669
-
670
982
  const interactive = Boolean(process.stdin.isTTY) && !opts.yes
671
- if (interactive) {
672
- const { promptSetup } = require('./prompts.js')
673
- const result = await promptSetup({ isolationSeed: isolation })
674
- isolation = result.isolation
983
+
984
+ // Already set up? Route to resync / reset / leave instead of a silent
985
+ // create-missing (safer-init). A fresh repo falls straight through.
986
+ if (isExistingSetup(dir)) {
987
+ let action
988
+ if (opts.reset) {
989
+ if (!opts.yes) {
990
+ process.stdout.write('init: --reset overwrites managed files — re-run with --yes. Left unchanged.\n')
991
+ break
992
+ }
993
+ action = 'reset'
994
+ } else if (opts.resync) action = 'resync'
995
+ else if (opts.force) action = 'resync' // --force resyncs, clobbering customized
996
+ else if (interactive) {
997
+ const { promptExistingSetup } = require('./prompts.js')
998
+ action = await promptExistingSetup()
999
+ } else action = 'create-missing' // non-interactive default: add missing, never clobber
1000
+
1001
+ if (action === 'leave') {
1002
+ process.stdout.write('init: existing setup left unchanged.\n')
1003
+ break
1004
+ }
1005
+ if (action === 'reset') {
1006
+ reset(dir, { claudeMd: opts.claudeMd })
1007
+ break
1008
+ }
1009
+ if (action === 'resync') {
1010
+ resync(dir, { claudeMd: opts.claudeMd, force: opts.force })
1011
+ break
1012
+ }
1013
+ // action === 'create-missing' → fall through to a normal (skip-existing) init.
675
1014
  }
676
1015
 
1016
+ // Fresh repo (or create-missing): isolation defaults OFF; a flag or an
1017
+ // interactive "yes" opts in. Only prompt for isolation on a fresh repo.
1018
+ let isolation = opts.isolation === true
1019
+ if (interactive && !isExistingSetup(dir)) {
1020
+ const { promptSetup } = require('./prompts.js')
1021
+ isolation = (await promptSetup({ isolationSeed: isolation })).isolation
1022
+ }
677
1023
  await init({ dir, force: opts.force, claudeMd: opts.claudeMd, mode: 'init', isolation })
678
1024
  break
679
1025
  }
680
1026
  case 'update':
681
- await init({ dir, force: true, claudeMd: opts.claudeMd, mode: 'update' })
1027
+ // `update` is a resync refresh managed files, keep customized ones
1028
+ // (--force to overwrite). Leaves specs/ and live .core config alone.
1029
+ resync(dir, { claudeMd: opts.claudeMd, force: opts.force })
682
1030
  await cleanupReleaseTooling(dir, opts)
683
1031
  break
684
1032
  default:
package/src/env/config.js CHANGED
@@ -28,7 +28,9 @@
28
28
  * registry: ".spec-env/registry.json",
29
29
  * branch: { pattern, identifierField }, // git branch naming (provider-neutral)
30
30
  * baseBranch: "", // "" = auto-detect (origin/HEAD → main → master)
31
- * guards: { refuseTeardownIfDirty, refuseTeardownIfUnpushed }
31
+ * guards: { refuseTeardownIfDirty, refuseTeardownIfUnpushed },
32
+ * live: { migrations: [ "glob", ... ] } // migration globs → `live take`
33
+ * // refuses a branch that changes them (code-only v1)
32
34
  * }
33
35
  */
34
36
 
@@ -74,6 +76,10 @@ const DEFAULT_CONFIG = Object.freeze({
74
76
  // Integration base branch. Empty = auto-detect (origin/HEAD → main → master).
75
77
  baseBranch: '',
76
78
  guards: Object.freeze({ refuseTeardownIfDirty: true, refuseTeardownIfUnpushed: true }),
79
+ // Live overlay (`spec-env live`). `migrations` is a list of globs marking
80
+ // migration files; a branch that changes any of them is treated as stateful and
81
+ // `live take` refuses it (code-only v1). Default: none (nothing is stateful).
82
+ live: Object.freeze({ migrations: Object.freeze([]) }),
77
83
  })
78
84
 
79
85
  function isObject(value) {
@@ -94,6 +100,7 @@ function defaults() {
94
100
  branch: { ...DEFAULT_CONFIG.branch },
95
101
  baseBranch: DEFAULT_CONFIG.baseBranch,
96
102
  guards: { ...DEFAULT_CONFIG.guards },
103
+ live: { migrations: [] },
97
104
  }
98
105
  }
99
106
 
@@ -237,6 +244,10 @@ function mergeConfig(base, parsed) {
237
244
  assign(base.guards, parsed.guards, 'refuseTeardownIfUnpushed', 'boolean')
238
245
  }
239
246
 
247
+ if (isObject(parsed.live) && Array.isArray(parsed.live.migrations)) {
248
+ base.live.migrations = normalizeFileList(parsed.live.migrations)
249
+ }
250
+
240
251
  return base
241
252
  }
242
253