@skitterbyte/skitterspec-linear 10.2.0 → 10.3.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.
@@ -4,8 +4,13 @@
4
4
  /**
5
5
  * The Linear-provider distribution's bin — a superset of the base CLI.
6
6
  *
7
- * `spec-sync …` is handled here (the provider engine seam); every other command
8
- * (`init`, `update`, `spec-env`, `--help`, …) delegates to the base CLI unchanged.
7
+ * The provider's own commands are routed from ONE table (`src/commands.js`),
8
+ * which also generates their `--help` section; every other command (`init`,
9
+ * `update`, `spec-env`, …) delegates to the base CLI unchanged.
10
+ *
11
+ * `--help` is the exception that has to be handled here rather than delegated:
12
+ * the base prints its own HELP const, which cannot know what a provider adds, so
13
+ * delegating made this distribution report that `spec-sync` did not exist.
9
14
  */
10
15
 
11
16
  // This package's bin/, src/ and assets/ are COMPOSED by scripts/build-dist.js and
@@ -27,24 +32,33 @@ if (!existsSync(join(__dirname, '..', 'src'))) {
27
32
  process.exit(1)
28
33
  }
29
34
 
30
- const { run } = require('../src/cli.js')
31
- const { specSync } = require('../src/vendor/linear/cli-sync.js')
32
- const { specSanitise } = require('../src/vendor/linear/cli-sanitise.js')
35
+ const { run, HELP } = require('../src/cli.js')
36
+ const {
37
+ PROVIDER_COMMANDS,
38
+ providerHelpSection,
39
+ } = require('../src/vendor/linear/commands.js')
33
40
 
