unitbob 0.3.4 → 0.3.6
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.
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
export const RUN_BUDGET = { workers: 4, review_rounds: 2, repair_rounds: 8 };
|
|
4
|
+
// Only two of the three have a counter behind them. The connector cannot see
|
|
5
|
+
// the host launch a subagent, so `workers` is a number it states and cannot
|
|
6
|
+
// check. It travels in the same block anyway, and the recipe is told not to sort
|
|
7
|
+
// the fields into checked and unchecked: an agent that knows which half is
|
|
8
|
+
// watched has been handed a reason to treat the other half as advice.
|
|
9
|
+
const SPENT_FILE = 'budget-spent.json';
|
|
10
|
+
function spentPath(projectRoot) {
|
|
11
|
+
return join(projectRoot, '.unitbob', 'suite-build', SPENT_FILE);
|
|
12
|
+
}
|
|
13
|
+
// On disk, not in memory, because the loop these bound spans separate processes:
|
|
14
|
+
// every `run-local` and every `suite-review-prepare` is a fresh `npx`. A count
|
|
15
|
+
// held in a running command would reset on each one and bound nothing.
|
|
16
|
+
export function spend(projectRoot, key) {
|
|
17
|
+
const path = spentPath(projectRoot);
|
|
18
|
+
const spent = readSpent(path);
|
|
19
|
+
const count = (spent[key] ?? 0) + 1;
|
|
20
|
+
spent[key] = count;
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
22
|
+
// Written whole and moved into place, never written in place. A plain write
|
|
23
|
+
// that is interrupted leaves truncated JSON, which `readSpent` then reads as
|
|
24
|
+
// nothing spent — so the count would reset exactly when a run is being killed
|
|
25
|
+
// and restarted, which is the loop this exists to bound.
|
|
26
|
+
const staging = `${path}.tmp`;
|
|
27
|
+
writeFileSync(staging, `${JSON.stringify(spent, null, 2)}\n`);
|
|
28
|
+
renameSync(staging, path);
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
// A new build request starts on a fresh budget. Without this a project Unitbob
|
|
32
|
+
// ran a month ago opens today already over its ceiling, and every command
|
|
33
|
+
// announces the last round — noise, and advice that is wrong besides.
|
|
34
|
+
//
|
|
35
|
+
// True when there was something to clear, so the caller can say so out loud.
|
|
36
|
+
// Re-running `suite-prepare` is a documented step of the loop, and it resets
|
|
37
|
+
// both counters; a reset nobody is told about is a ceiling that quietly is not
|
|
38
|
+
// one.
|
|
39
|
+
export function clearSpending(projectRoot) {
|
|
40
|
+
const path = spentPath(projectRoot);
|
|
41
|
+
const had = existsSync(path);
|
|
42
|
+
rmSync(path, { force: true });
|
|
43
|
+
return had;
|
|
44
|
+
}
|
|
45
|
+
// A damaged or hand-edited file counts as nothing spent. The alternative is
|
|
46
|
+
// refusing to run over a bookkeeping file, which would make a counter that
|
|
47
|
+
// deliberately never blocks into the one thing that does.
|
|
48
|
+
function readSpent(path) {
|
|
49
|
+
if (!existsSync(path))
|
|
50
|
+
return {};
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
53
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
54
|
+
return {};
|
|
55
|
+
return Object.fromEntries(Object.entries(parsed)
|
|
56
|
+
.filter(([, value]) => typeof value === 'number' && Number.isFinite(value)));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// A request written by an older connector carries no budget, and that is not an
|
|
63
|
+
// error: there is no ceiling to enforce, so every counter goes quiet and the run
|
|
64
|
+
// works as it did before (spec 34-2, edge cases).
|
|
65
|
+
export function readBudget(value) {
|
|
66
|
+
if (!value || typeof value !== 'object')
|
|
67
|
+
return undefined;
|
|
68
|
+
const block = value;
|
|
69
|
+
const fields = ['workers', 'review_rounds', 'repair_rounds']
|
|
70
|
+
.map((field) => [field, block[field]]);
|
|
71
|
+
if (fields.some(([, number]) => typeof number !== 'number' || !Number.isFinite(number)))
|
|
72
|
+
return undefined;
|
|
73
|
+
return Object.fromEntries(fields);
|
|
74
|
+
}
|
package/dist/files/suiteBuild.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { dirname, join, sep } from 'node:path';
|
|
4
4
|
import { assertUnitbobPath } from "./artifactPath.js";
|
|
5
|
+
import { readBudget, RUN_BUDGET } from "./budget.js";
|
|
5
6
|
export function requestPath(projectRoot) {
|
|
6
7
|
return join(projectRoot, '.unitbob', 'suite-build', 'request.json');
|
|
7
8
|
}
|
|
@@ -172,6 +173,7 @@ export function writeSuiteBuildRequest(projectRoot, branches, knownDefectContext
|
|
|
172
173
|
output_path: outputPath(projectRoot),
|
|
173
174
|
branches,
|
|
174
175
|
known_defect_context: knownDefectContext,
|
|
176
|
+
budget: RUN_BUDGET,
|
|
175
177
|
};
|
|
176
178
|
const path = requestPath(projectRoot);
|
|
177
179
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -193,6 +195,7 @@ export function readSuiteBuildRequest(projectRoot) {
|
|
|
193
195
|
return {
|
|
194
196
|
...request,
|
|
195
197
|
known_defect_context: readKnownDefectContext(request.known_defect_context, path),
|
|
198
|
+
budget: readBudget(request.budget),
|
|
196
199
|
};
|
|
197
200
|
}
|
|
198
201
|
function readKnownDefectContext(value, path) {
|
package/dist/verbs/runLocal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { branchRunner, readHostSuiteOutputsPerBranch, readSuiteBuildRequest, } from "../files/suiteBuild.js";
|
|
2
|
+
import { spend } from "../files/budget.js";
|
|
2
3
|
import { validateStack } from "../runner/precheck.js";
|
|
3
4
|
import { runBddSuite } from "../runner/bdd.js";
|
|
4
5
|
import { runStructuralByRunner } from "./run.js";
|
|
@@ -25,9 +26,31 @@ export async function runLocal(config, args = [], deps) {
|
|
|
25
26
|
d.stdout.write(`Cannot run this branch — its entry in your answer could not be read: ${broken.message}\n`);
|
|
26
27
|
continue;
|
|
27
28
|
}
|
|
28
|
-
await runOneBranch(config, d, suiteKind, outputs.find((entry) => entry.suite_kind === suiteKind));
|
|
29
|
+
const ran = await runOneBranch(config, d, suiteKind, outputs.find((entry) => entry.suite_kind === suiteKind));
|
|
30
|
+
// Only a run that happened spends a repair round. A branch with no entry
|
|
31
|
+
// written yet, or one the stack cannot execute, produced nothing to repair
|
|
32
|
+
// against — charging it would exhaust the budget on rounds that never
|
|
33
|
+
// examined the suite.
|
|
34
|
+
if (!ran)
|
|
35
|
+
continue;
|
|
36
|
+
const spent = spend(config.projectRoot, `run-local:${suiteKind}`);
|
|
37
|
+
if (request.budget && spent > request.budget.repair_rounds) {
|
|
38
|
+
d.stdout.write(polishedEnoughNotice(suiteKind, spent, request.budget.repair_rounds));
|
|
39
|
+
}
|
|
29
40
|
}
|
|
30
41
|
}
|
|
42
|
+
// Not a refusal, and deliberately not about the budget either. After eight
|
|
43
|
+
// rounds of repair the interesting fact is not that a number ran out — it is
|
|
44
|
+
// what the remaining reds most likely are. Both real logs show 3-5 runs of a
|
|
45
|
+
// branch as ordinary work, so a branch on its ninth has already been repaired
|
|
46
|
+
// past the point where the harness is the usual explanation.
|
|
47
|
+
function polishedEnoughNotice(suiteKind, spent, allowed) {
|
|
48
|
+
return (`\nThat was run ${spent} of the ${suiteKind} branch; this build budgeted ${allowed}. ` +
|
|
49
|
+
'Reds that survive this many rounds of repair are far more likely to be defects of your product ' +
|
|
50
|
+
'than of the harness around it.\n' +
|
|
51
|
+
'Publish the suite as it stands rather than keep polishing. A first suite that comes out red is ' +
|
|
52
|
+
'a finding, not a failure — finding those reds is what it was written to do.\n');
|
|
53
|
+
}
|
|
31
54
|
// Which branches to run. No argument runs every branch the request asked for —
|
|
32
55
|
// the same "one suite, one run" shape both recipes insist on, so the default
|
|
33
56
|
// never teaches the habit the recipes forbid. A named branch is for the repair
|
|
@@ -43,17 +66,19 @@ function selectBranches(request, args) {
|
|
|
43
66
|
}
|
|
44
67
|
return named;
|
|
45
68
|
}
|
|
69
|
+
// True when the runner actually executed the branch — which is what a repair
|
|
70
|
+
// round is, and the only thing the caller charges the budget for.
|
|
46
71
|
async function runOneBranch(config, d, suiteKind, output) {
|
|
47
72
|
// Nothing written for this branch yet. That is the ordinary state halfway
|
|
48
73
|
// through a build, not an error — say what is missing and move to the peer.
|
|
49
74
|
if (!output) {
|
|
50
75
|
d.stdout.write(`Nothing to run: your answer has no entry for this branch yet. Write its suite under ` +
|
|
51
76
|
`${branchRoot(config, suiteKind)} and add its entry to the answer, then run this again.\n`);
|
|
52
|
-
return;
|
|
77
|
+
return false;
|
|
53
78
|
}
|
|
54
79
|
if (output.build_error) {
|
|
55
80
|
d.stdout.write(`Not built, by your own answer: ${output.build_error.message}\n`);
|
|
56
|
-
return;
|
|
81
|
+
return false;
|
|
57
82
|
}
|
|
58
83
|
let runner;
|
|
59
84
|
let suitePath;
|
|
@@ -63,12 +88,12 @@ async function runOneBranch(config, d, suiteKind, output) {
|
|
|
63
88
|
}
|
|
64
89
|
catch (err) {
|
|
65
90
|
d.stdout.write(`Cannot run this branch: ${err.message}\n`);
|
|
66
|
-
return;
|
|
91
|
+
return false;
|
|
67
92
|
}
|
|
68
93
|
const check = d.validateStack(config.projectRoot, runner);
|
|
69
94
|
if (!check.ok) {
|
|
70
95
|
d.stdout.write(`Cannot run this branch: ${check.message ?? `this project does not match "${runner}".`}\n`);
|
|
71
|
-
return;
|
|
96
|
+
return false;
|
|
72
97
|
}
|
|
73
98
|
let result;
|
|
74
99
|
try {
|
|
@@ -79,9 +104,10 @@ async function runOneBranch(config, d, suiteKind, output) {
|
|
|
79
104
|
}
|
|
80
105
|
catch (err) {
|
|
81
106
|
d.stdout.write(`The runner could not start: ${err.message}\n`);
|
|
82
|
-
return;
|
|
107
|
+
return false;
|
|
83
108
|
}
|
|
84
109
|
d.stdout.write(report(result));
|
|
110
|
+
return true;
|
|
85
111
|
}
|
|
86
112
|
// The command first, and always — including on a green run. It is the answer to
|
|
87
113
|
// "how do I run just this one file again", which is the question the whole
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { clearSpending } from "../files/budget.js";
|
|
1
2
|
import { materializeHelper } from "../files/guardrails.js";
|
|
2
3
|
import { recipeNameFor, writeSuiteBuildRequest, } from "../files/suiteBuild.js";
|
|
3
4
|
import { bootCheck, SIGNAL_STRENGTH } from "../runner/bootcheck.js";
|
|
@@ -141,6 +142,13 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
141
142
|
throw new Error(`No suite branch can be built this run:\n${blockedNotices.join('\n')}\nNothing was written and nothing was uploaded.`);
|
|
142
143
|
}
|
|
143
144
|
const request = writeSuiteBuildRequest(config.projectRoot, branches, defectContext);
|
|
145
|
+
// A new request is a new build, and a new build starts on the whole budget
|
|
146
|
+
// (spec 34-2). Said out loud, because re-running this verb is a documented
|
|
147
|
+
// step of the loop — the fixable-runner path below ends by asking for it — so
|
|
148
|
+
// a reset that happened silently would be a ceiling that quietly is not one.
|
|
149
|
+
if (clearSpending(config.projectRoot)) {
|
|
150
|
+
actual.stdout.write('Starting a fresh build: the review and run counts from the previous one are cleared.\n');
|
|
151
|
+
}
|
|
144
152
|
const kinds = branches.map((branch) => branch.suite_kind).join(' and ');
|
|
145
153
|
const nextCommand = branches.some((branch) => branch.suite_kind === 'behavioral')
|
|
146
154
|
? '`unitbob suite-review-prepare` before upload'
|
|
@@ -6,6 +6,7 @@ import { copyBehavioralRunnerEnvironment, filesLostOnMaterialize, materializeBeh
|
|
|
6
6
|
import { runBddSuite } from "../runner/bdd.js";
|
|
7
7
|
import { boundReport } from "../runner/boundReport.js";
|
|
8
8
|
import { branchRunner, readHostSuiteOutputs, readSuiteBuildRequest, reviewRequestPath, writeBehavioralReviewRequest, } from "../files/suiteBuild.js";
|
|
9
|
+
import { spend } from "../files/budget.js";
|
|
9
10
|
export async function suiteReviewPrepare(config, _args = [], deps) {
|
|
10
11
|
const actual = {
|
|
11
12
|
runCandidate: runCandidate,
|
|
@@ -37,6 +38,27 @@ export async function suiteReviewPrepare(config, _args = [], deps) {
|
|
|
37
38
|
const request = writeBehavioralReviewRequest(config.projectRoot, behavioral, candidateRun, buildRequest.known_defect_context, fixedCandidateRun);
|
|
38
39
|
actual.stdout.write(`Behavioral review request written to ${reviewRequestPath(config.projectRoot)}\n`);
|
|
39
40
|
actual.stdout.write(`Next: have an independent reviewer inspect this exact candidate and write bdd_quality_review and known_defect_probe to ${request.output_path}, then run \`unitbob put-suite-build\`.\n`);
|
|
41
|
+
// Counted after the request is written, never before it. Refusing the round
|
|
42
|
+
// would rebuild the autobrella deadlock on a different number: the branch
|
|
43
|
+
// could not publish, so the work would be lost again for the sake of the
|
|
44
|
+
// ceiling meant to protect it. What the ceiling buys is a sentence.
|
|
45
|
+
const round = spend(config.projectRoot, 'review_rounds');
|
|
46
|
+
const budget = buildRequest.budget;
|
|
47
|
+
if (budget && round > budget.review_rounds) {
|
|
48
|
+
actual.stdout.write(lastRoundNotice(round, budget.review_rounds));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Says what to do, not that something is forbidden. This is only sayable because
|
|
52
|
+
// spec 34-2 already made an imperfect suite publishable: before it, "publish what
|
|
53
|
+
// you have" was not available to the reviewer at all, and the only way to record
|
|
54
|
+
// a bad Scenario was to take the branch down.
|
|
55
|
+
function lastRoundNotice(round, allowed) {
|
|
56
|
+
return (`\nThis is review round ${round}; the budget for this build was ${allowed}. Treat it as the last round ` +
|
|
57
|
+
'and publish what you have.\n' +
|
|
58
|
+
'A Scenario the reviewer still objects to does not hold the branch back: it is recorded with ' +
|
|
59
|
+
'`verdict: "does_not_pass"` and a `reviewer_objection_text` saying what is wrong, the branch publishes, ' +
|
|
60
|
+
'and the objection is read afterwards by the operator. Another round of rewriting buys less than ' +
|
|
61
|
+
'publishing the objection does.\n');
|
|
40
62
|
}
|
|
41
63
|
async function runCandidate(projectRoot, output, revision) {
|
|
42
64
|
if (revision)
|
|
@@ -199,16 +199,25 @@ function checkSurfaceCoverage(row, id, expected, suiteText, add) {
|
|
|
199
199
|
// here in the server's own shape; refusing it locally would refuse an answer
|
|
200
200
|
// the server takes, which is the one direction of drift that costs a branch.
|
|
201
201
|
const declaredUnreachable = collectUnreachable(row, id, reached, add);
|
|
202
|
-
const
|
|
202
|
+
const deferred = collectDeferred(row, id, reached, declaredUnreachable, add);
|
|
203
|
+
// Spec 34-3, criterion 6. Cheap here and expensive later: over the ceiling is
|
|
204
|
+
// one of the answers the server rejects, and finding it after the suite has
|
|
205
|
+
// been written, run and reviewed costs the whole cycle.
|
|
206
|
+
const budget = expected.surfaceBudget;
|
|
207
|
+
if (budget !== undefined && reached.size > budget) {
|
|
208
|
+
add(`${id} guards ${reached.size} surfaces, over the surface_budget of ${budget}` +
|
|
209
|
+
' — guard the most important ones up to that number and list the rest in deferred_surfaces.');
|
|
210
|
+
}
|
|
211
|
+
const missed = expected.surfaces.filter((surface) => !reached.has(surface) && !declaredUnreachable.has(surface) && !deferred.has(surface));
|
|
203
212
|
if (missed.length > 0) {
|
|
204
213
|
add(`${id} surface_coverage accounts for no scenario at ${missed.join(', ')}` +
|
|
205
|
-
' — drive it,
|
|
214
|
+
' — drive it, declare it unreachable with a business reason, or defer it under the surface budget.');
|
|
206
215
|
}
|
|
207
216
|
// No check here for "declares everything unreachable and drives nothing": the
|
|
208
217
|
// caller already returned when the capability's marker appears in no suite
|
|
209
218
|
// file, so a capability with no Scenario never reaches this function at all.
|
|
210
219
|
// The server refuses that state for the same reason, one rule earlier.
|
|
211
|
-
const foreign = [...reached, ...declaredUnreachable].filter((surface) => !expected.surfaces.includes(surface));
|
|
220
|
+
const foreign = [...reached, ...declaredUnreachable, ...deferred].filter((surface) => !expected.surfaces.includes(surface));
|
|
212
221
|
if (foreign.length > 0) {
|
|
213
222
|
add(`${id} surface_coverage names ${foreign.join(', ')}, which this branch's assignment does not carry.`);
|
|
214
223
|
}
|
|
@@ -262,6 +271,49 @@ function collectUnreachable(row, id, reached, add) {
|
|
|
262
271
|
}
|
|
263
272
|
return surfaces;
|
|
264
273
|
}
|
|
274
|
+
// Spec 34-3, criterion 6. The addresses this run did not take, because the
|
|
275
|
+
// capability carried more than `surface_budget` of them. Mirrored here for the
|
|
276
|
+
// same reason the unreachable list is, and more urgently: refusing this answer
|
|
277
|
+
// locally does not merely disagree with the server, it hands the host an error
|
|
278
|
+
// message pointing at `unreachable_surfaces` — the one place these must never
|
|
279
|
+
// go, because "nothing can cause this request" and "there were better ones" are
|
|
280
|
+
// different sentences and only one of them is true.
|
|
281
|
+
//
|
|
282
|
+
// Plain surface ids, with no reason each. That asymmetry with the unreachable
|
|
283
|
+
// list is deliberate: there the sentence is the guard, because an address you
|
|
284
|
+
// cannot write a sentence about is not really unreachable. Here the reason is
|
|
285
|
+
// the same for every entry and already known — the ceiling.
|
|
286
|
+
function collectDeferred(row, id, reached, unreachable, add) {
|
|
287
|
+
const declared = row.deferred_surfaces;
|
|
288
|
+
if (declared === undefined)
|
|
289
|
+
return new Set();
|
|
290
|
+
if (!Array.isArray(declared) || declared.length === 0) {
|
|
291
|
+
add(`${id} deferred_surfaces must be a non-empty array of surface ids when it is present.`);
|
|
292
|
+
return new Set();
|
|
293
|
+
}
|
|
294
|
+
const surfaces = new Set();
|
|
295
|
+
for (const [index, item] of declared.entries()) {
|
|
296
|
+
const surface = typeof item === 'string' ? item.trim() : '';
|
|
297
|
+
if (!surface) {
|
|
298
|
+
add(`${id} deferred_surfaces[${index}] names no surface.`);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (reached.has(surface)) {
|
|
302
|
+
add(`${id} both drives ${surface} in a scenario and defers it — it is one or the other.`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (unreachable.has(surface)) {
|
|
306
|
+
add(`${id} declares ${surface} both unreachable and deferred — cannot be reached and was not taken this time are different answers.`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (surfaces.has(surface)) {
|
|
310
|
+
add(`${id} defers ${surface} more than once.`);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
surfaces.add(surface);
|
|
314
|
+
}
|
|
315
|
+
return surfaces;
|
|
316
|
+
}
|
|
265
317
|
// Scenario names carrying one marker, by shape rather than by grammar. See the
|
|
266
318
|
// caller for why this stays deliberately timid.
|
|
267
319
|
function scenarioNamesTagged(suiteText, marker) {
|
|
@@ -333,6 +385,7 @@ function hasContent(assignment) {
|
|
|
333
385
|
// case, wherever the shape happens to nest it.
|
|
334
386
|
function assignedCases(assignment) {
|
|
335
387
|
const found = [];
|
|
388
|
+
let surfaceBudget;
|
|
336
389
|
const walk = (value) => {
|
|
337
390
|
if (Array.isArray(value)) {
|
|
338
391
|
value.forEach(walk);
|
|
@@ -341,6 +394,11 @@ function assignedCases(assignment) {
|
|
|
341
394
|
if (!value || typeof value !== 'object')
|
|
342
395
|
return;
|
|
343
396
|
const row = value;
|
|
397
|
+
// Found by the same walk rather than by knowing where the server put it, for
|
|
398
|
+
// the same reason the cases are: the assignment body is opaque here.
|
|
399
|
+
if (typeof row.surface_budget === 'number' && Number.isFinite(row.surface_budget)) {
|
|
400
|
+
surfaceBudget = row.surface_budget;
|
|
401
|
+
}
|
|
344
402
|
const key = row.contract_key;
|
|
345
403
|
const marker = row.case_marker;
|
|
346
404
|
if (typeof key === 'string' && key.startsWith(CONTRACT_PREFIX) && typeof marker === 'string') {
|
|
@@ -356,7 +414,9 @@ function assignedCases(assignment) {
|
|
|
356
414
|
Object.values(row).forEach(walk);
|
|
357
415
|
};
|
|
358
416
|
walk(assignment);
|
|
359
|
-
|
|
417
|
+
// Stamped after the walk, never during it: nothing promises the ceiling is
|
|
418
|
+
// visited before the cases that answer to it.
|
|
419
|
+
return found.map((entry) => ({ ...entry, surfaceBudget }));
|
|
360
420
|
}
|
|
361
421
|
// Every byte of the branch's suite, main file and support files together, for
|
|
362
422
|
// the "is the marker actually in there" check.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unitbob",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Unitbob connector — thin local hands for the Unitbob Rails brain. Owns no domain logic: it runs tools, relays bytes over the wire, and prints what the server returns.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
"build": "rm -rf dist && tsc -p tsconfig.json && chmod +x dist/bin.js",
|
|
17
17
|
"prepublishOnly": "npm run build",
|
|
18
18
|
"test": "node --test test/*.test.ts",
|
|
19
|
-
"check:release": "node scripts/check-release.mjs"
|
|
20
|
-
"hooks:install": "git config core.hooksPath hooks"
|
|
19
|
+
"check:release": "node scripts/check-release.mjs"
|
|
21
20
|
},
|
|
22
21
|
"publishConfig": {
|
|
23
22
|
"access": "public"
|