@skitterbyte/skitterspec-linear 10.6.0 → 10.7.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 (34) hide show
  1. package/assets/claude-md-section.md +19 -9
  2. package/assets/commands/spec-connect.md +13 -0
  3. package/assets/commands/spec-live.md +14 -0
  4. package/assets/core/ci-stages.md +110 -0
  5. package/assets/core/env.config.md +18 -0
  6. package/assets/core/linear.config.json.example +3 -0
  7. package/assets/core/linear.config.md +46 -0
  8. package/assets/rules/commit-trailers.md +37 -5
  9. package/assets/rules/spec-planning.md +17 -3
  10. package/assets/skills/spec/SKILL.md +20 -0
  11. package/assets/skills/spec-bug/SKILL.md +16 -2
  12. package/assets/skills/spec-cancel/SKILL.md +9 -0
  13. package/assets/skills/spec-complete/SKILL.md +12 -1
  14. package/assets/skills/spec-go/SKILL.md +10 -8
  15. package/assets/skills/spec-hotfix/SKILL.md +17 -3
  16. package/assets/skills/spec-linear-setup/SKILL.md +38 -2
  17. package/assets/skills/spec-status/SKILL.md +1 -0
  18. package/assets/skills/spec-sync/SKILL.md +1 -0
  19. package/assets/skills/spec-to-main/SKILL.md +2 -1
  20. package/bin/skitterspec-linear.js +10 -0
  21. package/package.json +1 -1
  22. package/src/cli.js +176 -39
  23. package/src/env/config.js +19 -0
  24. package/src/env/teardown.js +62 -4
  25. package/src/init.js +120 -2
  26. package/src/vendor/linear/cli-sync.js +402 -33
  27. package/src/vendor/linear/config.js +91 -1
  28. package/src/vendor/linear/doctor.js +67 -1
  29. package/src/vendor/linear/released.js +85 -1
  30. package/src/vendor/sync-core/index.js +4 -1
  31. package/src/vendor/sync-core/src/compare.js +59 -3
  32. package/src/vendor/sync-core/src/normalize.js +65 -2
  33. package/assets/skills/spec-connect/SKILL.md +0 -59
  34. package/assets/skills/spec-live/SKILL.md +0 -73
package/src/init.js CHANGED
@@ -23,6 +23,21 @@ function listSkills() {
23
23
  .sort()
24
24
  }
25
25
 
