spexcode 0.1.2 → 0.1.4
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/LICENSE +21 -0
- package/package.json +7 -3
- package/spec-cli/src/harness.ts +3 -3
- package/spec-cli/src/init.ts +13 -0
- package/spec-cli/src/layout.ts +17 -2
- package/spec-cli/src/materialize.ts +5 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SpexCode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spexcode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"bin": {
|
|
7
|
+
"bin": {
|
|
8
|
+
"spex": "spec-cli/bin/spex.mjs"
|
|
9
|
+
},
|
|
8
10
|
"files": [
|
|
9
11
|
"spec-cli/src",
|
|
10
12
|
"!spec-cli/src/**/*.test.ts",
|
|
@@ -18,7 +20,9 @@
|
|
|
18
20
|
"spec-dashboard/dist",
|
|
19
21
|
"README.md"
|
|
20
22
|
],
|
|
21
|
-
"engines": {
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
22
26
|
"scripts": {
|
|
23
27
|
"api": "npm --prefix spec-cli run serve",
|
|
24
28
|
"web": "npm --prefix spec-dashboard run dev",
|
package/spec-cli/src/harness.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { execFile } from 'node:child_process'
|
|
|
7
7
|
import { promisify } from 'node:util'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
import { claudeSlashCommands, codexSlashCommands, type SlashCommand } from './slash-commands.js'
|
|
10
|
-
import { runtimeRoot, mainCheckout } from './layout.js'
|
|
10
|
+
import { runtimeRoot, mainCheckout, readConfig } from './layout.js'
|
|
11
11
|
import { tsxBin } from './tsx-bin.js'
|
|
12
12
|
|
|
13
13
|
// @@@ harness-adapter - the ONE seam between SpexCode and the coding-agent harness (Claude Code, Codex, …).
|
|
@@ -609,7 +609,7 @@ export const claudeHarness: Harness = {
|
|
|
609
609
|
events: CLAUDE_EVENTS,
|
|
610
610
|
ownsRendezvous: true, // reclaude opens the rendezvous control socket (prompt delivery + liveness)
|
|
611
611
|
paneTitleIsSelfSummary: true, // claude writes its live task summary into the OSC pane title → headline derives from it
|
|
612
|
-
launchCmd: () => process.env.SPEXCODE_CLAUDE_CMD || 'claude --dangerously-skip-permissions',
|
|
612
|
+
launchCmd: () => process.env.SPEXCODE_CLAUDE_CMD || readConfig(mainCheckout()).sessions?.claudeCmd || 'claude --dangerously-skip-permissions',
|
|
613
613
|
sessionIdArg: (id) => `--session-id ${id}`, // the caller chooses the id
|
|
614
614
|
sessionEnvVar: 'CLAUDE_CODE_SESSION_ID',
|
|
615
615
|
shimFile: (proj) => join(proj, '.claude', 'settings.json'),
|
|
@@ -628,7 +628,7 @@ export const codexHarness: Harness = {
|
|
|
628
628
|
events: CODEX_EVENTS,
|
|
629
629
|
ownsRendezvous: false, // no reclaude daemon — liveness + prompts through the project app-server socket
|
|
630
630
|
paneTitleIsSelfSummary: false, // codex's pane title is a spinner + the cwd folder name, NOT a task summary → headline uses the prompt
|
|
631
|
-
launchCmd: (id, runtimeDir) => codexLaunchCommand(id,
|
|
631
|
+
launchCmd: (id, runtimeDir) => codexLaunchCommand(id, process.env.SPEXCODE_CODEX_CMD || readConfig(mainCheckout()).sessions?.codexCmd, undefined, runtimeDir ?? runtimeRoot()), // env→config→default; ONE app-server per PROJECT
|
|
632
632
|
sessionIdArg: () => '', // codex assigns its own id (the backend owns it via thread/start)
|
|
633
633
|
sessionEnvVar: 'CODEX_THREAD_ID',
|
|
634
634
|
// Codex discovers a LINKED worktree's PROJECT hooks from the ROOT CHECKOUT's `.codex`, NOT the worktree's
|
package/spec-cli/src/init.ts
CHANGED
|
@@ -45,6 +45,19 @@ function resolveHooksDir(dir: string): string | null {
|
|
|
45
45
|
|
|
46
46
|
export async function specInit(targetArg: string | undefined): Promise<void> {
|
|
47
47
|
const targetDir = resolve(targetArg ?? process.cwd())
|
|
48
|
+
|
|
49
|
+
// SpexCode is git-backed: git IS the version database and the hooks live in `.git`. A non-git target
|
|
50
|
+
// would leave a HALF-STATE — specs on disk but no version history, no hooks, no harness shims, no
|
|
51
|
+
// sessions. So fail LOUD before writing anything, rather than seeding debris and warning past it. We do
|
|
52
|
+
// NOT `git init` for them: creating a repo is a side effect beyond init's remit (a subdir, a dir not
|
|
53
|
+
// meant as a repo root), and `git init` is one deliberate command.
|
|
54
|
+
try {
|
|
55
|
+
execFileSync('git', ['-C', targetDir, 'rev-parse', '--is-inside-work-tree'], { stdio: ['ignore', 'ignore', 'ignore'] })
|
|
56
|
+
} catch {
|
|
57
|
+
console.error(`spex init: ${targetDir} is not a git repository. SpexCode is git-backed (git is the version database; the hooks live in .git). Run \`git init\` there first, then \`spex init\`.`)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true })
|
|
49
62
|
console.log(`spex init → ${targetDir}`)
|
|
50
63
|
|
package/spec-cli/src/layout.ts
CHANGED
|
@@ -16,6 +16,8 @@ type Config = {
|
|
|
16
16
|
}
|
|
17
17
|
sessions?: {
|
|
18
18
|
maxActive?: number // concurrency cap: max agents AUTONOMOUSLY PROGRESSING at once (default 6; see sessions.ts maxActive)
|
|
19
|
+
claudeCmd?: string // worker launcher for Claude (default 'claude --dangerously-skip-permissions'); env SPEXCODE_CLAUDE_CMD overrides. A host-specific ABS path belongs in the gitignored spexcode.local.json, not here.
|
|
20
|
+
codexCmd?: string // worker launcher for Codex (default 'codex --yolo'); env SPEXCODE_CODEX_CMD overrides. Same host-path rule.
|
|
19
21
|
}
|
|
20
22
|
serve?: {
|
|
21
23
|
// public-exposure config for `spex serve --public` (resolved gateway-side; see [[public-mode]] / gateway.ts).
|
|
@@ -39,11 +41,24 @@ export type Worktree = {
|
|
|
39
41
|
}
|
|
40
42
|
export type Layout = { main: string; convention: Convention; worktrees: Worktree[] }
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
const p = join(root, 'spexcode.json')
|
|
44
|
+
function readJsonOr(p: string): any {
|
|
44
45
|
if (!existsSync(p)) return {}
|
|
45
46
|
try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return {} }
|
|
46
47
|
}
|
|
48
|
+
// committed `spexcode.json` with an OPTIONAL machine-local `spexcode.local.json` layered on top (gitignored).
|
|
49
|
+
// The local layer is the durable home for HOST-SPECIFIC values that must never be committed — e.g. an
|
|
50
|
+
// absolute worker-launcher path (the host-path leak the repo otherwise warns against). Precedence per field:
|
|
51
|
+
// local over committed; an env var (e.g. SPEXCODE_CLAUDE_CMD) still overrides both at its read site.
|
|
52
|
+
export function readConfig(root: string): Config {
|
|
53
|
+
const committed = readJsonOr(join(root, 'spexcode.json'))
|
|
54
|
+
const local = readJsonOr(join(root, 'spexcode.local.json'))
|
|
55
|
+
const out: any = { ...committed }
|
|
56
|
+
for (const k of Object.keys(local)) {
|
|
57
|
+
const b = committed[k], o = local[k]
|
|
58
|
+
out[k] = (b && o && typeof b === 'object' && typeof o === 'object' && !Array.isArray(o)) ? { ...b, ...o } : o
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
47
62
|
|
|
48
63
|
// the shared git common dir (env-stripped git() so a hook's exported GIT_DIR can't misdirect it). Memoized:
|
|
49
64
|
// it's a process constant, but mainBranch()/mainRoot() resolve it per call (~60 git rev-parse forks per board build without the cache).
|
|
@@ -77,7 +77,11 @@ export function materialize(proj = process.cwd()): string {
|
|
|
77
77
|
// only ignore paths that live INSIDE proj. The codex hooks shim now materializes at the MAIN checkout (codex
|
|
78
78
|
// reads a linked worktree's hooks from the root checkout — see harness.ts); from a linked worktree that path
|
|
79
79
|
// escapes proj (`../…`) and is gitignored by the main checkout's OWN materialize, not the worktree's.
|
|
80
|
-
|
|
80
|
+
// spexcode.local.json — the machine-local config overlay (host-specific values, e.g. an absolute worker
|
|
81
|
+
// launcher path; see portable-layout) — joins the SAME block on the same rationale: machine-specific, must
|
|
82
|
+
// never be committed. Without it an adopter who follows our own guidance to put a host path there would
|
|
83
|
+
// `git add -A` and leak it — the exact thing the overlay exists to prevent.
|
|
84
|
+
const ignorable = [...shimPaths.filter((p) => !p.startsWith('..')), 'spexcode.local.json']
|
|
81
85
|
if (ignorable.length) writeManagedBlock(join(proj, '.gitignore'), ignorable.sort().join('\n'), ['# ', ''])
|
|
82
86
|
// (5) stamp the content-hash marker LAST (so a crash mid-render leaves it stale → re-renders next gate).
|
|
83
87
|
const h = contentHash(proj)
|