@rungs/cli 0.2.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 +28 -15
- package/dist/cli.js +411 -46
- package/dist/cli.js.map +4 -4
- package/modules/ci/files/{{workflow_path}} +9 -1
- package/modules/ci/module.toml +1 -1
- package/modules/concurrency/files/docs/concurrent-sessions.md +11 -5
- package/modules/concurrency/module.toml +1 -1
- package/modules/release/gates/release.toml +29 -4
- package/modules/release/module.toml +1 -1
- package/modules/release/skills/cut-release/SKILL.md +4 -4
- package/package.json +1 -1
- package/src/backlog.ts +17 -2
- package/src/check.ts +9 -2
- package/src/cli.ts +109 -0
- package/src/concurrency.ts +412 -0
- package/src/engines.ts +22 -3
- package/src/engines2.ts +42 -11
- package/src/engines3.ts +4 -3
- package/src/lifecycle.ts +9 -4
- package/src/selftest.ts +16 -0
|
@@ -29,8 +29,16 @@ jobs:
|
|
|
29
29
|
|
|
30
30
|
# Runs every gate in .ai/gates.toml — the same set, in the same order, as
|
|
31
31
|
# `rungs check` locally. A CI-only gate is one nobody can reproduce.
|
|
32
|
+
#
|
|
33
|
+
# Plain `check`, no flags. This said `check --tier full --reporter github`:
|
|
34
|
+
# `--tier` is not a flag the CLI accepts and `--reporter` does not exist at
|
|
35
|
+
# all, so the two values were parsed as a *path* and a *tier* — every run
|
|
36
|
+
# checked a directory named `full` and exited 1 with "no gates registered —
|
|
37
|
+
# is this a rungs repo?". Shipped in three of five profiles, so every repo
|
|
38
|
+
# that installed `ci` had a job that failed on every push and blamed the
|
|
39
|
+
# repo for it (F-030).
|
|
32
40
|
- name: Run gates
|
|
33
|
-
run: npx @rungs/cli check
|
|
41
|
+
run: npx @rungs/cli check
|
|
34
42
|
|
|
35
43
|
# Add a matrix job per package here rather than a workflow per package.
|
|
36
44
|
# A checklist step that creates a file creates N files:
|
package/modules/ci/module.toml
CHANGED
|
@@ -35,9 +35,15 @@ deliberate question, never as a ritual.
|
|
|
35
35
|
|
|
36
36
|
## A failure is attributed, never just counted
|
|
37
37
|
|
|
38
|
-
`
|
|
39
|
-
**inherited** (already red before you started — stated, never blocking)
|
|
40
|
-
blocks). Anything it cannot attribute also blocks: **we do not land on an
|
|
38
|
+
`land` re-runs each failing gate against the merge base in the throwaway worktree it already has,
|
|
39
|
+
and reports each failure as **inherited** (already red before you started — stated, never blocking)
|
|
40
|
+
or **INTRODUCED** (yours — blocks). Anything it cannot attribute also blocks: **we do not land on an
|
|
41
|
+
unknown.**
|
|
42
|
+
|
|
43
|
+
Attribution is **per finding, not per gate.** The first implementation compared gate ids, which
|
|
44
|
+
made an already-red gate a blind spot: a branch could add new broken links and land them as
|
|
45
|
+
inherited, because that gate was red either way. Measured on a scratch repo, and fixed the same
|
|
46
|
+
day — a gate you have not fixed does not excuse the new violations of it you bring.
|
|
41
47
|
|
|
42
48
|
This is not politeness about blame. *A gate that is red for reasons you did not cause and cannot
|
|
43
49
|
fix is a gate you learn to bypass, and a bypassed gate reports nothing.*
|
|
@@ -62,8 +68,8 @@ bug in whatever it is doing.
|
|
|
62
68
|
|
|
63
69
|
## Three things to know when a land refuses
|
|
64
70
|
|
|
65
|
-
- **Your worktree is
|
|
66
|
-
|
|
71
|
+
- **Your worktree is untouched.** `land` does all its work in a throwaway worktree of its own, so
|
|
72
|
+
whatever you had checked out is still checked out. A refusal costs you nothing to recover from.
|
|
67
73
|
- **Check the exit code of `land` itself.** Piping it through `tail` or `grep` reports *that*
|
|
68
74
|
command's status, so a refused land reads as success.
|
|
69
75
|
- **Reconcile generated artifacts by regenerating, never by merging text.** Take one side, re-run
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[module]
|
|
2
2
|
name = "concurrency"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
rung = 5
|
|
5
5
|
summary = "Many sessions on one integration branch: a verified base to cut from, attributed failures, a land protocol that cannot redden the branch, and merge drivers per conflict class."
|
|
6
6
|
|
|
@@ -32,10 +32,21 @@ sources = [
|
|
|
32
32
|
{ file = "pyproject.toml", path = "project.version" },
|
|
33
33
|
]
|
|
34
34
|
rule = "all-agree"
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
# Files that are deliberately versioned on their own. Empty by default: which packages share a
|
|
37
|
+
# version is a judgement about *your* repo, and the engine guessing it is how a healthy layout gets
|
|
38
|
+
# a red gate. rungs' own docs site is the case that found this — a private, never-published package
|
|
39
|
+
# sitting at its own version beside the CLI (F-023).
|
|
40
|
+
exclude = []
|
|
41
|
+
|
|
42
|
+
# No `autofix` key. It named a "rungs release sync-version" command that does not exist and never
|
|
43
|
+
# has, so the message told whoever hit this gate to run something that would fail. The engine now
|
|
44
|
+
# names each file beside the version it states, which is what you need to fix it by hand.
|
|
45
|
+
# (Deliberately not in a code span: `module-commands-exist` reads those as command claims, and it
|
|
46
|
+
# is right to — a phantom command in backticks is exactly what it exists to refuse.)
|
|
36
47
|
message = """
|
|
37
48
|
Version disagrees across {count} locations: {values}. One of them was bumped and the others were
|
|
38
|
-
not, which is discovered by a user rather than by us.
|
|
49
|
+
not, which is discovered by a user rather than by us.
|
|
39
50
|
"""
|
|
40
51
|
|
|
41
52
|
# What this does not cover is pinned, so green never reads as "verified":
|
|
@@ -120,9 +131,23 @@ fixture = { dir = "changelog.d", version = "0.2.0", fragments = ["42.feature.md"
|
|
|
120
131
|
[[self_test]]
|
|
121
132
|
gate = "release-version-consistent"
|
|
122
133
|
expect = "fail"
|
|
123
|
-
fixture = {
|
|
134
|
+
fixture = { packages = { "package.json" = "1.2.0", "web/package.json" = "1.1.0" } }
|
|
135
|
+
|
|
136
|
+
[[self_test]]
|
|
137
|
+
gate = "release-version-consistent"
|
|
138
|
+
expect = "pass"
|
|
139
|
+
fixture = { packages = { "package.json" = "1.2.0", "web/package.json" = "1.2.0" } }
|
|
124
140
|
|
|
141
|
+
# The F-023 case: a sibling deliberately versioned on its own is excluded and the gate stays green,
|
|
142
|
+
# rather than reporting a healthy layout as a disagreement.
|
|
125
143
|
[[self_test]]
|
|
126
144
|
gate = "release-version-consistent"
|
|
127
145
|
expect = "pass"
|
|
128
|
-
fixture = {
|
|
146
|
+
fixture = { packages = { "package.json" = "1.2.0", "web/package.json" = "0.0.1" }, exclude = ["web/package.json"] }
|
|
147
|
+
|
|
148
|
+
# …and excluding one sibling must not blind the gate to a different one. An `exclude` that
|
|
149
|
+
# silently widened would be worse than no gate, because it would still report green.
|
|
150
|
+
[[self_test]]
|
|
151
|
+
gate = "release-version-consistent"
|
|
152
|
+
expect = "fail"
|
|
153
|
+
fixture = { packages = { "package.json" = "1.2.0", "web/package.json" = "0.0.1", "api/package.json" = "9.9.9" }, exclude = ["web/package.json"] }
|
|
@@ -25,10 +25,10 @@ mismatch is usually a fragment somebody skipped, not a versioning disagreement.
|
|
|
25
25
|
rungs check
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Run **every** registered gate. This
|
|
29
|
-
|
|
30
|
-
no gates rather than a pass. A release step that gates on nothing while looking
|
|
31
|
-
one that is skipped, because nobody goes back to check it.
|
|
28
|
+
Run **every** registered gate. This step used to name a "--tier full" flag that the CLI does not
|
|
29
|
+
accept, so both words were parsed as positionals and the run checked a directory that does not
|
|
30
|
+
exist — reporting no gates rather than a pass. A release step that gates on nothing while looking
|
|
31
|
+
green is worse than one that is skipped, because nobody goes back to check it.
|
|
32
32
|
|
|
33
33
|
Narrow to a tier (`rungs check --fast`) only when you know the tier holds what you meant to run.
|
|
34
34
|
|
package/package.json
CHANGED
package/src/backlog.ts
CHANGED
|
@@ -56,7 +56,14 @@ export function planArchive(repoRoot: string, backlogRoot = 'docs/backlog'): Arc
|
|
|
56
56
|
const items = files.filter((f) => posix(f).startsWith(posix(relative(repoRoot, itemsDir)) + '/') && f.endsWith('.md'));
|
|
57
57
|
|
|
58
58
|
for (const rel of items) {
|
|
59
|
-
|
|
59
|
+
// The **basename**, exactly — not a suffix of the path. `/TEMPLATE\.md$/i`
|
|
60
|
+
// also matches any item whose filename ends in `-template.md`, and it did:
|
|
61
|
+
// `WI-010-framework-extraction-template.md` was skipped on every run since
|
|
62
|
+
// this command shipped, so a `done` item stayed in `items/` while the
|
|
63
|
+
// command reported "nothing to archive". An anchored regex that is anchored
|
|
64
|
+
// to the wrong end reads as careful and is not.
|
|
65
|
+
const base = posix(rel).split('/').pop()!;
|
|
66
|
+
if (/^(README|TEMPLATE)\.md$/i.test(base)) continue;
|
|
60
67
|
const text = readFileSync(join(repoRoot, rel), 'utf8');
|
|
61
68
|
const status = field(text, 'status');
|
|
62
69
|
const id = field(text, 'id');
|
|
@@ -70,8 +77,16 @@ export function planArchive(repoRoot: string, backlogRoot = 'docs/backlog'): Arc
|
|
|
70
77
|
.split(',')
|
|
71
78
|
.map((s) => s.trim())
|
|
72
79
|
.filter(Boolean);
|
|
80
|
+
// A child that is **already archived** is finished — that is what being in
|
|
81
|
+
// `archive/` means. Searching only `items/` made every archived child read
|
|
82
|
+
// as unfinished, so an epic whose children had all landed could never be
|
|
83
|
+
// archived and the hold message named five done items as outstanding. The
|
|
84
|
+
// more finished an epic got, the more stuck it became.
|
|
85
|
+
const archived = files.filter((f) => posix(f).startsWith(posix(relative(repoRoot, archiveDir)) + '/') && f.endsWith('.md'));
|
|
73
86
|
const unfinished = children.filter((c) => {
|
|
74
|
-
const f = items.find((i) => i.includes(`${c}-`));
|
|
87
|
+
const f = items.find((i) => i.includes(`${c}-`)) ?? archived.find((i) => i.includes(`${c}-`));
|
|
88
|
+
// Still `!f` → genuinely unknown, and an unknown holds. A child nobody
|
|
89
|
+
// can find is not evidence that it finished.
|
|
75
90
|
return !f || !FINISHED.has(field(readFileSync(join(repoRoot, f), 'utf8'), 'status'));
|
|
76
91
|
});
|
|
77
92
|
if (unfinished.length) {
|
package/src/check.ts
CHANGED
|
@@ -79,7 +79,13 @@ export class UnknownTierError extends Error {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
/**
|
|
83
|
+
* `only` narrows the run to named gate ids. Attribution needs it: after a merged
|
|
84
|
+
* tree goes red, `land` re-runs **just the failing gates** against the merge base
|
|
85
|
+
* to decide whether they were already red. Re-running all of them would give the
|
|
86
|
+
* same verdict and cost a second full pass for gates nobody asked about.
|
|
87
|
+
*/
|
|
88
|
+
export function runGates(repoRoot: string, tier?: string, now = () => Date.now(), only?: ReadonlySet<string>): GateRun[] {
|
|
83
89
|
const { runner, gates } = loadRegistry(repoRoot);
|
|
84
90
|
const runnerTiers: string[] = Array.isArray(runner?.tiers) ? runner.tiers : [];
|
|
85
91
|
// A tier nobody declared selects nothing, and "selected nothing" is
|
|
@@ -95,6 +101,7 @@ export function runGates(repoRoot: string, tier?: string, now = () => Date.now()
|
|
|
95
101
|
// A hook fires on a tool call, not in the runner. Skipping it here is
|
|
96
102
|
// correct; counting it as a pass would not be.
|
|
97
103
|
if (g.trigger) continue;
|
|
104
|
+
if (only && !only.has(g.id)) continue;
|
|
98
105
|
if (tier && !tierSelects(runnerTiers, tier, g.tier)) continue;
|
|
99
106
|
|
|
100
107
|
const started = now();
|
|
@@ -181,7 +188,7 @@ export function loadTable(ref: string | undefined, repoRoot: string): any | null
|
|
|
181
188
|
let paramCache: { root: string; params: Params } | null = null;
|
|
182
189
|
|
|
183
190
|
/** Parameters as the repo installed them, falling back to module defaults. */
|
|
184
|
-
function installedParams(repoRoot: string): Params {
|
|
191
|
+
export function installedParams(repoRoot: string): Params {
|
|
185
192
|
if (paramCache?.root === repoRoot) return paramCache.params;
|
|
186
193
|
const defaults = resolveParams(loadAllModules(MODULES), {}, repoRoot);
|
|
187
194
|
const recordPath = join(repoRoot, '.ai', 'rungs.toml');
|
package/src/cli.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { appendLedger, type GateRun, ledgerQuestions, loadRegistry, runGates, Un
|
|
|
10
10
|
import { applyUpgrade, eject, planUpgrade, PROFILES, readRecord, setupGit } from './lifecycle.ts';
|
|
11
11
|
import { explain, IN_SCOPE as EXPLAINABLE } from './explain.ts';
|
|
12
12
|
import { applyArchive, planArchive } from './backlog.ts';
|
|
13
|
+
import { land, preflight, sessionStart, worktrees } from './concurrency.ts';
|
|
13
14
|
import { existsSync } from 'node:fs';
|
|
14
15
|
import type { DetectResult, Manifest } from './types.ts';
|
|
15
16
|
|
|
@@ -316,6 +317,34 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
|
|
|
316
317
|
}
|
|
317
318
|
(overrides[modName] ??= {})[param] = rhs.join('=');
|
|
318
319
|
}
|
|
320
|
+
|
|
321
|
+
// …and an unknown *name* is refused for the same reason a malformed key is.
|
|
322
|
+
// The comment above says a dropped `--set` "proceeded with the default and
|
|
323
|
+
// looked successful"; a mistyped module or parameter did exactly that, and the
|
|
324
|
+
// echo below then printed `set nosuch.param = 1` as though it had applied
|
|
325
|
+
// (F-028). The whole module set is loaded here, so the names are checkable —
|
|
326
|
+
// there was never a reason to trust them.
|
|
327
|
+
for (const [modName, vals] of Object.entries(overrides)) {
|
|
328
|
+
const mod = mods.find((m) => m.name === modName);
|
|
329
|
+
if (!mod) {
|
|
330
|
+
console.log(
|
|
331
|
+
c.red(`\n --set names a module that does not exist: ${modName}`) +
|
|
332
|
+
c.dim(`\n Known: ${mods.map((m) => m.name).join(', ')}\n`),
|
|
333
|
+
);
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
for (const k of Object.keys(vals)) {
|
|
337
|
+
if (!(k in mod.params)) {
|
|
338
|
+
const known = Object.keys(mod.params);
|
|
339
|
+
console.log(
|
|
340
|
+
c.red(`\n --set names a parameter ${modName} does not have: ${k}`) +
|
|
341
|
+
c.dim(`\n ${known.length ? `${modName} takes: ${known.join(', ')}` : `${modName} takes no parameters`}`) +
|
|
342
|
+
c.dim('\n `rungs modules --params` lists every parameter and its default.\n'),
|
|
343
|
+
);
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
319
348
|
const params = resolveParams(mods, overrides, root);
|
|
320
349
|
for (const [m, vals] of Object.entries(overrides)) {
|
|
321
350
|
for (const [k, v] of Object.entries(vals)) console.log(c.dim(` set ${m}.${k} = ${v}`));
|
|
@@ -467,6 +496,56 @@ function cmdRender(root: string, harnesses: Harness[], stamp: string) {
|
|
|
467
496
|
return 0;
|
|
468
497
|
}
|
|
469
498
|
|
|
499
|
+
/** The loop commands return lines and a verdict; printing them is the CLI's job. */
|
|
500
|
+
function report(r: { ok: boolean; lines: string[] }): number {
|
|
501
|
+
console.log();
|
|
502
|
+
for (const l of r.lines) console.log(` ${r.ok ? l : c.yellow(l)}`);
|
|
503
|
+
console.log();
|
|
504
|
+
return r.ok ? 0 : 1;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* `land` verifies the *merged* tree, so it needs the gate runner pointed at a
|
|
509
|
+
* directory that exists only inside the command. This is the reason the loop is
|
|
510
|
+
* CLI commands rather than scripts the module writes (ADR-0009).
|
|
511
|
+
*/
|
|
512
|
+
function landRunner(dir: string, only?: ReadonlySet<string>) {
|
|
513
|
+
const runs = runGates(dir, undefined, undefined, only);
|
|
514
|
+
const failing = runs.filter((r) => r.status === 'fail' || r.status === 'error');
|
|
515
|
+
return {
|
|
516
|
+
pass: runs.filter((r) => r.status === 'pass').length,
|
|
517
|
+
// `file: message`, so the same broken link in the same file is the same
|
|
518
|
+
// finding across two runs, and a *new* one is visibly not.
|
|
519
|
+
failing: failing.map((r) => ({
|
|
520
|
+
id: r.id,
|
|
521
|
+
findings: r.findings.map((f) => `${f.file ? `${f.file}: ` : ''}${f.message}`),
|
|
522
|
+
})),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function cmdWorktrees(root: string) {
|
|
527
|
+
const { rows, integration } = worktrees(root);
|
|
528
|
+
console.log(c.bold(`\nrungs worktrees — merged into ${integration}?\n`));
|
|
529
|
+
if (!rows.length) {
|
|
530
|
+
console.log(c.dim(' no linked worktrees. `rungs session start <branch>` creates one.\n'));
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
for (const w of rows) {
|
|
534
|
+
const state = w.merged && w.dirty ? c.red('merged · DIRTY') : w.merged ? c.green('merged · prunable') : c.dim('in flight');
|
|
535
|
+
console.log(` ${state.padEnd(28)} ${w.branch.padEnd(30)} ${c.dim(w.path)}`);
|
|
536
|
+
}
|
|
537
|
+
const risky = rows.filter((w) => w.merged && w.dirty);
|
|
538
|
+
const prunable = rows.filter((w) => w.merged && !w.dirty);
|
|
539
|
+
console.log();
|
|
540
|
+
if (risky.length) {
|
|
541
|
+
console.log(c.red(` ${risky.length} worktree(s) hold uncommitted work on a branch that already landed.`));
|
|
542
|
+
console.log(c.dim(' That is where work actually gets lost. Commit it somewhere or decide to drop it.'));
|
|
543
|
+
}
|
|
544
|
+
if (prunable.length) console.log(c.dim(` ${prunable.length} prunable. Removing a worktree is your call, not this command's.`));
|
|
545
|
+
console.log();
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
470
549
|
function cmdCheck(root: string, tier: string | undefined, stamp: string) {
|
|
471
550
|
let runs: GateRun[];
|
|
472
551
|
try {
|
|
@@ -718,6 +797,10 @@ const COMMANDS: [usage: string, blurb: string][] = [
|
|
|
718
797
|
['setup git [path]', 'install the merge drivers .gitattributes names'],
|
|
719
798
|
['modules', 'list the module set and audit the manifests'],
|
|
720
799
|
['backlog archive [path]', 'move finished items to archive/, repointing every link'],
|
|
800
|
+
['session start <branch>', 'cut a branch and worktree from the last verified merge'],
|
|
801
|
+
['preflight [path]', 'did the integration branch change files you changed?'],
|
|
802
|
+
['land <branch>', 'merge → verify the merged tree → advance, or refuse and park it'],
|
|
803
|
+
['worktrees [path]', 'which worktrees are merged, prunable, or merged and still dirty'],
|
|
721
804
|
];
|
|
722
805
|
|
|
723
806
|
/** Every flag the parser honours. A flag absent here is a flag nobody can find. */
|
|
@@ -831,6 +914,19 @@ switch (cmd) {
|
|
|
831
914
|
case 'eject':
|
|
832
915
|
process.exit(cmdEject(resolve(args[0] ?? process.cwd()), flags.has('--dry-run')));
|
|
833
916
|
case 'setup': {
|
|
917
|
+
// The path is `args[1]`, *after* the subcommand — so an omitted `git` put the
|
|
918
|
+
// path into the subcommand slot, where it was discarded, and `setup` then
|
|
919
|
+
// wrote git config into the current directory while reporting success about
|
|
920
|
+
// the repo you named (F-027). `backlog` had refused an unknown subcommand
|
|
921
|
+
// since it shipped; this one accepted anything and exited 0. The asymmetry
|
|
922
|
+
// between the two subcommand-taking commands was the whole bug.
|
|
923
|
+
if (args[0] !== 'git') {
|
|
924
|
+
console.log(
|
|
925
|
+
c.red(`\n unknown: rungs setup ${args[0] ?? ''}`.trimEnd()) +
|
|
926
|
+
c.dim('\n The only subcommand is `git`, and the path comes after it: `rungs setup git [path]`.\n'),
|
|
927
|
+
);
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
834
930
|
const r = setupGit(resolve(args[1] ?? process.cwd()), flags.has('--dry-run'));
|
|
835
931
|
console.log(
|
|
836
932
|
r.drivers.length
|
|
@@ -843,6 +939,19 @@ switch (cmd) {
|
|
|
843
939
|
}
|
|
844
940
|
case 'render':
|
|
845
941
|
process.exit(cmdRender(resolve(args[0] ?? process.cwd()), HARNESSES, STAMP));
|
|
942
|
+
case 'session': {
|
|
943
|
+
if (args[0] !== 'start') {
|
|
944
|
+
console.log(c.red(`\n unknown: rungs session ${args[0] ?? ''}`.trimEnd()) + c.dim('\n The only subcommand is `start`: `rungs session start <branch> [path]`.\n'));
|
|
945
|
+
process.exit(1);
|
|
946
|
+
}
|
|
947
|
+
process.exit(report(sessionStart(process.cwd(), args[1], args[2], flags.has('--dry-run'))));
|
|
948
|
+
}
|
|
949
|
+
case 'preflight':
|
|
950
|
+
process.exit(report(preflight(resolve(args[0] ?? process.cwd()))));
|
|
951
|
+
case 'land':
|
|
952
|
+
process.exit(report(land(process.cwd(), args[0], landRunner, flags.has('--dry-run'))));
|
|
953
|
+
case 'worktrees':
|
|
954
|
+
process.exit(cmdWorktrees(resolve(args[0] ?? process.cwd())));
|
|
846
955
|
case 'add': {
|
|
847
956
|
const target = flags.has('--into') ? args[args.length - 1] : process.cwd();
|
|
848
957
|
const names = flags.has('--into') ? args.slice(0, -1) : args;
|