@rungs/cli 0.3.0 → 0.3.1
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/README.md +1 -1
- package/dist/cli.js +29 -29
- package/dist/cli.js.map +3 -3
- package/package.json +1 -1
- package/src/engines.ts +22 -3
- package/src/engines2.ts +22 -10
- package/src/engines3.ts +4 -3
- package/src/lifecycle.ts +9 -4
package/package.json
CHANGED
package/src/engines.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { join, dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { matchAny, walk } from './glob.ts';
|
|
4
5
|
import { parse as parseToml } from 'smol-toml';
|
|
5
6
|
import { runSelfTests } from './selftest.ts';
|
|
6
7
|
import { loadAllModules } from './manifest.ts';
|
|
8
|
+
|
|
7
9
|
import { resolveParams, substitute } from './substitute.ts';
|
|
8
10
|
import {
|
|
9
11
|
computedClaim,
|
|
@@ -17,6 +19,23 @@ import {
|
|
|
17
19
|
} from './engines2.ts';
|
|
18
20
|
import { boardReconcile, changelogFreshness, gitState, mergeDriverCheck, rulePropagation, termOwnership } from './engines3.ts';
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Where the CLI's own `modules/` lives.
|
|
24
|
+
*
|
|
25
|
+
* This was `new URL(import.meta.url).pathname.slice(1)` in three places. The
|
|
26
|
+
* `.slice(1)` strips a leading `/`, which is right on Windows — `/C:/…` becomes
|
|
27
|
+
* `C:/…` — and **wrong everywhere else**, where `/home/runner/…` becomes the
|
|
28
|
+
* relative `home/runner/…`. On Linux and macOS the directory did not resolve,
|
|
29
|
+
* `loadAllModules` found nothing, and three gates silently lost the data they
|
|
30
|
+
* read from the module set: `skills-spec-pure` and `skills-description-routes`
|
|
31
|
+
* reported every opted-in extension as a non-spec key, and
|
|
32
|
+
* `gates-self-tests-both-directions` reported gates that have fixtures as
|
|
33
|
+
* having none. All three passed here and failed on the first Linux run (F-036).
|
|
34
|
+
*
|
|
35
|
+
* `fileURLToPath` is what the rest of the codebase already used.
|
|
36
|
+
*/
|
|
37
|
+
const CLI_MODULES = join(dirname(fileURLToPath(import.meta.url)), '..', 'modules');
|
|
38
|
+
|
|
20
39
|
export interface Finding {
|
|
21
40
|
file?: string;
|
|
22
41
|
message: string;
|
|
@@ -362,7 +381,7 @@ export const gateMeta: Engine = (_t, root) => {
|
|
|
362
381
|
if (!id || kind !== 'declared' || !table) continue;
|
|
363
382
|
examined++;
|
|
364
383
|
// Tables live in the CLI, not the repo, so read them from the module set.
|
|
365
|
-
const tablePath = join(
|
|
384
|
+
const tablePath = join(CLI_MODULES, dirname(table), 'gates', table.split('/').pop()!);
|
|
366
385
|
const src = existsSync(tablePath) ? readFileSync(tablePath, 'utf8') : '';
|
|
367
386
|
const forGate = [...src.matchAll(/\[\[self_test\]\][\s\S]*?(?=\n\[\[|\n\[|$)/g)]
|
|
368
387
|
.map((m) => m[0])
|
|
@@ -426,7 +445,7 @@ function optedInExtensions(rel: string, spec: any): Set<string> {
|
|
|
426
445
|
const name = rel.split('/').slice(-2)[0];
|
|
427
446
|
if (!name) return new Set();
|
|
428
447
|
try {
|
|
429
|
-
const mods = loadAllModules(
|
|
448
|
+
const mods = loadAllModules(CLI_MODULES);
|
|
430
449
|
const owner = mods.find((m) => m.skills?.[name]?.extensions);
|
|
431
450
|
return new Set(Object.keys(owner?.skills?.[name]?.extensions ?? {}));
|
|
432
451
|
} catch {
|
|
@@ -450,7 +469,7 @@ function parseTable(path: string, module: string): any | null {
|
|
|
450
469
|
// and the runner reported the gate broken — a mismatch entirely of the
|
|
451
470
|
// harness's making. A fixture and the table it tests must resolve against
|
|
452
471
|
// the same parameters or neither means anything.
|
|
453
|
-
const mods = loadAllModules(
|
|
472
|
+
const mods = loadAllModules(CLI_MODULES);
|
|
454
473
|
const params = resolveParams(mods, {}, '.');
|
|
455
474
|
return parseToml(substitute(readFileSync(path, 'utf8'), module, params));
|
|
456
475
|
} catch {
|
package/src/engines2.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { matchAny } from './glob.ts';
|
|
5
5
|
import type { Engine, Finding } from './engines.ts';
|
|
@@ -344,12 +344,28 @@ export const crossReference: Engine = (t, root, files) => {
|
|
|
344
344
|
* this repo does not use — it deletes branches on merge — and it is the
|
|
345
345
|
* direction to be wrong in, because the alternative is the daily false positive.
|
|
346
346
|
*/
|
|
347
|
+
/**
|
|
348
|
+
* `git` as an argv array, never a shell string.
|
|
349
|
+
*
|
|
350
|
+
* `--format=%(refname:short)` is a **bash syntax error** — unquoted parentheses —
|
|
351
|
+
* so `backlog-merged-status` threw on every Linux and macOS repo, hit its catch,
|
|
352
|
+
* and reported "cannot read git branches; status not reconciled" as a finding.
|
|
353
|
+
* The gate ships in four of five profiles and had never once worked off Windows,
|
|
354
|
+
* where `execSync` goes through cmd.exe and parentheses are ordinary characters.
|
|
355
|
+
* Found by the CI matrix on its first run (F-033).
|
|
356
|
+
*
|
|
357
|
+
* Branch names come out of work-item frontmatter, so this is also the difference
|
|
358
|
+
* between reading a field and passing it to a shell.
|
|
359
|
+
*/
|
|
360
|
+
const gitArgs = (root: string, args: string[]) =>
|
|
361
|
+
execFileSync('git', args, { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
362
|
+
|
|
347
363
|
function landedWork(root: string, branch: string, base: string): boolean {
|
|
348
|
-
const git = (
|
|
364
|
+
const git = (...args: string[]) => gitArgs(root, args);
|
|
349
365
|
try {
|
|
350
|
-
const tip = git(
|
|
351
|
-
if (tip === git(
|
|
352
|
-
return git(
|
|
366
|
+
const tip = git('rev-parse', branch);
|
|
367
|
+
if (tip === git('rev-parse', base)) return false;
|
|
368
|
+
return git('log', base, '--merges', '--format=%P')
|
|
353
369
|
.split('\n')
|
|
354
370
|
.some((line) => line.trim().split(/\s+/).slice(1).includes(tip));
|
|
355
371
|
} catch {
|
|
@@ -364,11 +380,7 @@ export const gitStatusReconcile: Engine = (t, root, files) => {
|
|
|
364
380
|
let merged: Set<string>;
|
|
365
381
|
try {
|
|
366
382
|
merged = new Set(
|
|
367
|
-
|
|
368
|
-
cwd: root,
|
|
369
|
-
stdio: 'pipe',
|
|
370
|
-
})
|
|
371
|
-
.toString()
|
|
383
|
+
gitArgs(root, ['branch', '--merged', t.integration_branch ?? 'main', '--format=%(refname:short)'])
|
|
372
384
|
.split('\n')
|
|
373
385
|
.map((s) => s.trim())
|
|
374
386
|
.filter(Boolean),
|
package/src/engines3.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { matchAny } from './glob.ts';
|
|
5
5
|
import type { Engine, Finding } from './engines.ts';
|
|
@@ -213,7 +213,7 @@ export const rulePropagation: Engine = (t, root, files) => {
|
|
|
213
213
|
export const gitState: Engine = (t, root) => {
|
|
214
214
|
let out: string;
|
|
215
215
|
try {
|
|
216
|
-
out =
|
|
216
|
+
out = execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd: root, stdio: 'pipe' }).toString();
|
|
217
217
|
} catch {
|
|
218
218
|
// Not a git repo, or git unavailable. An unattributable result blocks:
|
|
219
219
|
// we do not land on an unknown.
|
|
@@ -248,7 +248,8 @@ export const mergeDriverCheck: Engine = (t, root) => {
|
|
|
248
248
|
for (const driver of required) {
|
|
249
249
|
let configured = '';
|
|
250
250
|
try {
|
|
251
|
-
|
|
251
|
+
// Driver names come from `.gitattributes`, so they reach this as data.
|
|
252
|
+
configured = execFileSync('git', ['config', '--get', `merge.${driver}.driver`], { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
252
253
|
} catch {
|
|
253
254
|
/* absent config exits non-zero, which is the finding */
|
|
254
255
|
}
|
package/src/lifecycle.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import {
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { parse } from 'smol-toml';
|
|
6
6
|
import type { Manifest } from './types.ts';
|
|
7
7
|
import { contentHash, emittedFiles, registerGates } from './add.ts';
|
|
@@ -334,8 +334,13 @@ export function setupGit(repoRoot: string, dryRun = false) {
|
|
|
334
334
|
: 'git merge-file -L ours -L base -L theirs %A %O %B';
|
|
335
335
|
if (!dryRun) {
|
|
336
336
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
337
|
+
// argv, not a shell string. The `rungs-generated` driver command carries
|
|
338
|
+
// single quotes, a literal `\n` and `%A %O %B`, and it was being handed
|
|
339
|
+
// to a shell through `JSON.stringify` — quoting that happens to survive
|
|
340
|
+
// cmd.exe and does not survive bash the same way. The same class of bug
|
|
341
|
+
// as F-033, found in the same sweep.
|
|
342
|
+
execFileSync('git', ['config', `merge.${d}.name`, `rungs ${d.replace('rungs-', '')} driver`], { cwd: repoRoot, stdio: 'pipe' });
|
|
343
|
+
execFileSync('git', ['config', `merge.${d}.driver`, cmd], { cwd: repoRoot, stdio: 'pipe' });
|
|
339
344
|
} catch {
|
|
340
345
|
continue;
|
|
341
346
|
}
|
|
@@ -345,7 +350,7 @@ export function setupGit(repoRoot: string, dryRun = false) {
|
|
|
345
350
|
let rerere = false;
|
|
346
351
|
if (!dryRun) {
|
|
347
352
|
try {
|
|
348
|
-
|
|
353
|
+
execFileSync('git', ['config', 'rerere.enabled', 'true'], { cwd: repoRoot, stdio: 'pipe' });
|
|
349
354
|
rerere = true;
|
|
350
355
|
} catch {
|
|
351
356
|
/* not a git repo */
|