@skitterbyte/skitterspec 22.3.2 → 22.4.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/assets/core/env.config.md +13 -1
- package/package.json +1 -1
- package/src/cli.js +9 -10
- package/src/env/provision.js +71 -10
- package/src/env/teardown.js +4 -0
|
@@ -90,7 +90,17 @@ through to a documented conservative default; see the field notes below.
|
|
|
90
90
|
"projectNamePattern": "{repoSlug}_{slug}", // → COMPOSE_PROJECT_NAME
|
|
91
91
|
"portBase": 3000, // first port of slot 0's block
|
|
92
92
|
"portsPerSpec": 10, // block width; slot n → portBase + n*portsPerSpec
|
|
93
|
-
|
|
93
|
+
// Written into the worktree, and passed to compose as `--env-file`. A
|
|
94
|
+
// Docker spec's worktree steps run in this order: seedFiles, this env file
|
|
95
|
+
// (COMPOSE_PROJECT_NAME and PORT_OFFSET appended only where missing, so a
|
|
96
|
+
// value `setup` or you put there is kept), `setup`, then
|
|
97
|
+
// `docker compose … up -d` — all from inside the worktree, so the stack
|
|
98
|
+
// reads this spec's values and not the primary checkout's.
|
|
99
|
+
// It must be a file of its OWN: if it is also in `seedFiles` as a symlink,
|
|
100
|
+
// the write is refused rather than send this spec's port block into the
|
|
101
|
+
// primary checkout's file. Name a different file here (".worktree.env")
|
|
102
|
+
// and have your compose file read the values from it.
|
|
103
|
+
"envFile": ".env",
|
|
94
104
|
"backupCommand": "" // optional pre-teardown backup (e.g. pg_dump);
|
|
95
105
|
// empty = no backup, volumes dropped directly.
|
|
96
106
|
},
|
|
@@ -116,6 +126,8 @@ through to a documented conservative default; see the field notes below.
|
|
|
116
126
|
// `local.settings.json`, `config/local.yml`.
|
|
117
127
|
// Seeded files are gitignored, so they never make the worktree "dirty" and
|
|
118
128
|
// never block teardown; they vanish with the worktree at `spec-env down`.
|
|
129
|
+
// On a Docker project, do not seed `docker.envFile` as a symlink — the env
|
|
130
|
+
// file step refuses to write through one (see `envFile` above).
|
|
119
131
|
// [] (or absent) = seed nothing (current behaviour).
|
|
120
132
|
"seedFiles": {
|
|
121
133
|
"mode": "symlink",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.4.0",
|
|
4
4
|
"description": "Spec-driven development for Claude Code \u2014 a tracker-free filesystem workflow: lifecycle skills and per-spec isolation. For Linear sync, install @skitterbyte/skitterspec-linear instead.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/src/cli.js
CHANGED
|
@@ -1097,21 +1097,20 @@ function specEnvUp(dir, config, specArg, flags = {}) {
|
|
|
1097
1097
|
out.push('')
|
|
1098
1098
|
out.push(' to provision, run:')
|
|
1099
1099
|
for (const cmd of plan.commands) out.push(` ${cmd}`)
|
|
1100
|
-
//
|
|
1101
|
-
//
|
|
1102
|
-
|
|
1100
|
+
// Everything after the fork runs in the worktree, under one heading, in the
|
|
1101
|
+
// order each step depends on the last: seeds (the env file may be one), the
|
|
1102
|
+
// env file, setup (which may derive values into it), and only then the stack.
|
|
1103
|
+
const worktreeSteps = [
|
|
1104
|
+
...plan.seedCommands,
|
|
1105
|
+
...(plan.envCommands || []),
|
|
1106
|
+
...plan.setupCommands,
|
|
1107
|
+
...(plan.dockerCommands || []),
|
|
1108
|
+
]
|
|
1103
1109
|
if (worktreeSteps.length) {
|
|
1104
1110
|
out.push('')
|
|
1105
1111
|
out.push(' then, in the worktree, run:')
|
|
1106
1112
|
for (const cmd of worktreeSteps) out.push(` ${cmd}`)
|
|
1107
1113
|
}
|
|
1108
|
-
if (plan.envContents) {
|
|
1109
|
-
out.push('')
|
|
1110
|
-
out.push(` write ${config.docker.envFile} in the worktree:`)
|
|
1111
|
-
for (const line of plan.envContents.replace(/\n$/, '').split('\n')) {
|
|
1112
|
-
out.push(` ${line}`)
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
1114
|
process.stdout.write(out.join('\n') + '\n')
|
|
1116
1115
|
}
|
|
1117
1116
|
|
package/src/env/provision.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Pure provisioning planner for `spec-env up`.
|
|
5
5
|
*
|
|
6
6
|
* Given a resolved spec and its allocated slot, `planUp` returns the exact
|
|
7
|
-
* side-effecting commands the `/spec-env` skill runs (`git worktree add`,
|
|
8
|
-
* `docker compose up`) and the rendered `.env` contents — but performs no side
|
|
7
|
+
* side-effecting commands the `/spec-env` skill runs (`git worktree add`, the
|
|
8
|
+
* env file, `docker compose up`) and the rendered `.env` contents — but performs no side
|
|
9
9
|
* effects itself. The caller (the CLI) reads/allocates the
|
|
10
10
|
* registry and passes the slot; this stays deterministic and unit-testable with
|
|
11
11
|
* no live git/docker.
|
|
@@ -73,6 +73,50 @@ function seedCommandFor(file, mode) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
// Single-quote a value for POSIX sh: the only character that needs escaping
|
|
77
|
+
// inside single quotes is the single quote itself.
|
|
78
|
+
function shq(value) {
|
|
79
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build one idempotent POSIX-sh command that writes the spec's env file in the
|
|
84
|
+
* current worktree (the cwd when the skill runs it).
|
|
85
|
+
*
|
|
86
|
+
* It was prose once — "write <envFile> in the worktree:" — which left the write
|
|
87
|
+
* to whoever read the plan, and a secrets guard that blocks any tool call naming
|
|
88
|
+
* `.env` refused it. As a command it is one more step the plan runs.
|
|
89
|
+
*
|
|
90
|
+
* Idempotent and additive: it `touch`es the file and appends each key only when
|
|
91
|
+
* no line already sets it, so a re-run changes nothing and a file `setup` has
|
|
92
|
+
* already written to keeps every value it holds — including a deliberate
|
|
93
|
+
* `PORT_OFFSET` of its own.
|
|
94
|
+
*
|
|
95
|
+
* A SYMLINK IS REFUSED, loudly. Seeding links gitignored files from the primary
|
|
96
|
+
* checkout, so a seeded env file is the primary checkout's own; appending
|
|
97
|
+
* through it would write this spec's port block into the main stack's
|
|
98
|
+
* configuration. It runs after the seeds precisely so that collision is caught
|
|
99
|
+
* rather than hidden. The fix is a `docker.envFile` naming a file of its own.
|
|
100
|
+
*/
|
|
101
|
+
function envFileCommandFor(envFile, projectName, offset) {
|
|
102
|
+
const f = shq(envFile)
|
|
103
|
+
const ensure = (key, value) =>
|
|
104
|
+
`{ grep -q '^${key}=' ${f} || echo ${shq(`${key}=${value}`)} >> ${f}; }`
|
|
105
|
+
return (
|
|
106
|
+
`if [ -L ${f} ]; then echo ${shq(
|
|
107
|
+
`env file ${envFile} is a symlink — docker.envFile must name a file of its ` +
|
|
108
|
+
"own, not a seeded one; refusing to write this spec's port block through it",
|
|
109
|
+
)} >&2; exit 1; fi; ` +
|
|
110
|
+
`touch ${f} && ` +
|
|
111
|
+
// A file without a trailing newline would glue the first appended key onto
|
|
112
|
+
// its last line.
|
|
113
|
+
`{ [ ! -s ${f} ] || [ -z "$(tail -c 1 ${f})" ] || echo >> ${f}; } && ` +
|
|
114
|
+
`${ensure('COMPOSE_PROJECT_NAME', projectName)} && ` +
|
|
115
|
+
`${ensure('PORT_OFFSET', offset)} && ` +
|
|
116
|
+
`echo ${shq(`env file ${envFile}: ready`)}`
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
76
120
|
// Cap a path list in a message: enough to recognise, not a wall of text.
|
|
77
121
|
function listPaths(paths) {
|
|
78
122
|
const shown = paths.slice(0, 5).join(', ')
|
|
@@ -294,8 +338,10 @@ function resolveStack(spec, config, { docs = false, composeFilePresent } = {}) {
|
|
|
294
338
|
* @param {object} config normalised env config.
|
|
295
339
|
* @param {object} opts { docs } — documents mode (see below). Optional.
|
|
296
340
|
* @returns {object} { worktreePath, branch, projectName, slot, portOffset,
|
|
297
|
-
* envContents, commands, seedCommands,
|
|
298
|
-
* setupCommands, docs, attached }
|
|
341
|
+
* envContents, commands, seedCommands, envCommands,
|
|
342
|
+
* setupCommands, dockerCommands, docs, attached }
|
|
343
|
+
*
|
|
344
|
+
* The worktree steps run in that order: seeds, env file, setup, compose.
|
|
299
345
|
*/
|
|
300
346
|
function planUp(spec, alloc, config, ctx, opts = {}) {
|
|
301
347
|
const { slot, attached } = alloc
|
|
@@ -367,12 +413,24 @@ function planUp(spec, alloc, config, ctx, opts = {}) {
|
|
|
367
413
|
? `git worktree add ${spec.worktreePath} ${spec.branch}`
|
|
368
414
|
: `git worktree add ${spec.worktreePath} -b ${spec.branch}${forkPoint}`,
|
|
369
415
|
)
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
416
|
+
// The stack comes up LAST, in the worktree: after the seeds, after the env
|
|
417
|
+
// file, after `setup` — which may derive values into that file. It once ran
|
|
418
|
+
// from the primary checkout before the worktree existed, so compose read the
|
|
419
|
+
// primary checkout's env file, fell back to its defaults and collided with the
|
|
420
|
+
// main stack. `--env-file` is always passed: with the default `.env` it is what
|
|
421
|
+
// compose would have found anyway, and with any other name it is the only way
|
|
422
|
+
// compose finds it.
|
|
423
|
+
const envFile = (config.docker && config.docker.envFile) || '.env'
|
|
424
|
+
const envCommands = wantsDocker
|
|
425
|
+
? [`${guard}; ${envFileCommandFor(envFile, spec.projectName, offset)}`]
|
|
426
|
+
: []
|
|
427
|
+
const dockerCommands = wantsDocker
|
|
428
|
+
? [
|
|
429
|
+
`${guard}; docker compose` +
|
|
430
|
+
composeFileArg(config, ctx ? ctx.composeFilePresent : undefined) +
|
|
431
|
+
` --env-file ${shq(envFile)} --project-name ${spec.projectName} up -d`,
|
|
432
|
+
]
|
|
433
|
+
: []
|
|
376
434
|
|
|
377
435
|
// The tree gate. A worktree forks from the base branch's tree, so an
|
|
378
436
|
// uncommitted spec would produce a branch missing the spec it is for — this is
|
|
@@ -398,11 +456,13 @@ function planUp(spec, alloc, config, ctx, opts = {}) {
|
|
|
398
456
|
envContents,
|
|
399
457
|
commands: gate.blocked ? [] : [...gate.commands, ...commands],
|
|
400
458
|
seedCommands: gate.blocked ? [] : seedCommands,
|
|
459
|
+
envCommands: gate.blocked ? [] : envCommands,
|
|
401
460
|
// Documents mode drops these, and keeps the seed commands above. The two are
|
|
402
461
|
// not the same kind of thing: seeding links the gitignored files a worktree
|
|
403
462
|
// has none of — cheap, and a spec author may well want `.env` present —
|
|
404
463
|
// while `setup` is the expensive half that exists to make the tree run.
|
|
405
464
|
setupCommands: gate.blocked || docs ? [] : setupCommands,
|
|
465
|
+
dockerCommands: gate.blocked ? [] : dockerCommands,
|
|
406
466
|
// Reported so a caller reading `--json` can tell a documents worktree from a
|
|
407
467
|
// full one POSITIVELY, rather than inferring it from what is missing. An
|
|
408
468
|
// empty `setupCommands` also describes a project that configured no setup
|
|
@@ -481,6 +541,7 @@ module.exports = {
|
|
|
481
541
|
planCheckoutUp,
|
|
482
542
|
planSpecCommit,
|
|
483
543
|
seedCommandFor,
|
|
544
|
+
envFileCommandFor,
|
|
484
545
|
worktreeCd,
|
|
485
546
|
resolveStack,
|
|
486
547
|
composeFileArg,
|
package/src/env/teardown.js
CHANGED
|
@@ -94,6 +94,10 @@ function planDown(spec, config, flags, ctx) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// --- docker compose down (drop volumes unless kept) ---
|
|
97
|
+
// Run from the primary checkout with no `--env-file`, unlike `up`, and on
|
|
98
|
+
// purpose: compose finds the stack's containers, networks and volumes by the
|
|
99
|
+
// project label `--project-name` gives it, so it needs nothing from the env
|
|
100
|
+
// file — which may already be gone with a half-removed worktree.
|
|
97
101
|
if (wantsDocker) {
|
|
98
102
|
const base =
|
|
99
103
|
`docker compose${composeFileArg(config, ctx ? ctx.composeFilePresent : undefined)}` +
|