34
41
  async function main(argv) {
35
42
  const [cmd, ...rest] = argv
36
- if (cmd === 'spec-sync') {
37
- // Propagate the exit code, like spec-sanitise below. Dropping it made
38
- // `status --workspace-states` (a bad state name) and `stamp` (a refused
39
- // write) both look successful to any caller checking $?, which is exactly
40
- // what the /spec-push skill does before it applies a plan.
41
- process.exitCode = await specSync(rest)
43
+
44
+ // Base help + what this distribution adds. Matched on the COMMAND SLOT only,
45
+ // never the whole argv: `spec-sanitise --help` must reach that command's own
46
+ // help, not be swallowed by the top-level one.
47
+ if (!cmd || cmd === '--help' || cmd === '-h') {
48
+ process.stdout.write(`${HELP}\n${providerHelpSection()}`)
42
49
  return
43
50
  }
44
- if (cmd === 'spec-sanitise') {
45
- process.exitCode = await specSanitise(rest)
51
+
52
+ const provider = PROVIDER_COMMANDS[cmd]
53
+ if (provider) {
54
+ // Propagate the exit code. Dropping it made `spec-sync status
55
+ // --workspace-states` (a bad state name) and `stamp` (a refused write) both
56
+ // look successful to any caller checking $?, which is exactly what the
57
+ // /spec-push skill does before it applies a plan.
58
+ process.exitCode = await provider.run(rest)
46
59
  return
47
60
  }
61
+
48
62
  await run(argv)
49
63
  }
50
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skitterbyte/skitterspec-linear",
3
- "version": "10.2.0",
3
+ "version": "10.3.0",
4
4
  "description": "Spec-driven development for Claude Code, with one-way Linear sync — a superset of @skitterbyte/skitterspec: the base filesystem workflow plus /spec-status · /spec-push and the spec-sync CLI. The repo is canonical; Linear is a generated mirror. Install this OR the base, not both.",
5
5
  "keywords": [
6
6
  "claude",
package/src/cli.js CHANGED
@@ -48,6 +48,29 @@ const { renderRoutes, portsInUse, waitListening } = require('./env/proxy.js')
48
48
 
49
49
  const pkg = require('../package.json')
50
50
 
51
+ // Commands this (tracker-free) base does NOT ship, and the distribution that
52
+ // does. Without this the base says only "unknown command: spec-sync", which a
53
+ // user correctly reads as "no such feature" — nothing anywhere named the
54
+ // distribution that has it, so they were stranded. Naming Linear here is a
55
+ // diagnostic string, not provider machinery: `init.js` already knows
56
+ // `linear.config.json` and `linear-base/` by name in order to protect them.
57
+ const PROVIDER_COMMANDS = {
58
+ 'spec-sync': '@skitterbyte/skitterspec-linear',
59
+ 'spec-sanitise': '@skitterbyte/skitterspec-linear',
60
+ }
61
+
62
+ function unknownCommand(cmd) {
63
+ const dist = PROVIDER_COMMANDS[cmd]
64
+ if (dist) {
65
+ return (
66
+ `unknown command: ${cmd} — this is the base distribution, which does not ` +
67
+ `ship it.\n ${cmd} comes from ${dist} (a superset of this package): ` +
68
+ `install that instead.`
69
+ )
70
+ }
71
+ return `unknown command: ${cmd} (try --help)`
72
+ }
73
+
51
74
  const HELP = `skitterspec — spec-driven-development for Claude Code
52
75
 
53
76
  Usage:
@@ -1384,8 +1407,8 @@ async function run(argv) {
1384
1407
  await cleanupReleaseTooling(dir, opts)
1385
1408
  break
1386
1409
  default:
1387
- throw new Error(`unknown command: ${cmd} (try --help)`)
1410
+ throw new Error(unknownCommand(cmd))
1388
1411
  }
1389
1412
  }
1390
1413
 
1391
- module.exports = { run, parse }
1414
+ module.exports = { run, parse, HELP, unknownCommand }
@@ -158,8 +158,15 @@ function warnToErr(snapshotDir, config, err) {
158
158
  if (lines.length) err.write(lines.join('\n') + '\n')
159
159
  }
160
160
 
161
+ // Resolve a spec argument to its folder, or null with a message on stdout.
162
+ // EVERY failure path here must print and EVERY caller must return a non-zero
163
+ // code: /spec-push checks $? before it applies a plan, so a resolve failure that
164
+ // exits 0 reads as "nothing to do" rather than "I could not find the spec".
161
165
  function resolveOrExit(specArg, dir, out) {
162
- if (!specArg) return null
166
+ if (!specArg) {
167
+ out.write('spec-sync: no spec given\n')
168
+ return null
169
+ }
163
170
  const snapshotDir = resolveSnapshotDir(specArg, dir)
164
171
  if (!snapshotDir) {
165
172
  out.write(`spec-sync: spec not found: ${specArg}\n`)
@@ -171,7 +178,7 @@ function resolveOrExit(specArg, dir, out) {
171
178
  // `spec-sync normalize <spec>` — print the local projection as JSON.
172
179
  function specSyncNormalize(dir, config, specArg, out, err) {
173
180
  const snapshotDir = resolveOrExit(specArg, dir, out)
174
- if (!snapshotDir) return
181
+ if (!snapshotDir) return 1
175
182
  // stdout is the projection and nothing else — callers pipe it into jq.
176
183
  warnToErr(snapshotDir, config, err)
177
184
  out.write(JSON.stringify(projectionOf(snapshotDir, config), null, 2) + '\n')
@@ -404,7 +411,7 @@ function specSyncStamp(dir, config, specArg, flags, out) {
404
411
  // files. The skill calls this AFTER applying the plan and stamping new ids.
405
412
  function specSyncRecord(dir, config, specArg, out) {
406
413
  const snapshotDir = resolveOrExit(specArg, dir, out)
407
- if (!snapshotDir) return
414
+ if (!snapshotDir) return 1
408
415
  const identifier = specIdentifier(snapshotDir, config)
409
416
  const file = recordPush({ dir, snapshotDir, identifier, config })
410
417
  out.write(`spec-sync record: snapshot written → ${path.relative(dir, file)}\n`)
@@ -417,7 +424,7 @@ function specSyncRecord(dir, config, specArg, out) {
417
424
  // configured state names and fails loudly on a typo Linear would silently no-op.
418
425
  function specSyncStatus(dir, config, specArg, flags, out) {
419
426
  const snapshotDir = resolveOrExit(specArg, dir, out)
420
- if (!snapshotDir) return
427
+ if (!snapshotDir) return 1
421
428
  const identifier = specIdentifier(snapshotDir, config)
422
429
  const lines = [`spec-sync status: ${identifier}`, ...warningLines(snapshotDir, config)]
423
430
 
@@ -1227,15 +1234,13 @@ async function specSync(rest, io = {}) {
1227
1234
 
1228
1235
  switch (sub) {
1229
1236
  case 'normalize':
1230
- specSyncNormalize(dir, config, positional[0], out, err)
1231
- return 0
1237
+ return specSyncNormalize(dir, config, positional[0], out, err) || 0
1232
1238
  case 'push':
1233
1239
  return specSyncPush(dir, config, positional[0], flags, out, err) || 0
1234
1240
  case 'stamp':
1235
1241
  return specSyncStamp(dir, config, positional[0], flags, out)
1236
1242
  case 'record':
1237
- specSyncRecord(dir, config, positional[0], out)
1238
- return 0
1243
+ return specSyncRecord(dir, config, positional[0], out) || 0
1239
1244
  case 'status':
1240
1245
  return specSyncStatus(dir, config, positional[0], flags, out) || 0
1241
1246
  case 'projects':
@@ -0,0 +1,54 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * The commands this provider distribution adds on top of the base CLI.
5
+ *
6
+ * ONE table drives both routing and `--help`. That pairing is the point: the bug
7
+ * this fixes was `spec-sync` being routed by the bin while the base's `HELP`
8
+ * const knew nothing about it, so the one distribution that ships the command
9
+ * told users it did not exist. Anything added here is routed and documented in
10
+ * the same edit — the two cannot drift, and a test asserts it.
11
+ *
12
+ * `run(rest)` returns an exit code (the bin propagates it); `summary` is the
13
+ * one-line description `--help` prints.
14
+ */
15
+
16
+ const { specSync } = require('./cli-sync.js')
17
+ const { specSanitise } = require('./cli-sanitise.js')
18
+
19
+ const DIST = '@skitterbyte/skitterspec-linear'
20
+
21
+ const PROVIDER_COMMANDS = {
22
+ 'spec-sync': {
23
+ run: specSync,
24
+ usage: 'skitterspec spec-sync <cmd>',
25
+ summary:
26
+ 'One-way sync to Linear (repo -> tracker; opt-in, needs\n' +
27
+ 'specs/.core/linear.config.json). Run it with no args to\n' +
28
+ 'list its subcommands.',
29
+ },
30
+ 'spec-sanitise': {
31
+ run: specSanitise,
32
+ usage: 'skitterspec spec-sanitise',
33
+ summary:
34
+ 'Rewrite spec markdown so no emphasis or link straddles a\n' +
35
+ 'line break. Dry-run; --write to apply.',
36
+ },
37
+ }
38
+
39
+ // The `--help` section for these commands, in the base HELP's column layout:
40
+ // two-space indent, description starting at column 30, continuations aligned.
41
+ const COL = 30
42
+
43
+ function providerHelpSection() {
44
+ const lines = [`Provider commands (${DIST}):`]
45
+ for (const name of Object.keys(PROVIDER_COMMANDS)) {
46
+ const { usage, summary } = PROVIDER_COMMANDS[name]
47
+ const [first, ...rest] = summary.split('\n')
48
+ lines.push(` ${usage.padEnd(COL - 2)}${first}`)
49
+ for (const line of rest) lines.push(`${' '.repeat(COL)}${line}`)
50
+ }
51
+ return lines.join('\n') + '\n'
52
+ }
53
+
54
+ module.exports = { PROVIDER_COMMANDS, providerHelpSection, DIST }