26
+ // Slash commands shipped as `assets/commands/*.md`, installed to
27
+ // `.claude/commands/`. Discovered from the bundled tree exactly like skills, so
28
+ // each distribution installs precisely what it ships.
29
+ function listCommands() {
30
+ const dir = path.join(ASSETS, 'commands')
31
+ try {
32
+ return fs
33
+ .readdirSync(dir)
34
+ .filter((f) => f.endsWith('.md'))
35
+ .sort()
36
+ } catch {
37
+ return [] // a distribution may ship no commands
38
+ }
39
+ }
40
+
26
41
  function listRules() {
27
42
  return fs
28
43
  .readdirSync(path.join(ASSETS, 'rules'))
@@ -42,6 +57,8 @@ function listCoreTemplates() {
42
57
 
43
58
  const SKILLS = listSkills()
44
59
 
60
+ const COMMANDS = listCommands()
61
+
45
62
  const RULES = listRules()
46
63
 
47
64
  const SPEC_FOLDERS = ['.core', 'backlog', 'in-progress', 'complete', 'cancelled']
@@ -50,6 +67,89 @@ const SPEC_FOLDERS = ['.core', 'backlog', 'in-progress', 'complete', 'cancelled'
50
67
  // env.config isolation templates; a provider superset also ships its own).
51
68
  const CORE_FILES = listCoreTemplates()
52
69
 
70
+ // The CLI is a local devDependency and never on PATH, so a command file that
71
+ // pre-executes it must carry a literal, working invocation. Detect the runner
72
+ // from the lockfile and bake it in at write time.
73
+ //
74
+ // The lockfile is a POSITIVE signal — a file that must be present for the answer
75
+ // to be yes — rather than an absence. When none is found we do not guess a
76
+ // package manager we have no evidence for; `npx` is the fallback because it is
77
+ // the one runner that works across all three installs.
78
+ const PACKAGE_MANAGERS = [
79
+ ['pnpm-lock.yaml', 'pnpm exec'],
80
+ ['yarn.lock', 'yarn'],
81
+ ['package-lock.json', 'npx'],
82
+ ['bun.lockb', 'bunx'],
83
+ ]
84
+
85
+ function detectPackageManager(dir) {
86
+ for (const [lockfile, exec] of PACKAGE_MANAGERS) {
87
+ if (fs.existsSync(path.join(dir, lockfile))) return exec
88
+ }
89
+ return 'npx'
90
+ }
91
+
92
+ // Fill a command file's `{{exec}}` placeholders. Kept a pure function of
93
+ // (content, dir) so `managedTargets` can compare against exactly what
94
+ // `installCommands` would write — otherwise every install would hash as
95
+ // customized on the next run.
96
+ function renderCommand(content, dir) {
97
+ return content.split('{{exec}}').join(detectPackageManager(dir))
98
+ }
99
+
100
+ // --- composed-assets guard -------------------------------------------------
101
+ //
102
+ // A source package's `assets/` is PRE-composition: `<!-- seam:NAME -->` is still
103
+ // literal text that scripts/build-dist.js replaces — with a provider's fragment
104
+ // for the provider distribution, with nothing for the base. Only a built
105
+ // distribution's assets are installable.
106
+ //
107
+ // This is a POSITIVE signal, not an absence: a seam marker must be **present**
108
+ // for the guard to fire, so it cannot misfire on a tree it simply failed to read.
109
+ // And it refuses rather than repairing — installing the wrong thing is the
110
+ // expensive mistake here. In a dev-linked checkout the installed skills are
111
+ // symlinks to the composed distribution, so a `--force` install would write
112
+ // these markers straight through the link and into the built assets.
113
+ const SEAM_RE = /<!--\s*seam:[A-Za-z0-9_-]+\s*-->/
114
+
115
+ function assetFiles(dir) {
116
+ const out = []
117
+ const walk = (d) => {
118
+ let entries
119
+ try {
120
+ entries = fs.readdirSync(d, { withFileTypes: true })
121
+ } catch {
122
+ return // a distribution need not ship every asset kind
123
+ }
124
+ for (const e of entries) {
125
+ const p = path.join(d, e.name)
126
+ if (e.isDirectory()) walk(p)
127
+ else if (e.name.endsWith('.md')) out.push(p)
128
+ }
129
+ }
130
+ walk(dir)
131
+ return out
132
+ }
133
+
134
+ // Throws when this package's assets are uncomposed. Called from the **bins** —
135
+ // the outermost boundary, and the only way a real install happens — never from
136
+ // init()/resync()/run(), which the unit tests legitimately drive against this
137
+ // source tree. Guarding any deeper would make the library untestable while
138
+ // protecting nothing extra.
139
+ function assertComposedAssets() {
140
+ const offenders = assetFiles(ASSETS)
141
+ .filter((f) => SEAM_RE.test(fs.readFileSync(f, 'utf8')))
142
+ .map((f) => path.relative(ASSETS, f))
143
+ if (!offenders.length) return
144
+ throw new Error(
145
+ `refusing to install: this package's assets are uncomposed (${offenders.length} ` +
146
+ `file(s) still carry a <!-- seam:… --> marker, e.g. ${offenders[0]}).\n` +
147
+ ' These are a workspace source package\'s assets, not a distribution\'s. ' +
148
+ 'Run "npm run build" in the skitterspec repo, then run the command again ' +
149
+ 'from the built distribution.',
150
+ )
151
+ }
152
+
53
153
  const SPEC_MARKER_START = '<!-- skitterspec:start -->'
54
154
  const SPEC_MARKER_END = '<!-- skitterspec:end -->'
55
155
 
@@ -99,13 +199,15 @@ function sha1(content) {
99
199
  // the bundled content that ships in this distribution's assets.
100
200
  function managedTargets(dir) {
101
201
  const out = []
102
- const add = (assetRel, targetAbs) =>
202
+ const add = (assetRel, targetAbs, render = (c) => c) =>
103
203
  out.push({
104
204
  relPath: rel(dir, targetAbs),
105
205
  abs: targetAbs,
106
- bundled: fs.readFileSync(path.join(ASSETS, assetRel), 'utf8'),
206
+ bundled: render(fs.readFileSync(path.join(ASSETS, assetRel), 'utf8'), dir),
107
207
  })
108
208
  for (const name of SKILLS) add(path.join('skills', name, 'SKILL.md'), path.join(dir, '.claude', 'skills', name, 'SKILL.md'))
209
+ for (const name of COMMANDS)
210
+ add(path.join('commands', name), path.join(dir, '.claude', 'commands', name), renderCommand)
109
211
  for (const name of RULES) add(path.join('rules', name), path.join(dir, '.claude', 'rules', name))
110
212
  for (const asset of CORE_FILES) add(asset, path.join(dir, 'specs', '.core', path.basename(asset)))
111
213
  return out
@@ -230,6 +332,16 @@ function installSkills(dir, opts) {
230
332
  }
231
333
  }
232
334
 
335
+ function installCommands(dir, opts) {
336
+ for (const name of COMMANDS) {
337
+ const content = renderCommand(
338
+ fs.readFileSync(path.join(ASSETS, 'commands', name), 'utf8'),
339
+ dir,
340
+ )
341
+ writeFile(dir, path.join(dir, '.claude', 'commands', name), content, opts)
342
+ }
343
+ }
344
+
233
345
  function installRule(dir, opts) {
234
346
  for (const name of RULES) {
235
347
  copyAsset(
@@ -519,6 +631,7 @@ function reset(dir, { claudeMd = true } = {}) {
519
631
  }
520
632
  if (claudeMd) stripClaudeMdSection(dir)
521
633
  installSkills(dir, { force: true })
634
+ installCommands(dir, { force: true })
522
635
  installRule(dir, { force: true })
523
636
  installFolders(dir)
524
637
  removeRetiredFiles(dir)
@@ -597,6 +710,7 @@ async function init({ dir, force, claudeMd, mode, isolation }) {
597
710
  resetReport()
598
711
 
599
712
  installSkills(dir, { force })
713
+ installCommands(dir, { force })
600
714
  installRule(dir, { force })
601
715
  installFolders(dir)
602
716
  removeRetiredFiles(dir)
@@ -615,6 +729,7 @@ async function init({ dir, force, claudeMd, mode, isolation }) {
615
729
  module.exports = {
616
730
  init,
617
731
  SKILLS,
732
+ COMMANDS,
618
733
  RULES,
619
734
  SPEC_FOLDERS,
620
735
  MANIFEST_FILE,
@@ -627,4 +742,7 @@ module.exports = {
627
742
  resync,
628
743
  reset,
629
744
  assertSafeToDelete,
745
+ assertComposedAssets,
746
+ detectPackageManager,
747
+ renderCommand,
630
748
  }