cyber-sdd 0.7.0 → 0.7.2
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/.claude-plugin/plugin.json +1 -2
- package/.codex-plugin/plugin.json +1 -2
- package/CHANGELOG.md +320 -0
- package/agents/sdd-scanner.md +1 -1
- package/package.json +11 -12
- package/plugin.json +55 -0
- package/skills/align-spec/SKILL.md +1 -1
- package/skills/align-spec/scripts/align-spec.mjs +6755 -12
- package/skills/blast-estimate/README.md +3 -3
- package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
- package/skills/check-plan-safety/SKILL.md +2 -3
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
- package/skills/check-project-specs/SKILL.md +1 -1
- package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
- package/skills/check-retired-terms/SKILL.md +2 -3
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
- package/skills/check-scenario-overlap/SKILL.md +1 -2
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
- package/skills/check-spec-references/SKILL.md +2 -2
- package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
- package/skills/check-spec-structure/SKILL.md +1 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
- package/skills/collision-ladder/README.md +2 -2
- package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/SKILL.md +1 -1
- package/skills/concept-index/scripts/concept-index.mjs +183 -12
- package/skills/discover-plans/README.md +3 -3
- package/skills/discover-plans/SKILL.md +1 -1
- package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
- package/skills/discover-specs/README.md +3 -3
- package/skills/discover-specs/SKILL.md +1 -1
- package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
- package/skills/doctrine-loop/SKILL.md +1 -1
- package/skills/gate-validation-governance/SKILL.md +1 -1
- package/skills/init/README.md +2 -2
- package/skills/init/scripts/wire-statusline.mjs +168 -12
- package/skills/manage-ignore/README.md +3 -3
- package/skills/manage-ignore/SKILL.md +1 -2
- package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
- package/skills/manage-scenario-bridge/README.md +3 -3
- package/skills/manage-scenario-bridge/SKILL.md +1 -1
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
- package/skills/manage-spec-anchors/README.md +3 -3
- package/skills/manage-spec-anchors/SKILL.md +1 -1
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
- package/skills/mission-graph/README.md +2 -2
- package/skills/mission-graph/SKILL.md +1 -1
- package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
- package/skills/pause-mission/SKILL.md +1 -1
- package/skills/place-node/SKILL.md +2 -2
- package/skills/place-node/scripts/place-node.mjs +117 -12
- package/skills/plan-retirement/README.md +2 -2
- package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
- package/skills/resolve-governances/README.md +3 -3
- package/skills/resolve-governances/SKILL.md +1 -1
- package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
- package/skills/resolve-tracking/SKILL.md +1 -1
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
- package/skills/scaffold-project-spec/SKILL.md +1 -1
- package/skills/spec-gate/README.md +12 -9
- package/skills/spec-gate/SKILL.md +2 -2
- package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
- package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
- package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
- package/skills/spec-producer-governance/SKILL.md +2 -2
- package/skills/start-mission/SKILL.md +1 -1
- package/skills/suite-format-governance/README.md +1 -1
- package/skills/suite-format-governance/SKILL.md +1 -1
- package/skills/touch-set-correction/README.md +5 -5
- package/skills/touch-set-correction/SKILL.md +1 -1
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
- package/skills/verify-scenarios/README.md +6 -6
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
- package/.plugin/pins.json +0 -3
- package/.plugin/plugin.json +0 -19
- package/dist/align-spec/scripts/align-spec.js +0 -102
- package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
- package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
- package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
- package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
- package/dist/concept-index/scripts/concept-index.js +0 -181
- package/dist/discover-specs/scripts/discover-specs.js +0 -318
- package/dist/spec-gate/scripts/check-spec-state.js +0 -420
- package/dist/spec-gate/scripts/check-suite.js +0 -317
- package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
- package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
- package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
- package/skills/align-spec/scripts/align-spec.mts +0 -188
- package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
- package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
- package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
- package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
- package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
- package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
- package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
- package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
- package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
- package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
- package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
- package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
- package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
- package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
- package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
- package/skills/concept-index/scripts/concept-index.mts +0 -246
- package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
- package/skills/discover-plans/scripts/discover-plans.mts +0 -215
- package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
- package/skills/discover-specs/scripts/discover-specs.mts +0 -420
- package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
- package/skills/init/scripts/wire-statusline.mts +0 -279
- package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
- package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
- package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
- package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
- package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
- package/skills/place-node/scripts/place-node.mts +0 -158
- package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
- package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
- package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
- package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
- package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
- package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
- package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
- package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
- package/skills/spec-gate/scripts/check-suite.mts +0 -538
- package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
- package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
- package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
- package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
- package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
- package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// wire-statusline — gateway/init's concrete wiring engine for the mission statusline. It reads/writes
|
|
3
|
-
// ONLY project `.claude/settings.json` (never the global `~/.claude/settings.json`) and, only when the
|
|
4
|
-
// root is a git repo, `.gitignore`. It never touches spec/contract state (status, approval, spec.md).
|
|
5
|
-
// It also READS the global `~/.claude/settings.json` (or an override path) to detect and, on a fresh
|
|
6
|
-
// wire, compose its statusLine command as the wrapped base — read-only, never written.
|
|
7
|
-
//
|
|
8
|
-
// Operation:
|
|
9
|
-
// --wire --mode own-line|same-line compose the SDD statusLine segment into project settings and
|
|
10
|
-
// (in a git repo) add the status file to .gitignore; idempotent
|
|
11
|
-
// [--global-settings <file>] [--no-global-base]
|
|
12
|
-
// --detect [--global-settings <file>] read-only: report project/global statusLine state and shadow risk
|
|
13
|
-
//
|
|
14
|
-
// The wired command reads the single-line status file `.agents/sdd/statusline` the conductor writes
|
|
15
|
-
// (`../../mission/conductor/`) and renders it — own-line (a new row) or same-line (an appended
|
|
16
|
-
// segment) — falling through to nothing (or the composed base, unchanged) when the file is absent.
|
|
17
|
-
// Compose, never stomp: an existing `statusLine` command is preserved as the wrapped "base"; the SDD
|
|
18
|
-
// segment is added around it. Re-wiring (same or a different mode) rewrites the one managed block —
|
|
19
|
-
// it never stacks a second SDD segment.
|
|
20
|
-
//
|
|
21
|
-
// Global shadow (issue #164): Claude Code's project statusLine REPLACES (never merges with) the
|
|
22
|
-
// global one. When the project has no statusLine of its own, a fresh wire composes the global
|
|
23
|
-
// command as the base by default so the global line keeps rendering instead of going blank. A
|
|
24
|
-
// re-run of an already-wired (or foreign) project command never re-consults the global — only a
|
|
25
|
-
// fresh wire ever adopts it.
|
|
26
|
-
// Pure functions are exported for node:test; running the file directly drives the CLI. No deps.
|
|
27
|
-
|
|
28
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
29
|
-
import { homedir } from 'node:os'
|
|
30
|
-
import { dirname, join } from 'node:path'
|
|
31
|
-
import { pathToFileURL } from 'node:url'
|
|
32
|
-
|
|
33
|
-
export const STATUS_FILE = '.agents/sdd/statusline'
|
|
34
|
-
export const SETTINGS_FILE = '.claude/settings.json'
|
|
35
|
-
export const GITIGNORE_FILE = '.gitignore'
|
|
36
|
-
export const GLOBAL_SETTINGS_FILE = join(homedir(), '.claude', 'settings.json')
|
|
37
|
-
|
|
38
|
-
export type StatusLineMode = 'own-line' | 'same-line'
|
|
39
|
-
|
|
40
|
-
const MARKER_BEGIN = '# sdd-statusline:begin'
|
|
41
|
-
const MARKER_END = '# sdd-statusline:end'
|
|
42
|
-
const MODE_PREFIX = '# sdd-statusline:mode:'
|
|
43
|
-
const ORIG_OPEN = '__sdd_orig() {'
|
|
44
|
-
const ORIG_CLOSE = '}'
|
|
45
|
-
const NO_BASE = ':'
|
|
46
|
-
|
|
47
|
-
// ── the managed command block ──
|
|
48
|
-
|
|
49
|
-
// Build the self-contained POSIX sh command that reads the status file and renders it in `mode`,
|
|
50
|
-
// wrapping `base` (the pre-existing statusLine command, if any) so its output is preserved.
|
|
51
|
-
export function buildCommand(base: string | undefined, mode: StatusLineMode): string {
|
|
52
|
-
const origBody = base && base.trim() !== '' ? base : NO_BASE
|
|
53
|
-
const render =
|
|
54
|
-
mode === 'own-line'
|
|
55
|
-
? `if [ -s "$__sdd_file" ]; then\n __sdd_val="$(cat "$__sdd_file")"\n printf '%s\\n%s\\n' "$__sdd_base" "$__sdd_val"\nelse\n printf '%s\\n' "$__sdd_base"\nfi`
|
|
56
|
-
: `if [ -s "$__sdd_file" ]; then\n __sdd_val="$(cat "$__sdd_file")"\n printf '%s | %s\\n' "$__sdd_base" "$__sdd_val"\nelse\n printf '%s\\n' "$__sdd_base"\nfi`
|
|
57
|
-
return [
|
|
58
|
-
MARKER_BEGIN,
|
|
59
|
-
`${MODE_PREFIX}${mode}`,
|
|
60
|
-
ORIG_OPEN,
|
|
61
|
-
origBody,
|
|
62
|
-
ORIG_CLOSE,
|
|
63
|
-
'__sdd_base="$(__sdd_orig 2>/dev/null)"',
|
|
64
|
-
`__sdd_file="${STATUS_FILE}"`,
|
|
65
|
-
render,
|
|
66
|
-
MARKER_END,
|
|
67
|
-
].join('\n')
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface ParsedCommand {
|
|
71
|
-
wired: boolean
|
|
72
|
-
base?: string
|
|
73
|
-
mode?: StatusLineMode
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Recognize a previously wired command and recover its wrapped base + mode, so re-wiring rebuilds the
|
|
77
|
-
// same managed block instead of stacking a second one. A command with no marker is not wired — the
|
|
78
|
-
// whole string becomes the base to wrap.
|
|
79
|
-
export function parseCommand(command: string | undefined): ParsedCommand {
|
|
80
|
-
if (!command?.includes(MARKER_BEGIN)) return { wired: false }
|
|
81
|
-
const modeMatch = new RegExp(`${MODE_PREFIX}(own-line|same-line)`).exec(command)
|
|
82
|
-
const origMatch = /__sdd_orig\(\) \{\n([\s\S]*?)\n\}/.exec(command)
|
|
83
|
-
const mode = (modeMatch?.[1] as StatusLineMode | undefined) ?? 'own-line'
|
|
84
|
-
const origBody = origMatch?.[1] ?? NO_BASE
|
|
85
|
-
return { wired: true, base: origBody === NO_BASE ? undefined : origBody, mode }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// The command to wrap as `base` when composing: an already-wired command hands back its recovered
|
|
89
|
-
// original base (never nests); any other command is the base verbatim.
|
|
90
|
-
function baseToWrap(existingCommand: string | undefined): string | undefined {
|
|
91
|
-
const parsed = parseCommand(existingCommand)
|
|
92
|
-
return parsed.wired ? parsed.base : existingCommand
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ── project settings.json ──
|
|
96
|
-
|
|
97
|
-
interface StatusLineConfig {
|
|
98
|
-
type: string
|
|
99
|
-
command: string
|
|
100
|
-
[key: string]: unknown
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
interface Settings {
|
|
104
|
-
statusLine?: StatusLineConfig
|
|
105
|
-
[key: string]: unknown
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function readSettingsFile(file: string): Settings {
|
|
109
|
-
if (!existsSync(file)) return {}
|
|
110
|
-
try {
|
|
111
|
-
return JSON.parse(readFileSync(file, 'utf8')) as Settings
|
|
112
|
-
} catch {
|
|
113
|
-
return {}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function readSettings(root: string): Settings {
|
|
118
|
-
return readSettingsFile(join(root, SETTINGS_FILE))
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Read a settings JSON file's statusLine.command, read-only. Absent file, unparseable JSON, or a
|
|
122
|
-
// missing/non-string/blank command all collapse to undefined — a malformed global settings file is
|
|
123
|
-
// treated the same as no global statusLine (frozen scenario).
|
|
124
|
-
export function readStatusLineCommand(file: string): string | undefined {
|
|
125
|
-
const settings = readSettingsFile(file)
|
|
126
|
-
const command = settings.statusLine?.command
|
|
127
|
-
if (typeof command !== 'string' || command.trim() === '') return undefined
|
|
128
|
-
return command
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function writeSettings(root: string, settings: Settings): void {
|
|
132
|
-
const file = join(root, SETTINGS_FILE)
|
|
133
|
-
mkdirSync(dirname(file), { recursive: true })
|
|
134
|
-
writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export interface ComposeResult {
|
|
138
|
-
changed: boolean
|
|
139
|
-
command: string
|
|
140
|
-
globalBaseComposed: boolean
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Compose the SDD statusLine segment into project settings.json. Preserves any existing statusLine
|
|
144
|
-
// command's output as the wrapped base (never overwrites it); creates the key when absent. Rewiring
|
|
145
|
-
// the same mode over an already-wired command is a no-op (byte-identical command).
|
|
146
|
-
//
|
|
147
|
-
// Base selection: when the project settings define no usable `statusLine` command (the same
|
|
148
|
-
// predicate detectStatusLines reports as `absent`), this is a fresh wire — the base is
|
|
149
|
-
// `globalCommand` (possibly undefined). When a usable project `statusLine` already exists (foreign
|
|
150
|
-
// or already wired), today's behavior is unchanged — the existing command's recovered base is used
|
|
151
|
-
// and `globalCommand` is ignored entirely, so a re-run never re-consults or adopts the global.
|
|
152
|
-
export function composeStatusLine(root: string, mode: StatusLineMode, globalCommand?: string): ComposeResult {
|
|
153
|
-
const settings = readSettings(root)
|
|
154
|
-
const existing = settings.statusLine?.command
|
|
155
|
-
const hasProjectStatusLine = typeof existing === 'string' && existing.trim() !== ''
|
|
156
|
-
const globalBaseComposed = !hasProjectStatusLine && globalCommand !== undefined
|
|
157
|
-
const base = hasProjectStatusLine ? baseToWrap(existing) : globalCommand
|
|
158
|
-
const command = buildCommand(base, mode)
|
|
159
|
-
if (existing === command) return { changed: false, command, globalBaseComposed: false }
|
|
160
|
-
settings.statusLine = { ...settings.statusLine, type: 'command', command }
|
|
161
|
-
writeSettings(root, settings)
|
|
162
|
-
return { changed: true, command, globalBaseComposed }
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ── .gitignore ──
|
|
166
|
-
|
|
167
|
-
export function isGitRepo(root: string): boolean {
|
|
168
|
-
return existsSync(join(root, '.git'))
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function splitLines(text: string): string[] {
|
|
172
|
-
const lines = text.split('\n')
|
|
173
|
-
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
|
|
174
|
-
return lines
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Idempotently add the status file to .gitignore (creating it if absent). Only called when the root
|
|
178
|
-
// is a git repo; a no-op when the entry is already present.
|
|
179
|
-
export function addGitignoreEntry(root: string): { changed: boolean } {
|
|
180
|
-
const file = join(root, GITIGNORE_FILE)
|
|
181
|
-
const lines = existsSync(file) ? splitLines(readFileSync(file, 'utf8')) : []
|
|
182
|
-
if (lines.some((l) => l.trim() === STATUS_FILE)) return { changed: false }
|
|
183
|
-
const next = [...lines, STATUS_FILE]
|
|
184
|
-
writeFileSync(file, `${next.join('\n')}\n`)
|
|
185
|
-
return { changed: true }
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── global statusline detection ──
|
|
189
|
-
|
|
190
|
-
export interface DetectResult {
|
|
191
|
-
project: 'absent' | 'wired' | 'foreign'
|
|
192
|
-
global: 'absent' | 'present'
|
|
193
|
-
shadow: boolean
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Read-only: report the project statusLine state, the global statusLine state, and whether wiring
|
|
197
|
-
// would shadow a global statusLine that is currently rendering (project has none, global has one).
|
|
198
|
-
// Never writes anything — used to surface the shadow risk before wiring and by `--detect`.
|
|
199
|
-
export function detectStatusLines(root: string, globalSettingsFile?: string): DetectResult {
|
|
200
|
-
const projectCommand = readStatusLineCommand(join(root, SETTINGS_FILE))
|
|
201
|
-
const project = projectCommand === undefined ? 'absent' : parseCommand(projectCommand).wired ? 'wired' : 'foreign'
|
|
202
|
-
const global = readStatusLineCommand(globalSettingsFile ?? GLOBAL_SETTINGS_FILE) !== undefined ? 'present' : 'absent'
|
|
203
|
-
const shadow = project === 'absent' && global === 'present'
|
|
204
|
-
return { project, global, shadow }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// ── top-level wire ──
|
|
208
|
-
|
|
209
|
-
export interface WireResult {
|
|
210
|
-
settings: ComposeResult
|
|
211
|
-
gitignore: { changed: boolean; skipped: boolean }
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export interface WireOptions {
|
|
215
|
-
globalSettingsFile?: string
|
|
216
|
-
globalBase?: boolean
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Wire the reader into project settings and (only in a git repo) gitignore the status file. The
|
|
220
|
-
// single entry point init's SKILL.md calls on consent. `globalBase` (default true) controls whether
|
|
221
|
-
// a fresh wire composes the global statusLine command as the base; `globalSettingsFile` overrides
|
|
222
|
-
// the default global settings path (for tests, or a non-default HOME).
|
|
223
|
-
export function wireStatusline(root: string, mode: StatusLineMode, opts?: WireOptions): WireResult {
|
|
224
|
-
const globalBase = opts?.globalBase ?? true
|
|
225
|
-
const globalCommand = globalBase ? readStatusLineCommand(opts?.globalSettingsFile ?? GLOBAL_SETTINGS_FILE) : undefined
|
|
226
|
-
const settings = composeStatusLine(root, mode, globalCommand)
|
|
227
|
-
const gitignore = isGitRepo(root) ? { ...addGitignoreEntry(root), skipped: false } : { changed: false, skipped: true }
|
|
228
|
-
return { settings, gitignore }
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ── CLI ──
|
|
232
|
-
|
|
233
|
-
function flag(argv: string[], name: string): string | undefined {
|
|
234
|
-
const i = argv.indexOf(name)
|
|
235
|
-
return i === -1 ? undefined : argv[i + 1]
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function main(argv: string[]): number {
|
|
239
|
-
const root = flag(argv, '--root') ?? '.'
|
|
240
|
-
const globalSettingsFile = flag(argv, '--global-settings')
|
|
241
|
-
const w = (s: string) => process.stdout.write(`${s}\n`)
|
|
242
|
-
|
|
243
|
-
if (argv.includes('--detect')) {
|
|
244
|
-
const result = detectStatusLines(root, globalSettingsFile)
|
|
245
|
-
w(`project statusLine: ${result.project}`)
|
|
246
|
-
w(`global statusLine: ${result.global}`)
|
|
247
|
-
w(
|
|
248
|
-
result.shadow
|
|
249
|
-
? 'shadow risk: yes — wiring would shadow the global statusline; compose it as the base (default) or pass --no-global-base to shadow deliberately'
|
|
250
|
-
: 'shadow risk: no',
|
|
251
|
-
)
|
|
252
|
-
return 0
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (argv.includes('--wire')) {
|
|
256
|
-
const mode = flag(argv, '--mode')
|
|
257
|
-
if (mode !== 'own-line' && mode !== 'same-line') {
|
|
258
|
-
w('refused: --mode must be own-line or same-line')
|
|
259
|
-
return 1
|
|
260
|
-
}
|
|
261
|
-
const globalBase = !argv.includes('--no-global-base')
|
|
262
|
-
const result = wireStatusline(root, mode, { globalSettingsFile, globalBase })
|
|
263
|
-
w(result.settings.changed ? `wired statusLine (${mode})` : 'statusLine already wired (unchanged)')
|
|
264
|
-
if (result.settings.globalBaseComposed) w('composed the global statusLine command as the wrapped base')
|
|
265
|
-
if (result.gitignore.skipped) w('not a git repo — gitignore skipped')
|
|
266
|
-
else w(result.gitignore.changed ? `added ${STATUS_FILE} to .gitignore` : '.gitignore already up to date')
|
|
267
|
-
return 0
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
w(
|
|
271
|
-
'usage: wire-statusline --root <dir> --wire --mode own-line|same-line [--global-settings <file>] [--no-global-base]\n' +
|
|
272
|
-
' wire-statusline --root <dir> --detect [--global-settings <file>]',
|
|
273
|
-
)
|
|
274
|
-
return 0
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
278
|
-
process.exit(main(process.argv.slice(2)))
|
|
279
|
-
}
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
//#region skills/manage-ignore/scripts/manage-ignore.mts
|
|
6
|
-
const IGNORE_FILE = ".agents/sdd/.sddignore";
|
|
7
|
-
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
8
|
-
"node_modules",
|
|
9
|
-
".git",
|
|
10
|
-
"dist",
|
|
11
|
-
".turbo",
|
|
12
|
-
".next",
|
|
13
|
-
"coverage"
|
|
14
|
-
]);
|
|
15
|
-
function splitLines(text) {
|
|
16
|
-
const lines = text.split("\n");
|
|
17
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
18
|
-
return lines;
|
|
19
|
-
}
|
|
20
|
-
function readIgnoreLines(root) {
|
|
21
|
-
const file = join(root, IGNORE_FILE);
|
|
22
|
-
if (!existsSync(file)) return [];
|
|
23
|
-
try {
|
|
24
|
-
return splitLines(readFileSync(file, "utf8"));
|
|
25
|
-
} catch {
|
|
26
|
-
return [];
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function writeIgnoreLines(root, lines) {
|
|
30
|
-
const file = join(root, IGNORE_FILE);
|
|
31
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
32
|
-
writeFileSync(file, lines.length === 0 ? "" : `${lines.join("\n")}\n`);
|
|
33
|
-
}
|
|
34
|
-
function isRuleLine(line) {
|
|
35
|
-
const t = line.trim();
|
|
36
|
-
return t !== "" && !t.startsWith("#");
|
|
37
|
-
}
|
|
38
|
-
function listRules(root) {
|
|
39
|
-
return readIgnoreLines(root).filter(isRuleLine).map((l) => l.trim());
|
|
40
|
-
}
|
|
41
|
-
function hasUnclosedBracket(s) {
|
|
42
|
-
let i = 0;
|
|
43
|
-
while (i < s.length) {
|
|
44
|
-
if (s[i] === "\\") {
|
|
45
|
-
i += 2;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (s[i] === "[") {
|
|
49
|
-
const close = s.indexOf("]", i + 1);
|
|
50
|
-
if (close === -1) return true;
|
|
51
|
-
i = close + 1;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
i++;
|
|
55
|
-
}
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
function isValidPattern(pattern) {
|
|
59
|
-
let s = pattern;
|
|
60
|
-
if (s.startsWith("!")) s = s.slice(1);
|
|
61
|
-
s = s.replace(/^\s+|\s+$/g, "");
|
|
62
|
-
if (s === "" || s.startsWith("#")) return false;
|
|
63
|
-
if (hasUnclosedBracket(s)) return false;
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
function addRule(root, pattern) {
|
|
67
|
-
const p = pattern.trim();
|
|
68
|
-
if (!isValidPattern(p)) return {
|
|
69
|
-
ok: false,
|
|
70
|
-
reason: "malformed gitignore pattern"
|
|
71
|
-
};
|
|
72
|
-
const lines = readIgnoreLines(root);
|
|
73
|
-
if (lines.some((l) => l.trim() === p)) return {
|
|
74
|
-
ok: true,
|
|
75
|
-
changed: false,
|
|
76
|
-
rules: listRules(root),
|
|
77
|
-
message: "already present"
|
|
78
|
-
};
|
|
79
|
-
const next = [...lines, p];
|
|
80
|
-
writeIgnoreLines(root, next);
|
|
81
|
-
return {
|
|
82
|
-
ok: true,
|
|
83
|
-
changed: true,
|
|
84
|
-
rules: next.filter(isRuleLine).map((l) => l.trim()),
|
|
85
|
-
message: `added ${p}`
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
function removeRule(root, pattern) {
|
|
89
|
-
const p = pattern.trim();
|
|
90
|
-
const lines = readIgnoreLines(root);
|
|
91
|
-
const next = lines.filter((l) => l.trim() !== p);
|
|
92
|
-
if (next.length === lines.length) return {
|
|
93
|
-
ok: true,
|
|
94
|
-
changed: false,
|
|
95
|
-
rules: listRules(root),
|
|
96
|
-
message: "nothing was removed"
|
|
97
|
-
};
|
|
98
|
-
writeIgnoreLines(root, next);
|
|
99
|
-
return {
|
|
100
|
-
ok: true,
|
|
101
|
-
changed: true,
|
|
102
|
-
rules: next.filter(isRuleLine).map((l) => l.trim()),
|
|
103
|
-
message: `removed ${p}`
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
function inducePatterns(_root, samplePath) {
|
|
107
|
-
const normalized = samplePath.trim().replace(/\\/g, "/");
|
|
108
|
-
if (normalized.startsWith("/")) return {
|
|
109
|
-
ok: false,
|
|
110
|
-
reason: "path is not repo-relative (absolute)"
|
|
111
|
-
};
|
|
112
|
-
const rel = normalized.replace(/\/+$/g, "");
|
|
113
|
-
if (rel === "" || /(^|\/)\.\.(\/|$)/.test(rel)) return {
|
|
114
|
-
ok: false,
|
|
115
|
-
reason: "path is not repo-relative"
|
|
116
|
-
};
|
|
117
|
-
const generalized = `**/${rel.split("/").pop() ?? rel}`;
|
|
118
|
-
return {
|
|
119
|
-
ok: true,
|
|
120
|
-
candidates: generalized === rel ? [rel] : [rel, generalized]
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
function gitignoreToRegExp(pattern) {
|
|
124
|
-
let pat = pattern;
|
|
125
|
-
if (pat.endsWith("/")) pat = pat.slice(0, -1);
|
|
126
|
-
let anchored = pat.startsWith("/");
|
|
127
|
-
if (anchored) pat = pat.slice(1);
|
|
128
|
-
if (pat.includes("/")) anchored = true;
|
|
129
|
-
let body = "";
|
|
130
|
-
for (let i = 0; i < pat.length; i++) {
|
|
131
|
-
const c = pat[i];
|
|
132
|
-
if (c === "*") {
|
|
133
|
-
if (pat[i + 1] === "*") {
|
|
134
|
-
i++;
|
|
135
|
-
if (pat[i + 1] === "/") {
|
|
136
|
-
i++;
|
|
137
|
-
body += "(?:.*/)?";
|
|
138
|
-
} else body += ".*";
|
|
139
|
-
} else body += "[^/]*";
|
|
140
|
-
} else if (c === "?") body += "[^/]";
|
|
141
|
-
else if (c === "[") {
|
|
142
|
-
let j = i + 1;
|
|
143
|
-
let cls = "[";
|
|
144
|
-
if (pat[j] === "!") {
|
|
145
|
-
cls += "^";
|
|
146
|
-
j++;
|
|
147
|
-
}
|
|
148
|
-
while (j < pat.length && pat[j] !== "]") {
|
|
149
|
-
cls += /[\\^\]]/.test(pat[j]) ? `\\${pat[j]}` : pat[j];
|
|
150
|
-
j++;
|
|
151
|
-
}
|
|
152
|
-
cls += "]";
|
|
153
|
-
body += cls;
|
|
154
|
-
i = j;
|
|
155
|
-
} else body += c.replace(/[.+^${}()|\\]/g, "\\$&");
|
|
156
|
-
}
|
|
157
|
-
return new RegExp(`${anchored ? "^" : "^(?:.*/)?"}${body}(?:/.*)?$`);
|
|
158
|
-
}
|
|
159
|
-
function matchesPattern(pattern, path) {
|
|
160
|
-
return gitignoreToRegExp(pattern.startsWith("!") ? pattern.slice(1) : pattern).test(path);
|
|
161
|
-
}
|
|
162
|
-
function walkTree(root) {
|
|
163
|
-
const out = [];
|
|
164
|
-
const walk = (rel) => {
|
|
165
|
-
let entries;
|
|
166
|
-
try {
|
|
167
|
-
entries = readdirSync(join(root, rel), { withFileTypes: true });
|
|
168
|
-
} catch {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
for (const e of entries) {
|
|
172
|
-
if (SKIP_DIRS.has(e.name)) continue;
|
|
173
|
-
const child = rel ? `${rel}/${e.name}` : e.name;
|
|
174
|
-
out.push(child);
|
|
175
|
-
if (e.isDirectory()) walk(child);
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
walk("");
|
|
179
|
-
return out.sort();
|
|
180
|
-
}
|
|
181
|
-
function previewPattern(root, pattern) {
|
|
182
|
-
const p = pattern.trim();
|
|
183
|
-
if (!isValidPattern(p)) return {
|
|
184
|
-
ok: false,
|
|
185
|
-
reason: "malformed gitignore pattern"
|
|
186
|
-
};
|
|
187
|
-
return {
|
|
188
|
-
ok: true,
|
|
189
|
-
negated: p.startsWith("!"),
|
|
190
|
-
matches: walkTree(root).filter((path) => matchesPattern(p, path))
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
function flag(argv, name) {
|
|
194
|
-
const i = argv.indexOf(name);
|
|
195
|
-
return i === -1 ? void 0 : argv[i + 1];
|
|
196
|
-
}
|
|
197
|
-
function main(argv) {
|
|
198
|
-
const root = flag(argv, "--root") ?? ".";
|
|
199
|
-
const w = (s) => process.stdout.write(`${s}\n`);
|
|
200
|
-
if (argv.includes("--list")) {
|
|
201
|
-
for (const rule of listRules(root)) w(rule);
|
|
202
|
-
return 0;
|
|
203
|
-
}
|
|
204
|
-
if (argv.includes("--add")) {
|
|
205
|
-
const r = addRule(root, flag(argv, "--add") ?? "");
|
|
206
|
-
w(r.ok ? r.message : `refused: ${r.reason}`);
|
|
207
|
-
return r.ok ? 0 : 1;
|
|
208
|
-
}
|
|
209
|
-
if (argv.includes("--remove")) {
|
|
210
|
-
const r = removeRule(root, flag(argv, "--remove") ?? "");
|
|
211
|
-
w(r.ok ? r.message : `refused: ${r.reason}`);
|
|
212
|
-
return r.ok ? 0 : 1;
|
|
213
|
-
}
|
|
214
|
-
if (argv.includes("--induce")) {
|
|
215
|
-
const r = inducePatterns(root, flag(argv, "--induce") ?? "");
|
|
216
|
-
if (!r.ok) {
|
|
217
|
-
w(`unusable: ${r.reason}`);
|
|
218
|
-
return 1;
|
|
219
|
-
}
|
|
220
|
-
w("Candidate ignore patterns:");
|
|
221
|
-
for (const c of r.candidates) w(` ${c}`);
|
|
222
|
-
return 0;
|
|
223
|
-
}
|
|
224
|
-
if (argv.includes("--preview")) {
|
|
225
|
-
const r = previewPattern(root, flag(argv, "--preview") ?? "");
|
|
226
|
-
if (!r.ok) {
|
|
227
|
-
w(`invalid: ${r.reason}`);
|
|
228
|
-
return 1;
|
|
229
|
-
}
|
|
230
|
-
const verb = r.negated ? "re-track" : "ignore";
|
|
231
|
-
if (r.matches.length === 0) {
|
|
232
|
-
w(`This pattern would ${verb} no path.`);
|
|
233
|
-
return 0;
|
|
234
|
-
}
|
|
235
|
-
w(`This pattern would ${verb}:`);
|
|
236
|
-
for (const m of r.matches) w(` ${m}`);
|
|
237
|
-
return 0;
|
|
238
|
-
}
|
|
239
|
-
w("usage: manage-ignore --list | --add <p> | --remove <p> | --induce <path> | --preview <p>");
|
|
240
|
-
return 0;
|
|
241
|
-
}
|
|
242
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
243
|
-
//#endregion
|
|
244
|
-
export { addRule, gitignoreToRegExp, inducePatterns, isRuleLine, isValidPattern, listRules, main, matchesPattern, previewPattern, readIgnoreLines, removeRule, splitLines };
|