@tablation/crew 0.88.0 → 0.90.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/dist/cli.js +462 -152
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -139,6 +139,89 @@ async function periscopesForStatus(stateDir, probe = portListening) {
|
|
|
139
139
|
}));
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// src/shell-init.ts
|
|
143
|
+
var SHELLS = ["zsh", "bash", "fish"];
|
|
144
|
+
function shellFor(requested, envShell) {
|
|
145
|
+
const pick = (requested ?? envShell ?? "").split("/").pop() ?? "";
|
|
146
|
+
return SHELLS.includes(pick) ? pick : "zsh";
|
|
147
|
+
}
|
|
148
|
+
var POSIX = String.raw`# crew shell-init: "periscope up" lands in the worktree, "periscope down" returns to the checkout.
|
|
149
|
+
periscope() {
|
|
150
|
+
case "${"$"}{1-}" in
|
|
151
|
+
up|start)
|
|
152
|
+
shift
|
|
153
|
+
local _out _wt _url _noopen=
|
|
154
|
+
_out=$(mktemp) || return 1
|
|
155
|
+
case " $* " in *" --no-open "*) _noopen=1 ;; esac
|
|
156
|
+
command crew periscope up --json "$@" 2>&1 >"$_out" \
|
|
157
|
+
| command sed -e 's/^{.*"message":"\([^"]*\)".*}$/ \1/' >&2
|
|
158
|
+
if command grep -q '"event":"done"' "$_out"; then
|
|
159
|
+
_wt=$(command sed -n 's/.*"worktree":"\([^"]*\)".*/\1/p' "$_out" | command tail -n 1)
|
|
160
|
+
_url=$(command sed -n 's/.*"url":"\([^"]*\)".*/\1/p' "$_out" | command tail -n 1)
|
|
161
|
+
fi
|
|
162
|
+
command rm -f "$_out"
|
|
163
|
+
[ -n "$_wt" ] && [ -d "$_wt" ] || return 1
|
|
164
|
+
builtin cd "$_wt" || return 1
|
|
165
|
+
printf '%s\n%s\n' "$_url" "$_wt"
|
|
166
|
+
if [ -z "$_noopen" ] && [ -n "$_url" ]; then
|
|
167
|
+
if command -v open >/dev/null 2>&1; then command open "$_url"
|
|
168
|
+
elif command -v xdg-open >/dev/null 2>&1; then command xdg-open "$_url" >/dev/null 2>&1; fi
|
|
169
|
+
fi
|
|
170
|
+
;;
|
|
171
|
+
down|end)
|
|
172
|
+
local _common _checkout=
|
|
173
|
+
_common=$(command git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) && _checkout=${"$"}{_common%/.git}
|
|
174
|
+
command crew periscope "$@" || return $?
|
|
175
|
+
if [ -n "$_checkout" ] && [ -d "$_checkout" ]; then builtin cd "$_checkout" || return 1; fi
|
|
176
|
+
;;
|
|
177
|
+
*)
|
|
178
|
+
command crew periscope "$@"
|
|
179
|
+
;;
|
|
180
|
+
esac
|
|
181
|
+
}
|
|
182
|
+
`;
|
|
183
|
+
var FISH = String.raw`# crew shell-init: "periscope up" lands in the worktree, "periscope down" returns to the checkout.
|
|
184
|
+
function periscope
|
|
185
|
+
switch "$argv[1]"
|
|
186
|
+
case up start
|
|
187
|
+
set -e argv[1]
|
|
188
|
+
set -l out (mktemp); or return 1
|
|
189
|
+
command crew periscope up --json $argv 2>&1 >$out \
|
|
190
|
+
| command sed -e 's/^{.*"message":"\([^"]*\)".*}$/ \1/' >&2
|
|
191
|
+
set -l wt
|
|
192
|
+
set -l url
|
|
193
|
+
if command grep -q '"event":"done"' $out
|
|
194
|
+
set wt (command sed -n 's/.*"worktree":"\([^"]*\)".*/\1/p' $out | command tail -n 1)
|
|
195
|
+
set url (command sed -n 's/.*"url":"\([^"]*\)".*/\1/p' $out | command tail -n 1)
|
|
196
|
+
end
|
|
197
|
+
command rm -f $out
|
|
198
|
+
test -n "$wt" -a -d "$wt"; or return 1
|
|
199
|
+
builtin cd $wt; or return 1
|
|
200
|
+
printf '%s\n%s\n' $url $wt
|
|
201
|
+
if not contains -- --no-open $argv; and test -n "$url"
|
|
202
|
+
if command -sq open
|
|
203
|
+
command open $url
|
|
204
|
+
else if command -sq xdg-open
|
|
205
|
+
command xdg-open $url >/dev/null 2>&1
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
case down end
|
|
209
|
+
set -l checkout
|
|
210
|
+
set -l common (command git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
|
|
211
|
+
and set checkout (string replace -r '/\.git$' '' -- $common)
|
|
212
|
+
command crew periscope $argv; or return $status
|
|
213
|
+
if test -n "$checkout" -a -d "$checkout"
|
|
214
|
+
builtin cd $checkout
|
|
215
|
+
end
|
|
216
|
+
case '*'
|
|
217
|
+
command crew periscope $argv
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
`;
|
|
221
|
+
function shellInitScript(shell) {
|
|
222
|
+
return shell === "fish" ? FISH : POSIX;
|
|
223
|
+
}
|
|
224
|
+
|
|
142
225
|
// src/config.ts
|
|
143
226
|
import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
|
|
144
227
|
import { resolve, dirname as dirname3, isAbsolute, join as join10, basename as basename3 } from "path";
|
|
@@ -2778,12 +2861,25 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
2778
2861
|
return result;
|
|
2779
2862
|
}
|
|
2780
2863
|
function routeForDir(cfg, dir) {
|
|
2781
|
-
|
|
2782
|
-
|
|
2864
|
+
return locateDir(cfg, dir)?.route;
|
|
2865
|
+
}
|
|
2866
|
+
function locateDir(cfg, dir, commonDirOf) {
|
|
2867
|
+
const within = (base, target2) => {
|
|
2783
2868
|
const b = resolve(base);
|
|
2784
|
-
return
|
|
2869
|
+
return target2 === b || target2.startsWith(`${b}/`);
|
|
2785
2870
|
};
|
|
2786
|
-
|
|
2871
|
+
const find = (target2) => {
|
|
2872
|
+
for (const route of cfg.routes) {
|
|
2873
|
+
const repo = reposOf(route).find((x) => within(x.dir, target2));
|
|
2874
|
+
if (repo) return { route, repo, checkout: resolve(repo.dir) };
|
|
2875
|
+
}
|
|
2876
|
+
return void 0;
|
|
2877
|
+
};
|
|
2878
|
+
const target = resolve(dir);
|
|
2879
|
+
const direct = find(target);
|
|
2880
|
+
if (direct) return direct;
|
|
2881
|
+
const common = commonDirOf?.(target);
|
|
2882
|
+
return common ? find(dirname3(resolve(target, common))) : void 0;
|
|
2787
2883
|
}
|
|
2788
2884
|
function findRoute(cfg, route) {
|
|
2789
2885
|
if (!route) {
|
|
@@ -9803,7 +9899,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
9803
9899
|
}
|
|
9804
9900
|
|
|
9805
9901
|
// src/cli.ts
|
|
9806
|
-
import { resolve as resolve5, dirname as dirname20, join as
|
|
9902
|
+
import { resolve as resolve5, dirname as dirname20, join as join41, basename as basename12 } from "path";
|
|
9807
9903
|
import { fileURLToPath } from "url";
|
|
9808
9904
|
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
9809
9905
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
@@ -9843,6 +9939,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
9843
9939
|
"write",
|
|
9844
9940
|
"topic",
|
|
9845
9941
|
"assignee",
|
|
9942
|
+
"issue",
|
|
9943
|
+
"shell",
|
|
9846
9944
|
"resume"
|
|
9847
9945
|
]);
|
|
9848
9946
|
function positionalArgs(argv2) {
|
|
@@ -16532,24 +16630,55 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
16532
16630
|
}
|
|
16533
16631
|
|
|
16534
16632
|
// src/cli.ts
|
|
16535
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
16633
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync33, existsSync as existsSync37, mkdirSync as mkdirSync24, writeFileSync as writeFileSync25, rmSync as rmSync17, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync11, readdirSync as readdirSync17 } from "fs";
|
|
16536
16634
|
import { createInterface } from "readline/promises";
|
|
16537
16635
|
|
|
16538
16636
|
// src/periscope-cmd.ts
|
|
16637
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync28, rmSync as rmSync15, writeFileSync as writeFileSync21 } from "fs";
|
|
16638
|
+
import { join as join35 } from "path";
|
|
16539
16639
|
var PeriscopeError = class extends Error {
|
|
16540
16640
|
};
|
|
16641
|
+
var priorDir = (stateDir) => join35(stateDir, "periscope-priors");
|
|
16642
|
+
var priorPath = (stateDir, ticketRecordId) => join35(priorDir(stateDir), `${ticketRecordId}.json`);
|
|
16643
|
+
function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
16644
|
+
rmSync15(priorPath(stateDir, ticketRecordId), { force: true });
|
|
16645
|
+
}
|
|
16646
|
+
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
16647
|
+
try {
|
|
16648
|
+
mkdirSync19(priorDir(stateDir), { recursive: true });
|
|
16649
|
+
writeFileSync21(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
16650
|
+
} catch {
|
|
16651
|
+
}
|
|
16652
|
+
}
|
|
16653
|
+
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
16654
|
+
try {
|
|
16655
|
+
const p = priorPath(stateDir, ticketRecordId);
|
|
16656
|
+
return existsSync32(p) ? JSON.parse(readFileSync28(p, "utf8")) : void 0;
|
|
16657
|
+
} catch {
|
|
16658
|
+
return void 0;
|
|
16659
|
+
}
|
|
16660
|
+
}
|
|
16541
16661
|
var DEFAULT_MAX_PERISCOPES2 = 3;
|
|
16542
16662
|
var DEFAULT_PERISCOPE_IDLE_MINUTES2 = 120;
|
|
16543
16663
|
function parsePeriscopeUpArgs(positional2, flags) {
|
|
16664
|
+
const usage2 = "usage: crew periscope up [[route] REPO] [--topic TEXT | --issue NNN] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--no-open] [--json]";
|
|
16544
16665
|
const rest = positional2.slice(2);
|
|
16545
|
-
if (rest.length
|
|
16546
|
-
throw new PeriscopeError("usage: crew periscope up [route] REPO [--topic TEXT] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--json]");
|
|
16547
|
-
}
|
|
16666
|
+
if (rest.length > 2) throw new PeriscopeError(usage2);
|
|
16548
16667
|
const topic = flags.topic?.trim();
|
|
16668
|
+
let issue;
|
|
16669
|
+
if (flags.issue !== void 0) {
|
|
16670
|
+
if (topic) throw new PeriscopeError(`--issue and --topic cannot be combined: --issue periscopes a ticket that already exists
|
|
16671
|
+
${usage2}`);
|
|
16672
|
+
const m = /^(?:[A-Za-z]+-)?(\d+)$/.exec(flags.issue.trim());
|
|
16673
|
+
if (!m) throw new PeriscopeError(`"${flags.issue}" is not a ticket number
|
|
16674
|
+
${usage2}`);
|
|
16675
|
+
issue = String(Number.parseInt(m[1], 10));
|
|
16676
|
+
}
|
|
16549
16677
|
return {
|
|
16550
16678
|
...rest.length === 2 ? { route: rest[0] } : {},
|
|
16551
|
-
repo: rest[rest.length - 1],
|
|
16679
|
+
...rest.length > 0 ? { repo: rest[rest.length - 1] } : {},
|
|
16552
16680
|
...topic ? { topic } : {},
|
|
16681
|
+
...issue ? { issue } : {},
|
|
16553
16682
|
isolate: flags.isolate,
|
|
16554
16683
|
...flags.host ? { host: flags.host } : {},
|
|
16555
16684
|
...flags.assignee ? { assignee: flags.assignee } : {},
|
|
@@ -16574,7 +16703,37 @@ function periscopeCaps(l) {
|
|
|
16574
16703
|
idleMinutes: l.periscopeIdleMinutes ?? DEFAULT_PERISCOPE_IDLE_MINUTES2
|
|
16575
16704
|
};
|
|
16576
16705
|
}
|
|
16706
|
+
async function existingRefusal(e, t) {
|
|
16707
|
+
if (t.closed) return `${t.tag} is ${t.status}; a closed ticket cannot be periscoped`;
|
|
16708
|
+
if (e.repoId && t.repoId !== e.repoId) return `${t.tag} belongs to a different repository than ${e.repoName}`;
|
|
16709
|
+
if (t.assignee && t.assignee !== e.pairId) {
|
|
16710
|
+
const agent = await e.agentHolder(t.assignee);
|
|
16711
|
+
if (agent) return `${t.tag} is held by ${agent}`;
|
|
16712
|
+
}
|
|
16713
|
+
if (t.heldByShipId && e.shipId && t.heldByShipId !== e.shipId) return `${t.tag} is held by another ship (${t.heldByShipId})`;
|
|
16714
|
+
if (e.hasLiveRun(t.id)) return `${t.tag} has a run in flight on this ship`;
|
|
16715
|
+
return void 0;
|
|
16716
|
+
}
|
|
16577
16717
|
var tail = (s, n = 20) => s.trimEnd().split("\n").slice(-n).join("\n");
|
|
16718
|
+
function defaultTopic(repo, now) {
|
|
16719
|
+
return `${repo}, ${now.toISOString().slice(0, 10)}`;
|
|
16720
|
+
}
|
|
16721
|
+
function openerFor(platform2) {
|
|
16722
|
+
return platform2 === "darwin" ? "open" : platform2 === "linux" ? "xdg-open" : void 0;
|
|
16723
|
+
}
|
|
16724
|
+
function upOutput(res, terminal) {
|
|
16725
|
+
return terminal ? `${res.ticket}
|
|
16726
|
+
${res.url}
|
|
16727
|
+
${res.worktree}
|
|
16728
|
+
` : `${res.ticket}
|
|
16729
|
+
${res.worktree}
|
|
16730
|
+
${res.url}
|
|
16731
|
+
`;
|
|
16732
|
+
}
|
|
16733
|
+
function shouldOpen(f) {
|
|
16734
|
+
if (f.noOpen || f.json) return false;
|
|
16735
|
+
return f.open || f.terminal;
|
|
16736
|
+
}
|
|
16578
16737
|
function progressComment(url, handoff) {
|
|
16579
16738
|
return [
|
|
16580
16739
|
`Periscope is up: ${url}`,
|
|
@@ -16595,17 +16754,35 @@ async function periscopePreflight(d, repo) {
|
|
|
16595
16754
|
}
|
|
16596
16755
|
async function runPeriscopeUp(d, args) {
|
|
16597
16756
|
await periscopePreflight(d, args.repo);
|
|
16598
|
-
const
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
if (
|
|
16602
|
-
|
|
16603
|
-
|
|
16757
|
+
const ex = d.existing;
|
|
16758
|
+
let draft;
|
|
16759
|
+
let prior;
|
|
16760
|
+
if (args.issue) {
|
|
16761
|
+
if (!ex) throw new PeriscopeError("--issue is not supported here");
|
|
16762
|
+
prior = await ex.load(args.issue);
|
|
16763
|
+
if (!prior) throw new PeriscopeError(`no ticket numbered ${args.issue} on this route`);
|
|
16764
|
+
const refused = await existingRefusal(ex, prior);
|
|
16765
|
+
if (refused) throw new PeriscopeError(refused);
|
|
16766
|
+
const unsafe = d.baseProblem();
|
|
16767
|
+
if (unsafe) throw new PeriscopeError(`DO NOT CUT A WORKTREE FROM THIS BASE \u2014 ${unsafe}`);
|
|
16768
|
+
await ex.hold(prior.id);
|
|
16769
|
+
ex.remember(prior.id, { assignee: prior.assignee, status: prior.status });
|
|
16770
|
+
draft = prior;
|
|
16771
|
+
d.say("held", `holding ${prior.tag} (status ${prior.status} stays)`, { ticket: prior.tag, ticketRecordId: prior.id });
|
|
16772
|
+
} else {
|
|
16773
|
+
const topic = args.topic ?? (await d.askTopic())?.trim() ?? defaultTopic(args.repo, d.now());
|
|
16774
|
+
const unsafe = d.baseProblem();
|
|
16775
|
+
if (unsafe) throw new PeriscopeError(`DO NOT CUT A WORKTREE FROM THIS BASE \u2014 ${unsafe}`);
|
|
16776
|
+
draft = await d.fileDraft(`Periscope: ${topic}`);
|
|
16777
|
+
d.say("filed", `filed draft ${draft.tag}`, { ticket: draft.tag, ticketRecordId: draft.id });
|
|
16778
|
+
}
|
|
16604
16779
|
let wt;
|
|
16605
16780
|
const abandon = async (why) => {
|
|
16606
16781
|
if (wt) await d.teardown(wt.path, wt.branch).catch(() => {
|
|
16607
16782
|
});
|
|
16608
|
-
await d.
|
|
16783
|
+
if (prior) await d.existing.restore(prior.id, prior.assignee).catch(() => {
|
|
16784
|
+
});
|
|
16785
|
+
else await d.closeDraft(draft.id, `Periscope abandoned: ${why.split("\n")[0]}`).catch(() => {
|
|
16609
16786
|
});
|
|
16610
16787
|
throw new PeriscopeError(why);
|
|
16611
16788
|
};
|
|
@@ -16682,25 +16859,36 @@ ${tail(preview.output)}`);
|
|
|
16682
16859
|
await d.comment(draft.id, progressComment(url, handoff?.output)).catch((e) => {
|
|
16683
16860
|
d.say("warning", `progress comment not posted: ${e.message}`);
|
|
16684
16861
|
});
|
|
16685
|
-
return {
|
|
16862
|
+
return {
|
|
16863
|
+
ticket: draft.tag,
|
|
16864
|
+
ticketRecordId: draft.id,
|
|
16865
|
+
worktree: wt.path,
|
|
16866
|
+
branch: wt.branch,
|
|
16867
|
+
url,
|
|
16868
|
+
periscopeRecordId: row.id,
|
|
16869
|
+
...prior ? { existing: true, priorStatus: prior.status } : {}
|
|
16870
|
+
};
|
|
16686
16871
|
}
|
|
16687
16872
|
function parsePeriscopeDownArgs(positional2, flags) {
|
|
16688
|
-
const usage2 = "usage: crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]";
|
|
16873
|
+
const usage2 = "usage: crew periscope down [[route] NNN] --keep|--discard [--allow-empty] [--json]";
|
|
16689
16874
|
const rest = positional2.slice(2);
|
|
16690
|
-
if (rest.length
|
|
16875
|
+
if (rest.length > 2) throw new PeriscopeError(usage2);
|
|
16691
16876
|
if (flags.keep === flags.discard) throw new PeriscopeError(`pass exactly one of --keep or --discard
|
|
16692
16877
|
${usage2}`);
|
|
16693
|
-
const m = /^(?:[A-Za-z]+-)?(\d+)$/.exec(rest[rest.length - 1]);
|
|
16694
|
-
if (!m) throw new PeriscopeError(`"${rest[rest.length - 1]}" is not a ticket number
|
|
16878
|
+
const m = rest.length === 0 ? null : /^(?:[A-Za-z]+-)?(\d+)$/.exec(rest[rest.length - 1]);
|
|
16879
|
+
if (rest.length > 0 && !m) throw new PeriscopeError(`"${rest[rest.length - 1]}" is not a ticket number
|
|
16695
16880
|
${usage2}`);
|
|
16696
16881
|
return {
|
|
16697
16882
|
...rest.length === 2 ? { route: rest[0] } : {},
|
|
16698
|
-
number: m[1],
|
|
16883
|
+
...m ? { number: m[1] } : {},
|
|
16699
16884
|
mode: flags.keep ? "keep" : "discard",
|
|
16700
16885
|
allowEmpty: flags.allowEmpty,
|
|
16701
16886
|
json: flags.json
|
|
16702
16887
|
};
|
|
16703
16888
|
}
|
|
16889
|
+
function rowForCwd(rows, cwd) {
|
|
16890
|
+
return rows.find((r) => r.state === "live" && r.worktree !== "" && (cwd === r.worktree || cwd.startsWith(`${r.worktree}/`)));
|
|
16891
|
+
}
|
|
16704
16892
|
var PROTOTYPE_HEADING = "## Prototype";
|
|
16705
16893
|
function prototypeSection(files) {
|
|
16706
16894
|
const added = files.reduce((n, f) => n + f.added, 0);
|
|
@@ -16731,6 +16919,21 @@ async function runPeriscopeDown(d, args) {
|
|
|
16731
16919
|
d.say("unchanged", `${done.ticket} is already ${done.state}; nothing to do`);
|
|
16732
16920
|
return { ticket: done.ticket, mode: args.mode, outcome: "unchanged", state: done.state };
|
|
16733
16921
|
}
|
|
16922
|
+
const existing = (await d.readTicket(row.ticketRecordId)).isDraft === false;
|
|
16923
|
+
const prior = existing ? d.priorHold?.(row.ticketRecordId) : void 0;
|
|
16924
|
+
if (existing && args.mode === "discard") {
|
|
16925
|
+
const killed2 = await d.stopServer(row.worktree, row.number);
|
|
16926
|
+
d.say("server", killed2 ? `stopped ${killed2} process(es)` : "no server was listening");
|
|
16927
|
+
const onRemote = d.remoteHasBranch?.(row.worktree, row.branch) ?? true;
|
|
16928
|
+
d.removeWorktree(row.worktree, row.branch, !onRemote);
|
|
16929
|
+
d.say("worktree", onRemote ? `removed ${row.worktree}; ${row.branch} stays, it is on the remote` : `removed ${row.worktree} and branch ${row.branch}`);
|
|
16930
|
+
await d.updateTicket(row.ticketRecordId, { status: "released", fillProjectAndRepo: false });
|
|
16931
|
+
d.say("ticket", `${row.ticket} hold cleared; status unchanged${prior ? ` (assignee before the periscope: ${prior.assignee ?? "none"})` : ""}`);
|
|
16932
|
+
await d.setRowState(row.id, "discarded");
|
|
16933
|
+
d.say("row", "row is discarded");
|
|
16934
|
+
forgetLocal(d, row.ticketRecordId);
|
|
16935
|
+
return { ticket: row.ticket, mode: "discard", outcome: "done", state: "discarded", existing: true, ...prior ? { status: prior.status } : {} };
|
|
16936
|
+
}
|
|
16734
16937
|
if (args.mode === "discard") {
|
|
16735
16938
|
const killed2 = await d.stopServer(row.worktree, row.number);
|
|
16736
16939
|
d.say("server", killed2 ? `stopped ${killed2} process(es)` : "no server was listening");
|
|
@@ -16762,12 +16965,17 @@ async function runPeriscopeDown(d, args) {
|
|
|
16762
16965
|
const description = t.description.includes(PROTOTYPE_HEADING) ? t.description : `${t.description.trimEnd()}
|
|
16763
16966
|
|
|
16764
16967
|
${section}`;
|
|
16765
|
-
|
|
16766
|
-
description,
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
16968
|
+
if (existing) {
|
|
16969
|
+
await d.updateTicket(row.ticketRecordId, { description, fillProjectAndRepo: false, status: "released" });
|
|
16970
|
+
d.say("ticket", `${row.ticket} has a Prototype section and no assignee; status unchanged${prior ? ` (assignee before the periscope: ${prior.assignee ?? "none"})` : ""}`);
|
|
16971
|
+
} else {
|
|
16972
|
+
await d.updateTicket(row.ticketRecordId, {
|
|
16973
|
+
description,
|
|
16974
|
+
fillProjectAndRepo: !t.hasProject || !t.hasRepo,
|
|
16975
|
+
status: "new"
|
|
16976
|
+
});
|
|
16977
|
+
d.say("ticket", `${row.ticket} is new and unassigned with a Prototype section`);
|
|
16978
|
+
}
|
|
16771
16979
|
await d.postPushed(row.ticketRecordId, pushed.sha, pushed.remote, row.branch);
|
|
16772
16980
|
const killed = await d.stopServer(row.worktree, row.number);
|
|
16773
16981
|
d.say("server", killed ? `stopped ${killed} process(es)` : "no server was listening");
|
|
@@ -16776,7 +16984,7 @@ ${section}`;
|
|
|
16776
16984
|
await d.setRowState(row.id, "kept");
|
|
16777
16985
|
d.say("row", "row is kept");
|
|
16778
16986
|
forgetLocal(d, row.ticketRecordId);
|
|
16779
|
-
return { ticket: row.ticket, mode: "keep", outcome: "done", state: "kept", sha: pushed.sha };
|
|
16987
|
+
return { ticket: row.ticket, mode: "keep", outcome: "done", state: "kept", sha: pushed.sha, ...existing ? { existing: true, ...prior ? { status: prior.status } : {} } : {} };
|
|
16780
16988
|
}
|
|
16781
16989
|
function periscopeListEntries(rows, now, isListening) {
|
|
16782
16990
|
return rows.filter((r) => r.state === "live").map((r) => {
|
|
@@ -16838,7 +17046,7 @@ ${tail2(preview.output)}`);
|
|
|
16838
17046
|
// src/periscope-idle.ts
|
|
16839
17047
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
16840
17048
|
import { statSync as statSync9 } from "fs";
|
|
16841
|
-
import { join as
|
|
17049
|
+
import { join as join36 } from "path";
|
|
16842
17050
|
var MAX_SCANNED_FILES = 2e4;
|
|
16843
17051
|
function newestWorktreeMtime(root) {
|
|
16844
17052
|
let listing;
|
|
@@ -16858,7 +17066,7 @@ function newestWorktreeMtime(root) {
|
|
|
16858
17066
|
if (!rel) continue;
|
|
16859
17067
|
if (++scanned > MAX_SCANNED_FILES) break;
|
|
16860
17068
|
try {
|
|
16861
|
-
const m = statSync9(
|
|
17069
|
+
const m = statSync9(join36(root, rel)).mtimeMs;
|
|
16862
17070
|
if (newest === null || m > newest) newest = m;
|
|
16863
17071
|
} catch {
|
|
16864
17072
|
}
|
|
@@ -16934,18 +17142,18 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
|
16934
17142
|
|
|
16935
17143
|
// src/ssh-keys.ts
|
|
16936
17144
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
16937
|
-
import { existsSync as
|
|
16938
|
-
import { dirname as dirname19, join as
|
|
17145
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, chmodSync } from "fs";
|
|
17146
|
+
import { dirname as dirname19, join as join37 } from "path";
|
|
16939
17147
|
var SshKeyError = class extends Error {
|
|
16940
17148
|
};
|
|
16941
17149
|
function shipSshKeyPathFor(stateDir) {
|
|
16942
|
-
return
|
|
17150
|
+
return join37(stateDir, "ssh", "id_ed25519");
|
|
16943
17151
|
}
|
|
16944
17152
|
function ensureShipSshKeypair(stateDir) {
|
|
16945
17153
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
16946
17154
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
16947
|
-
if (!
|
|
16948
|
-
|
|
17155
|
+
if (!existsSync33(privateKeyPath)) {
|
|
17156
|
+
mkdirSync20(dirname19(privateKeyPath), { recursive: true });
|
|
16949
17157
|
try {
|
|
16950
17158
|
execFileSync7("ssh-keygen", [
|
|
16951
17159
|
"-t",
|
|
@@ -16965,10 +17173,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
16965
17173
|
}
|
|
16966
17174
|
chmodSync(privateKeyPath, 384);
|
|
16967
17175
|
}
|
|
16968
|
-
if (!
|
|
17176
|
+
if (!existsSync33(publicKeyPath)) {
|
|
16969
17177
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
16970
17178
|
}
|
|
16971
|
-
const publicKey =
|
|
17179
|
+
const publicKey = readFileSync29(publicKeyPath, "utf8").trim();
|
|
16972
17180
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
16973
17181
|
}
|
|
16974
17182
|
function sshKeygenAvailable() {
|
|
@@ -17185,9 +17393,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
17185
17393
|
|
|
17186
17394
|
// src/tunnel.ts
|
|
17187
17395
|
import { spawn as spawn5 } from "child_process";
|
|
17188
|
-
import { existsSync as
|
|
17396
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, writeFileSync as writeFileSync22, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
|
|
17189
17397
|
import { randomInt } from "crypto";
|
|
17190
|
-
import { join as
|
|
17398
|
+
import { join as join38 } from "path";
|
|
17191
17399
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
17192
17400
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
17193
17401
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -17254,21 +17462,21 @@ function startTunnel(opts) {
|
|
|
17254
17462
|
};
|
|
17255
17463
|
}
|
|
17256
17464
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
17257
|
-
return
|
|
17465
|
+
return join38(stateDir, "passengers", `${workspaceId}.json`);
|
|
17258
17466
|
}
|
|
17259
17467
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
17260
17468
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
17261
|
-
if (!
|
|
17469
|
+
if (!existsSync34(path)) return void 0;
|
|
17262
17470
|
try {
|
|
17263
|
-
return JSON.parse(
|
|
17471
|
+
return JSON.parse(readFileSync30(path, "utf8"));
|
|
17264
17472
|
} catch {
|
|
17265
17473
|
return void 0;
|
|
17266
17474
|
}
|
|
17267
17475
|
}
|
|
17268
17476
|
function writePersistedTunnel(stateDir, state) {
|
|
17269
17477
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
17270
|
-
|
|
17271
|
-
|
|
17478
|
+
mkdirSync21(join38(stateDir, "passengers"), { recursive: true });
|
|
17479
|
+
writeFileSync22(path, `${JSON.stringify(state, null, 2)}
|
|
17272
17480
|
`);
|
|
17273
17481
|
}
|
|
17274
17482
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -17365,8 +17573,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
17365
17573
|
}
|
|
17366
17574
|
}
|
|
17367
17575
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
17368
|
-
const dir =
|
|
17369
|
-
if (!
|
|
17576
|
+
const dir = join38(stateDir, "passengers");
|
|
17577
|
+
if (!existsSync34(dir)) return [];
|
|
17370
17578
|
return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
17371
17579
|
}
|
|
17372
17580
|
|
|
@@ -17487,8 +17695,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
17487
17695
|
import { createServer as createHttpServer } from "http";
|
|
17488
17696
|
import { spawn as spawn6 } from "child_process";
|
|
17489
17697
|
import { randomBytes } from "crypto";
|
|
17490
|
-
import { existsSync as
|
|
17491
|
-
import { join as
|
|
17698
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, writeFileSync as writeFileSync23, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
|
|
17699
|
+
import { join as join39 } from "path";
|
|
17492
17700
|
|
|
17493
17701
|
// src/secret-compare.ts
|
|
17494
17702
|
import { timingSafeEqual } from "crypto";
|
|
@@ -17577,21 +17785,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
17577
17785
|
return child;
|
|
17578
17786
|
};
|
|
17579
17787
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
17580
|
-
return
|
|
17788
|
+
return join39(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
17581
17789
|
}
|
|
17582
17790
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
17583
17791
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
17584
|
-
if (!
|
|
17792
|
+
if (!existsSync35(path)) return void 0;
|
|
17585
17793
|
try {
|
|
17586
|
-
return JSON.parse(
|
|
17794
|
+
return JSON.parse(readFileSync31(path, "utf8"));
|
|
17587
17795
|
} catch {
|
|
17588
17796
|
return void 0;
|
|
17589
17797
|
}
|
|
17590
17798
|
}
|
|
17591
17799
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
17592
17800
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
17593
|
-
|
|
17594
|
-
|
|
17801
|
+
mkdirSync22(join39(stateDir, "passengers"), { recursive: true });
|
|
17802
|
+
writeFileSync23(path, `${JSON.stringify(state, null, 2)}
|
|
17595
17803
|
`);
|
|
17596
17804
|
}
|
|
17597
17805
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -17611,8 +17819,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
17611
17819
|
}
|
|
17612
17820
|
}
|
|
17613
17821
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
17614
|
-
const dir =
|
|
17615
|
-
if (!
|
|
17822
|
+
const dir = join39(stateDir, "passengers");
|
|
17823
|
+
if (!existsSync35(dir)) return [];
|
|
17616
17824
|
const suffix = "-sync.json";
|
|
17617
17825
|
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
17618
17826
|
}
|
|
@@ -17700,8 +17908,8 @@ function describeCause(err) {
|
|
|
17700
17908
|
}
|
|
17701
17909
|
|
|
17702
17910
|
// src/daemon.ts
|
|
17703
|
-
import { existsSync as
|
|
17704
|
-
import { join as
|
|
17911
|
+
import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync32, readdirSync as readdirSync16, rmSync as rmSync16, statSync as statSync10, writeFileSync as writeFileSync24 } from "fs";
|
|
17912
|
+
import { join as join40 } from "path";
|
|
17705
17913
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
17706
17914
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
17707
17915
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
@@ -17772,7 +17980,7 @@ function latestMtime(path) {
|
|
|
17772
17980
|
if (!stat.isDirectory()) return 0;
|
|
17773
17981
|
let latest = stat.mtimeMs;
|
|
17774
17982
|
for (const entry of readdirSync16(path, { withFileTypes: true })) {
|
|
17775
|
-
latest = Math.max(latest, latestMtime(
|
|
17983
|
+
latest = Math.max(latest, latestMtime(join40(path, entry.name)));
|
|
17776
17984
|
}
|
|
17777
17985
|
return latest;
|
|
17778
17986
|
}
|
|
@@ -17781,23 +17989,23 @@ function computeTreeSignature(paths) {
|
|
|
17781
17989
|
}
|
|
17782
17990
|
function makeStaleChecker(paths, opts = {}) {
|
|
17783
17991
|
const initial = computeTreeSignature(paths);
|
|
17784
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
17992
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync36(opts.reloadSentinel);
|
|
17785
17993
|
}
|
|
17786
17994
|
function reloadSentinelFor(stateDir) {
|
|
17787
|
-
return
|
|
17995
|
+
return join40(stateDir, "reload-requested");
|
|
17788
17996
|
}
|
|
17789
17997
|
function daemonStampFor(stateDir) {
|
|
17790
|
-
return
|
|
17998
|
+
return join40(stateDir, "daemon.started");
|
|
17791
17999
|
}
|
|
17792
18000
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
18001
|
+
mkdirSync23(stateDir, { recursive: true });
|
|
18002
|
+
rmSync16(reloadSentinelFor(stateDir), { force: true });
|
|
18003
|
+
writeFileSync24(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
17796
18004
|
`);
|
|
17797
18005
|
}
|
|
17798
18006
|
function readDaemonStarted(stateDir) {
|
|
17799
18007
|
try {
|
|
17800
|
-
const v = JSON.parse(
|
|
18008
|
+
const v = JSON.parse(readFileSync32(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
17801
18009
|
return typeof v === "number" ? v : null;
|
|
17802
18010
|
} catch {
|
|
17803
18011
|
return null;
|
|
@@ -18419,12 +18627,13 @@ function usage(problem, requested = false) {
|
|
|
18419
18627
|
crew reap [route] kill orphaned servers, drop worktrees for closed tickets
|
|
18420
18628
|
crew drop [route] NNN remove a merged ticket's worktree and branch
|
|
18421
18629
|
crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
|
|
18422
|
-
crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]
|
|
18630
|
+
crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]
|
|
18423
18631
|
crew periscope up --resume NNN restart the preview an idle periscope's server was stopped for
|
|
18424
18632
|
file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
|
|
18425
|
-
crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]
|
|
18633
|
+
crew periscope down [[route] NNN] --keep|--discard [--allow-empty] [--json]
|
|
18426
18634
|
end a periscope: commit and push its branch (ticket back to new), or throw it away (alias: end)
|
|
18427
18635
|
crew periscope list [route] [--json] the live periscopes on this ship, their idle minutes and whether each server is listening
|
|
18636
|
+
crew shell-init [--shell zsh|bash|fish] print the periscope shell function (install: eval "$(crew shell-init)")
|
|
18428
18637
|
crew worktree [route] NNN cut (or refresh) this run's worktree for a ticket from the remote; prints its path
|
|
18429
18638
|
crew unassign [route] NNN hand back a session's ticket \u2014 clears assignee, next cycle picks it up
|
|
18430
18639
|
crew sync [route] fast-forward the checkout and its worktrees from the remote
|
|
@@ -18530,6 +18739,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
18530
18739
|
"run",
|
|
18531
18740
|
"sample-ticket",
|
|
18532
18741
|
"selftest",
|
|
18742
|
+
"shell-init",
|
|
18533
18743
|
"ship",
|
|
18534
18744
|
"skills",
|
|
18535
18745
|
"status",
|
|
@@ -18541,6 +18751,16 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
18541
18751
|
"worktree"
|
|
18542
18752
|
]);
|
|
18543
18753
|
if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
|
|
18754
|
+
if (command === "shell-init") {
|
|
18755
|
+
const asked = value("shell");
|
|
18756
|
+
if (asked !== void 0 && !SHELLS.includes(asked)) {
|
|
18757
|
+
process.stderr.write(`crew shell-init: unknown shell "${asked}" (have: ${SHELLS.join(", ")})
|
|
18758
|
+
`);
|
|
18759
|
+
process.exit(2);
|
|
18760
|
+
}
|
|
18761
|
+
process.stdout.write(shellInitScript(shellFor(asked, process.env.SHELL)));
|
|
18762
|
+
process.exit(0);
|
|
18763
|
+
}
|
|
18544
18764
|
function runReposCheck(dir, ship) {
|
|
18545
18765
|
const report = checkRepoSpec(dir, { ship, strict: flag("strict") });
|
|
18546
18766
|
if (flag("json")) {
|
|
@@ -18699,7 +18919,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18699
18919
|
} else {
|
|
18700
18920
|
const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
18701
18921
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
18702
|
-
if (configless && !
|
|
18922
|
+
if (configless && !existsSync37(findConfigFile(CREW_HOME))) {
|
|
18703
18923
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
18704
18924
|
} else {
|
|
18705
18925
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -18937,8 +19157,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
18937
19157
|
}
|
|
18938
19158
|
if (host === "linux" && hasSystemd) {
|
|
18939
19159
|
const label = labelFor(CREW_HOME, "run");
|
|
18940
|
-
const staleTimer =
|
|
18941
|
-
if (
|
|
19160
|
+
const staleTimer = join41(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
19161
|
+
if (existsSync37(staleTimer)) {
|
|
18942
19162
|
process.stdout.write(
|
|
18943
19163
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
18944
19164
|
`
|
|
@@ -18965,7 +19185,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18965
19185
|
const tracker = new Tracker(c, cfg.ship);
|
|
18966
19186
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
18967
19187
|
for (const r of await resolvedRepos(c)) {
|
|
18968
|
-
if (!
|
|
19188
|
+
if (!existsSync37(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
18969
19189
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
18970
19190
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
18971
19191
|
}
|
|
@@ -18983,7 +19203,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18983
19203
|
}
|
|
18984
19204
|
async function doctorFix(o) {
|
|
18985
19205
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
18986
|
-
const unitExists = (p) =>
|
|
19206
|
+
const unitExists = (p) => existsSync37(p);
|
|
18987
19207
|
const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
18988
19208
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
18989
19209
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -19000,7 +19220,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
19000
19220
|
for (const a of plan.actions) {
|
|
19001
19221
|
if (a.kind === "enable") {
|
|
19002
19222
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
19003
|
-
|
|
19223
|
+
writeFileSync25(cfg.configFile, planEnable(readFileSync33(cfg.configFile, "utf8"), a.route));
|
|
19004
19224
|
enabledSound.add(a.route);
|
|
19005
19225
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
19006
19226
|
} else {
|
|
@@ -20104,7 +20324,7 @@ ${cu.tail.trimEnd()}
|
|
|
20104
20324
|
process.exit(1);
|
|
20105
20325
|
}
|
|
20106
20326
|
requireArmed("run the daemon loop");
|
|
20107
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
20327
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join41(CREW_HOME, "src"), join41(CREW_HOME, "dist"), join41(CREW_HOME, "bin")];
|
|
20108
20328
|
const configPaths = daemonConfigPaths(cfg);
|
|
20109
20329
|
watchPaths.push(...configPaths);
|
|
20110
20330
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -20333,10 +20553,10 @@ ${cu.tail.trimEnd()}
|
|
|
20333
20553
|
`);
|
|
20334
20554
|
if (!dryRun) {
|
|
20335
20555
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
20336
|
-
const raw =
|
|
20556
|
+
const raw = existsSync37(resolvedPath) ? JSON.parse(readFileSync33(resolvedPath, "utf8")) : {};
|
|
20337
20557
|
raw.agentSkills = result.agentSkills;
|
|
20338
|
-
|
|
20339
|
-
|
|
20558
|
+
mkdirSync24(dirOf2(resolvedPath), { recursive: true });
|
|
20559
|
+
writeFileSync25(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
20340
20560
|
`);
|
|
20341
20561
|
}
|
|
20342
20562
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -20359,7 +20579,7 @@ ${cu.tail.trimEnd()}
|
|
|
20359
20579
|
throw e;
|
|
20360
20580
|
}
|
|
20361
20581
|
const oldName = cfg.ship.name;
|
|
20362
|
-
|
|
20582
|
+
writeFileSync25(cfg.configFile, setShipNameInYaml(readFileSync33(cfg.configFile, "utf8"), newName));
|
|
20363
20583
|
cfg.ship.name = newName;
|
|
20364
20584
|
let renameMachineId;
|
|
20365
20585
|
try {
|
|
@@ -20406,7 +20626,7 @@ ${cu.tail.trimEnd()}
|
|
|
20406
20626
|
process.exit(2);
|
|
20407
20627
|
};
|
|
20408
20628
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
20409
|
-
const configText =
|
|
20629
|
+
const configText = readFileSync33(cfg.configFile, "utf8");
|
|
20410
20630
|
if (sub === "check") {
|
|
20411
20631
|
try {
|
|
20412
20632
|
const routeArg = value("route") ?? positional[2];
|
|
@@ -20437,8 +20657,8 @@ ${cu.tail.trimEnd()}
|
|
|
20437
20657
|
try {
|
|
20438
20658
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
20439
20659
|
const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
|
|
20440
|
-
if (!
|
|
20441
|
-
const files = { exists: (r) =>
|
|
20660
|
+
if (!existsSync37(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
20661
|
+
const files = { exists: (r) => existsSync37(resolvePath(dir, r)), list: (r) => {
|
|
20442
20662
|
try {
|
|
20443
20663
|
return readdirSync17(resolvePath(dir, r));
|
|
20444
20664
|
} catch {
|
|
@@ -20446,7 +20666,7 @@ ${cu.tail.trimEnd()}
|
|
|
20446
20666
|
}
|
|
20447
20667
|
}, read: (r) => {
|
|
20448
20668
|
try {
|
|
20449
|
-
return
|
|
20669
|
+
return readFileSync33(resolvePath(dir, r), "utf8");
|
|
20450
20670
|
} catch {
|
|
20451
20671
|
return void 0;
|
|
20452
20672
|
}
|
|
@@ -20488,19 +20708,19 @@ ${cu.tail.trimEnd()}
|
|
|
20488
20708
|
let cleared;
|
|
20489
20709
|
if (clear2) {
|
|
20490
20710
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
20491
|
-
if (plan.removed.length > 0)
|
|
20711
|
+
if (plan.removed.length > 0) writeFileSync25(cfg.configFile, plan.text);
|
|
20492
20712
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
20493
20713
|
}
|
|
20494
20714
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
20495
20715
|
if (write === "route") {
|
|
20496
20716
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
20497
|
-
|
|
20717
|
+
writeFileSync25(cfg.configFile, plan.text);
|
|
20498
20718
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
20499
20719
|
} else if (write === "repo") {
|
|
20500
20720
|
const have = existingRepoConfig(files);
|
|
20501
20721
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
20502
20722
|
const file = resolvePath(dir, ".crew.yaml");
|
|
20503
|
-
|
|
20723
|
+
writeFileSync25(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
20504
20724
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
20505
20725
|
}
|
|
20506
20726
|
if (flag("json")) {
|
|
@@ -20556,7 +20776,7 @@ ${cu.tail.trimEnd()}
|
|
|
20556
20776
|
if (sub === "list" && !routeName) usage2();
|
|
20557
20777
|
if (sub === "add") {
|
|
20558
20778
|
try {
|
|
20559
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
20779
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync37(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
20560
20780
|
routeName = a.route;
|
|
20561
20781
|
addPath = a.path;
|
|
20562
20782
|
addName = a.name;
|
|
@@ -20585,16 +20805,16 @@ ${cu.tail.trimEnd()}
|
|
|
20585
20805
|
}
|
|
20586
20806
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
20587
20807
|
let resolvedRemotes;
|
|
20588
|
-
if (
|
|
20808
|
+
if (existsSync37(resolvedFile)) {
|
|
20589
20809
|
try {
|
|
20590
|
-
resolvedRemotes = JSON.parse(
|
|
20810
|
+
resolvedRemotes = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
20591
20811
|
} catch {
|
|
20592
20812
|
}
|
|
20593
20813
|
}
|
|
20594
20814
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
20595
20815
|
const text = configText;
|
|
20596
20816
|
const base = dirname20(cfg.configFile);
|
|
20597
|
-
const isCheckout = (d) =>
|
|
20817
|
+
const isCheckout = (d) => existsSync37(`${d}/.git`);
|
|
20598
20818
|
try {
|
|
20599
20819
|
if (sub === "list") {
|
|
20600
20820
|
if (flag("json")) {
|
|
@@ -20604,11 +20824,11 @@ ${cu.tail.trimEnd()}
|
|
|
20604
20824
|
base,
|
|
20605
20825
|
resolved: resolvedRemotes,
|
|
20606
20826
|
originOf,
|
|
20607
|
-
exists: (d) =>
|
|
20827
|
+
exists: (d) => existsSync37(d),
|
|
20608
20828
|
isCheckout,
|
|
20609
20829
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
20610
20830
|
try {
|
|
20611
|
-
return Object.keys(parseRepoConfig(
|
|
20831
|
+
return Object.keys(parseRepoConfig(readFileSync33(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
20612
20832
|
} catch {
|
|
20613
20833
|
return false;
|
|
20614
20834
|
}
|
|
@@ -20625,7 +20845,7 @@ ${cu.tail.trimEnd()}
|
|
|
20625
20845
|
for (const r of repos) {
|
|
20626
20846
|
const origin2 = originOf(r.dir);
|
|
20627
20847
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
20628
|
-
const note = !
|
|
20848
|
+
const note = !existsSync37(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
20629
20849
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
20630
20850
|
`);
|
|
20631
20851
|
}
|
|
@@ -20637,9 +20857,9 @@ ${cu.tail.trimEnd()}
|
|
|
20637
20857
|
` : `${human}
|
|
20638
20858
|
`);
|
|
20639
20859
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
20640
|
-
if (
|
|
20860
|
+
if (existsSync37(plan2.target) && (existsSync37(`${plan2.target}/.git`) || readdirSync17(plan2.target).length > 0)) {
|
|
20641
20861
|
throw new ReposError(
|
|
20642
|
-
|
|
20862
|
+
existsSync37(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
|
|
20643
20863
|
);
|
|
20644
20864
|
}
|
|
20645
20865
|
if (dryRun) {
|
|
@@ -20657,14 +20877,14 @@ ${cu.tail.trimEnd()}
|
|
|
20657
20877
|
throw new ReposError(`${e.message}${authHint}
|
|
20658
20878
|
crew.yaml was not changed`);
|
|
20659
20879
|
}
|
|
20660
|
-
|
|
20880
|
+
writeFileSync25(cfg.configFile, plan2.text);
|
|
20661
20881
|
if (plan2.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan2.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
|
|
20662
20882
|
`);
|
|
20663
20883
|
say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
|
|
20664
20884
|
break;
|
|
20665
20885
|
}
|
|
20666
20886
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
20667
|
-
const top =
|
|
20887
|
+
const top = existsSync37(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
20668
20888
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
20669
20889
|
const origin = originOf(top);
|
|
20670
20890
|
const plan = planRepoAdd({
|
|
@@ -20716,15 +20936,15 @@ ${cu.tail.trimEnd()}
|
|
|
20716
20936
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
20717
20937
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
20718
20938
|
`);
|
|
20719
|
-
if (
|
|
20720
|
-
const raw = JSON.parse(
|
|
20939
|
+
if (existsSync37(resolvedFile)) {
|
|
20940
|
+
const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
20721
20941
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
20722
20942
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
20723
|
-
|
|
20943
|
+
writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
20724
20944
|
`);
|
|
20725
20945
|
}
|
|
20726
20946
|
}
|
|
20727
|
-
|
|
20947
|
+
writeFileSync25(cfg.configFile, plan.text);
|
|
20728
20948
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
20729
20949
|
`);
|
|
20730
20950
|
} catch (e) {
|
|
@@ -21091,7 +21311,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21091
21311
|
process.exit(2);
|
|
21092
21312
|
}
|
|
21093
21313
|
if (found.shipNameAdopted && !dryRun) {
|
|
21094
|
-
|
|
21314
|
+
writeFileSync25(cfg.configFile, setShipNameInYaml(readFileSync33(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
21095
21315
|
cfg.ship.name = found.shipNameAdopted;
|
|
21096
21316
|
}
|
|
21097
21317
|
if (found.shipRow) {
|
|
@@ -21134,10 +21354,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21134
21354
|
}
|
|
21135
21355
|
}
|
|
21136
21356
|
let previous;
|
|
21137
|
-
const hadPreviousFile =
|
|
21357
|
+
const hadPreviousFile = existsSync37(resolvedPath);
|
|
21138
21358
|
if (hadPreviousFile) {
|
|
21139
21359
|
try {
|
|
21140
|
-
previous = JSON.parse(
|
|
21360
|
+
previous = JSON.parse(readFileSync33(resolvedPath, "utf8"));
|
|
21141
21361
|
} catch {
|
|
21142
21362
|
}
|
|
21143
21363
|
}
|
|
@@ -21266,10 +21486,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21266
21486
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
21267
21487
|
`);
|
|
21268
21488
|
} else {
|
|
21269
|
-
|
|
21489
|
+
mkdirSync24(dirname20(resolvedPath), { recursive: true });
|
|
21270
21490
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
21271
21491
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
21272
|
-
|
|
21492
|
+
writeFileSync25(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
21273
21493
|
`);
|
|
21274
21494
|
renameSync3(tmpPath, resolvedPath);
|
|
21275
21495
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -21315,8 +21535,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21315
21535
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
21316
21536
|
`);
|
|
21317
21537
|
} else {
|
|
21318
|
-
|
|
21319
|
-
|
|
21538
|
+
mkdirSync24(dirname20(apiKeyFile), { recursive: true });
|
|
21539
|
+
writeFileSync25(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
21320
21540
|
`, { mode: 384 });
|
|
21321
21541
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
21322
21542
|
`);
|
|
@@ -21866,8 +22086,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21866
22086
|
case "periscope": {
|
|
21867
22087
|
const subP = positional[1];
|
|
21868
22088
|
if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
|
|
21869
|
-
process.stderr.write("crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]\n");
|
|
21870
|
-
process.stderr.write("crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]\n");
|
|
22089
|
+
process.stderr.write("crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]\n");
|
|
22090
|
+
process.stderr.write("crew periscope down [[route] NNN] --keep|--discard [--allow-empty] [--json]\n");
|
|
21871
22091
|
process.stderr.write("crew periscope list [route] [--json]\n");
|
|
21872
22092
|
process.exit(2);
|
|
21873
22093
|
}
|
|
@@ -21912,11 +22132,27 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21912
22132
|
`);
|
|
21913
22133
|
break;
|
|
21914
22134
|
}
|
|
21915
|
-
const
|
|
22135
|
+
const real = (p) => {
|
|
22136
|
+
try {
|
|
22137
|
+
return realpathSync3(p);
|
|
22138
|
+
} catch {
|
|
22139
|
+
return p;
|
|
22140
|
+
}
|
|
22141
|
+
};
|
|
22142
|
+
const gathered = await Promise.all(routesP.map(async (rt) => ({ rt, rows: await gather(rt) })));
|
|
22143
|
+
const number = downArgs.number ?? (() => {
|
|
22144
|
+
const here = real(process.cwd());
|
|
22145
|
+
for (const g of gathered) {
|
|
22146
|
+
const hit = rowForCwd(g.rows.map((r) => ({ ...r, worktree: real(r.worktree) })), here);
|
|
22147
|
+
if (hit) return hit.number;
|
|
22148
|
+
}
|
|
22149
|
+
throw new PeriscopeError("not inside a periscope worktree \u2014 name the ticket: crew periscope down NNN --keep|--discard");
|
|
22150
|
+
})();
|
|
22151
|
+
const a = { ...downArgs, number };
|
|
21916
22152
|
const hits = [];
|
|
21917
|
-
for (const
|
|
21918
|
-
const rows =
|
|
21919
|
-
if (rows.length) hits.push({ rt, rows });
|
|
22153
|
+
for (const g of gathered) {
|
|
22154
|
+
const rows = g.rows.filter((r) => r.number === a.number);
|
|
22155
|
+
if (rows.length) hits.push({ rt: g.rt, rows });
|
|
21920
22156
|
}
|
|
21921
22157
|
if (hits.length !== 1) {
|
|
21922
22158
|
throw new PeriscopeError(hits.length === 0 ? `no periscope for ticket ${a.number} on this ship` : `ticket ${a.number} has periscopes on more than one route (${hits.map((h) => h.rt.route).join(", ")}) \u2014 name the route`);
|
|
@@ -21950,11 +22186,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21950
22186
|
},
|
|
21951
22187
|
readTicket: async (id) => {
|
|
21952
22188
|
const t = await trD.ticket(id);
|
|
21953
|
-
return { description: String(t.description ?? ""), hasProject: !!t[cD.columns.slice], hasRepo: !!t[cD.columns.repo] };
|
|
22189
|
+
return { description: String(t.description ?? ""), hasProject: !!t[cD.columns.slice], hasRepo: !!t[cD.columns.repo], isDraft: t.status === cD.statuses.draft };
|
|
21954
22190
|
},
|
|
22191
|
+
priorHold: (id) => readPeriscopePrior(cfg.ship.stateDir, id),
|
|
22192
|
+
remoteHasBranch: (_wt, branch) => remoteBranchExists(repoD.dir, remote, branch),
|
|
21955
22193
|
updateTicket: async (id, p) => {
|
|
21956
22194
|
const patch2 = {};
|
|
21957
|
-
if (p.status === "
|
|
22195
|
+
if (p.status === "released") {
|
|
22196
|
+
patch2[cD.columns.assignee] = null;
|
|
22197
|
+
if ((await trD.authorshipProbe()).issueHeld) patch2[cD.columns.heldBy] = null;
|
|
22198
|
+
if (p.description !== void 0) patch2.description = p.description;
|
|
22199
|
+
} else if (p.status === "new") {
|
|
21958
22200
|
patch2[cD.columns.status] = "new";
|
|
21959
22201
|
patch2[cD.columns.assignee] = null;
|
|
21960
22202
|
if (p.description !== void 0) patch2.description = p.description;
|
|
@@ -21979,13 +22221,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21979
22221
|
if (deleteBranch) gitOk(repoD.dir, ["branch", "-D", branch]);
|
|
21980
22222
|
},
|
|
21981
22223
|
setRowState: (id, state2) => trD.updatePeriscope(id, { state: state2 }),
|
|
21982
|
-
forgetLocal: (id) =>
|
|
22224
|
+
forgetLocal: (id) => {
|
|
22225
|
+
removePeriscopeSidecar(cfg.ship.stateDir, id);
|
|
22226
|
+
removePeriscopePrior(cfg.ship.stateDir, id);
|
|
22227
|
+
},
|
|
21983
22228
|
say
|
|
21984
22229
|
};
|
|
21985
22230
|
if (dryRun) {
|
|
21986
22231
|
say("dry-run", `would end the periscope for ${rowsD[0].ticket} (${a.mode})`);
|
|
21987
22232
|
break;
|
|
21988
22233
|
}
|
|
22234
|
+
process.chdir(repoD.dir);
|
|
21989
22235
|
const res = await runPeriscopeDown(depsD, a);
|
|
21990
22236
|
if (jsonP) process.stdout.write(`${JSON.stringify({ event: "done", ...res })}
|
|
21991
22237
|
`);
|
|
@@ -22024,7 +22270,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22024
22270
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
22025
22271
|
const resumed = await runPeriscopeResume({
|
|
22026
22272
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
22027
|
-
worktreeExists: (p) =>
|
|
22273
|
+
worktreeExists: (p) => existsSync37(p),
|
|
22028
22274
|
caps: periscopeCaps(cfg.ship),
|
|
22029
22275
|
runHook: async (name, cwd, env) => {
|
|
22030
22276
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -22043,7 +22289,19 @@ ${resumed.url}
|
|
|
22043
22289
|
`);
|
|
22044
22290
|
break;
|
|
22045
22291
|
}
|
|
22046
|
-
const
|
|
22292
|
+
const parsedUp = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
|
|
22293
|
+
let a;
|
|
22294
|
+
if (parsedUp.repo !== void 0) a = { ...parsedUp, repo: parsedUp.repo };
|
|
22295
|
+
else {
|
|
22296
|
+
const here = locateDir(cfg, process.cwd(), (d) => gitOk(d, ["rev-parse", "--path-format=absolute", "--git-common-dir"]) ?? void 0);
|
|
22297
|
+
if (!here) {
|
|
22298
|
+
const where = cfg.routes.flatMap((r) => reposOf(r).map((x) => ` ${x.name}: ${x.dir}`));
|
|
22299
|
+
throw new PeriscopeError(`run it from inside a configured checkout, or name the repository (crew periscope up REPO). Configured checkouts:
|
|
22300
|
+
${where.join("\n")}`);
|
|
22301
|
+
}
|
|
22302
|
+
a = { ...parsedUp, repo: here.repo.name, route: parsedUp.route ?? here.route.route };
|
|
22303
|
+
}
|
|
22304
|
+
const terminal = !!process.stdin.isTTY && !!process.stdout.isTTY && !jsonP;
|
|
22047
22305
|
const candidatesP = a.route ? [findRoute(cfg, a.route)] : cfg.routes.filter((r) => r.enabled);
|
|
22048
22306
|
const servedP = [];
|
|
22049
22307
|
for (const cand of candidatesP) {
|
|
@@ -22075,10 +22333,11 @@ ${resumed.url}
|
|
|
22075
22333
|
return h.unsafe ? h.detail : void 0;
|
|
22076
22334
|
},
|
|
22077
22335
|
askTopic: async () => {
|
|
22078
|
-
if (!
|
|
22336
|
+
if (!terminal) return void 0;
|
|
22079
22337
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
22080
22338
|
try {
|
|
22081
|
-
|
|
22339
|
+
const answer = (await rl.question(`Periscope topic for ${a.repo} (blank for "${defaultTopic(a.repo, /* @__PURE__ */ new Date())}"): `)).trim();
|
|
22340
|
+
return answer || void 0;
|
|
22082
22341
|
} finally {
|
|
22083
22342
|
rl.close();
|
|
22084
22343
|
}
|
|
@@ -22105,10 +22364,10 @@ ${resumed.url}
|
|
|
22105
22364
|
},
|
|
22106
22365
|
prepare: async (path) => {
|
|
22107
22366
|
for (const f of repoP.config.worktrees.copy) {
|
|
22108
|
-
const src =
|
|
22109
|
-
if (!
|
|
22110
|
-
|
|
22111
|
-
copyFileSync2(src,
|
|
22367
|
+
const src = join41(repoP.dir, f);
|
|
22368
|
+
if (!existsSync37(src)) continue;
|
|
22369
|
+
mkdirSync24(dirOf2(join41(path, f)), { recursive: true });
|
|
22370
|
+
copyFileSync2(src, join41(path, f));
|
|
22112
22371
|
}
|
|
22113
22372
|
const setup = repoP.config.hooks.setup;
|
|
22114
22373
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -22132,22 +22391,73 @@ ${resumed.url}
|
|
|
22132
22391
|
const closed = cP.statuses.resolved.includes("closed_obsolete") ? "closed_obsolete" : cP.statuses.wontFix;
|
|
22133
22392
|
await trackerP.updateTicket(ticketId, { [cP.columns.status]: closed, [cP.columns.resolutionNote]: note });
|
|
22134
22393
|
},
|
|
22394
|
+
existing: {
|
|
22395
|
+
load: async (number) => {
|
|
22396
|
+
const t = (await trackerP.ticketsByNumbers([number])).find((x) => String(x[cP.columns.key]).replace(/^\D+/, "").replace(/^0+(?=\d)/, "") === number);
|
|
22397
|
+
if (!t) return void 0;
|
|
22398
|
+
return {
|
|
22399
|
+
id: t.id,
|
|
22400
|
+
tag: t.issue_tag ?? t.issue_id,
|
|
22401
|
+
issueId: t.issue_id,
|
|
22402
|
+
prefix: t.project_issue_prefix,
|
|
22403
|
+
status: t.status,
|
|
22404
|
+
closed: cP.statuses.resolved.includes(t.status) || t.status === cP.statuses.wontFix,
|
|
22405
|
+
repoId: t[cP.columns.repo] ?? null,
|
|
22406
|
+
assignee: t[cP.columns.assignee] ?? null,
|
|
22407
|
+
heldByShipId: t[cP.columns.heldBy] ?? null
|
|
22408
|
+
};
|
|
22409
|
+
},
|
|
22410
|
+
repoId: repoP.id ?? null,
|
|
22411
|
+
repoName: a.repo,
|
|
22412
|
+
pairId,
|
|
22413
|
+
shipId: (await trackerP.authorshipProbe()).shipId,
|
|
22414
|
+
agentHolder: async (assigneeId) => {
|
|
22415
|
+
const row = crewP.find((r) => r.id === assigneeId);
|
|
22416
|
+
return row?.agent_id ? row.name ?? assigneeId : void 0;
|
|
22417
|
+
},
|
|
22418
|
+
hasLiveRun: (ticketRecordId) => liveRuns(cfg.ship.stateDir).some((r) => r.ticketRecordId === ticketRecordId),
|
|
22419
|
+
hold: async (id) => {
|
|
22420
|
+
const auth = await trackerP.authorshipProbe();
|
|
22421
|
+
await trackerP.updateTicket(id, {
|
|
22422
|
+
[cP.columns.assignee]: pairId,
|
|
22423
|
+
...auth.issueHeld && auth.shipId ? { [cP.columns.heldBy]: auth.shipId } : {}
|
|
22424
|
+
});
|
|
22425
|
+
},
|
|
22426
|
+
restore: async (id, assignee) => {
|
|
22427
|
+
const auth = await trackerP.authorshipProbe();
|
|
22428
|
+
await trackerP.updateTicket(id, { [cP.columns.assignee]: assignee, ...auth.issueHeld ? { [cP.columns.heldBy]: null } : {} });
|
|
22429
|
+
},
|
|
22430
|
+
remember: (id, prior) => writePeriscopePrior(cfg.ship.stateDir, id, prior)
|
|
22431
|
+
},
|
|
22135
22432
|
now: () => /* @__PURE__ */ new Date(),
|
|
22136
22433
|
host: a.host ?? "localhost",
|
|
22137
22434
|
say
|
|
22138
22435
|
};
|
|
22139
22436
|
if (dryRun) {
|
|
22140
22437
|
await periscopePreflight(deps, a.repo);
|
|
22141
|
-
|
|
22438
|
+
if (a.issue) {
|
|
22439
|
+
const t = await deps.existing.load(a.issue);
|
|
22440
|
+
if (!t) throw new PeriscopeError(`no ticket numbered ${a.issue} on this route`);
|
|
22441
|
+
const refused = await existingRefusal(deps.existing, t);
|
|
22442
|
+
if (refused) throw new PeriscopeError(refused);
|
|
22443
|
+
say("dry-run", `would hold ${t.tag} (status ${t.status} stays) for ${pairId}, cut or reuse its worktree for ${a.repo}, run setup${a.isolate ? ", isolate" : ""}, ports, handoff and preview on ${deps.host}, and record a Periscopes row`);
|
|
22444
|
+
break;
|
|
22445
|
+
}
|
|
22446
|
+
say("dry-run", `would file "Periscope: ${a.topic ?? defaultTopic(a.repo, /* @__PURE__ */ new Date())}" as ${cP.statuses.draft} for ${pairId}, cut a worktree for ${a.repo}, run setup${a.isolate ? ", isolate" : ""}, ports, handoff and preview on ${deps.host}, and record a Periscopes row (expires after ${caps.idleMinutes} minutes idle)`);
|
|
22142
22447
|
break;
|
|
22143
22448
|
}
|
|
22144
22449
|
const res = await runPeriscopeUp(deps, a);
|
|
22145
22450
|
if (jsonP) process.stdout.write(`${JSON.stringify({ event: "done", ...res })}
|
|
22146
22451
|
`);
|
|
22147
|
-
else
|
|
22148
|
-
|
|
22149
|
-
|
|
22452
|
+
else {
|
|
22453
|
+
process.stdout.write(upOutput(res, terminal));
|
|
22454
|
+
const opener = openerFor(process.platform);
|
|
22455
|
+
if (opener && shouldOpen({ open: flag("open"), noOpen: flag("no-open"), json: jsonP, terminal })) {
|
|
22456
|
+
const r = spawnSync7(opener, [res.url], { stdio: "ignore" });
|
|
22457
|
+
if (r.error || r.status !== 0) process.stderr.write(`crew periscope: could not open ${res.url} with ${opener}
|
|
22150
22458
|
`);
|
|
22459
|
+
}
|
|
22460
|
+
}
|
|
22151
22461
|
} catch (e) {
|
|
22152
22462
|
if (e instanceof PeriscopeError || e instanceof ConfigError) {
|
|
22153
22463
|
process.stderr.write(`crew periscope: ${e.message}
|
|
@@ -22332,7 +22642,7 @@ ${res.url}
|
|
|
22332
22642
|
};
|
|
22333
22643
|
const droppedRoutes = [];
|
|
22334
22644
|
try {
|
|
22335
|
-
const rawCfg = parseYaml2(
|
|
22645
|
+
const rawCfg = parseYaml2(readFileSync33(cfg.configFile, "utf8"));
|
|
22336
22646
|
for (const r of rawCfg?.routes ?? []) {
|
|
22337
22647
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
22338
22648
|
try {
|
|
@@ -22374,8 +22684,8 @@ ${res.url}
|
|
|
22374
22684
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
22375
22685
|
break;
|
|
22376
22686
|
}
|
|
22377
|
-
|
|
22378
|
-
|
|
22687
|
+
mkdirSync24(cfg.ship.stateDir, { recursive: true });
|
|
22688
|
+
writeFileSync25(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
22379
22689
|
`);
|
|
22380
22690
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
22381
22691
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -22426,7 +22736,7 @@ ${res.url}
|
|
|
22426
22736
|
}
|
|
22427
22737
|
case "log": {
|
|
22428
22738
|
try {
|
|
22429
|
-
const text =
|
|
22739
|
+
const text = readFileSync33(cfg.ship.logFile, "utf8").split("\n");
|
|
22430
22740
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
22431
22741
|
`);
|
|
22432
22742
|
} catch {
|
|
@@ -22449,7 +22759,7 @@ ${res.url}
|
|
|
22449
22759
|
};
|
|
22450
22760
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
22451
22761
|
const tracker = new Tracker(route, cfg.ship);
|
|
22452
|
-
const configText =
|
|
22762
|
+
const configText = readFileSync33(cfg.configFile, "utf8");
|
|
22453
22763
|
const cfgBase = dirname20(cfg.configFile);
|
|
22454
22764
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
22455
22765
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
@@ -22478,7 +22788,7 @@ ${res.url}
|
|
|
22478
22788
|
}
|
|
22479
22789
|
if (configured.some((r) => r.dir === dir)) {
|
|
22480
22790
|
try {
|
|
22481
|
-
|
|
22791
|
+
writeFileSync25(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
22482
22792
|
} catch (e) {
|
|
22483
22793
|
if (e instanceof ReposError) fail(e.message);
|
|
22484
22794
|
throw e;
|
|
@@ -22486,29 +22796,29 @@ ${res.url}
|
|
|
22486
22796
|
}
|
|
22487
22797
|
if (row) {
|
|
22488
22798
|
await tracker.deleteRepoRow(row.id);
|
|
22489
|
-
if (
|
|
22490
|
-
const raw = JSON.parse(
|
|
22799
|
+
if (existsSync37(resolvedFile)) {
|
|
22800
|
+
const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
22491
22801
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
22492
22802
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
22493
|
-
|
|
22803
|
+
writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22494
22804
|
`);
|
|
22495
22805
|
}
|
|
22496
22806
|
}
|
|
22497
|
-
if (
|
|
22807
|
+
if (existsSync37(join41(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync17(dir, { recursive: true, force: true });
|
|
22498
22808
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
22499
22809
|
break;
|
|
22500
22810
|
}
|
|
22501
22811
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
22502
|
-
const fresh = !
|
|
22503
|
-
if (
|
|
22812
|
+
const fresh = !existsSync37(join41(dir, ".git"));
|
|
22813
|
+
if (existsSync37(dir) && fresh && readdirSync17(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
22504
22814
|
if (dryRun) {
|
|
22505
22815
|
say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
|
|
22506
22816
|
break;
|
|
22507
22817
|
}
|
|
22508
22818
|
if (fresh) {
|
|
22509
|
-
|
|
22510
|
-
|
|
22511
|
-
|
|
22819
|
+
mkdirSync24(dir, { recursive: true });
|
|
22820
|
+
writeFileSync25(join41(dir, "README.md"), SAMPLE_README);
|
|
22821
|
+
writeFileSync25(join41(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
22512
22822
|
const g = (...a) => {
|
|
22513
22823
|
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
22514
22824
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -22526,17 +22836,17 @@ ${res.url}
|
|
|
22526
22836
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
22527
22837
|
});
|
|
22528
22838
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
22529
|
-
if (
|
|
22530
|
-
const raw = JSON.parse(
|
|
22839
|
+
if (existsSync37(resolvedFile)) {
|
|
22840
|
+
const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
22531
22841
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
22532
22842
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
22533
|
-
|
|
22843
|
+
writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22534
22844
|
`);
|
|
22535
22845
|
}
|
|
22536
22846
|
}
|
|
22537
22847
|
if (!configured.some((r) => r.dir === dir)) {
|
|
22538
22848
|
try {
|
|
22539
|
-
|
|
22849
|
+
writeFileSync25(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
22540
22850
|
} catch (e) {
|
|
22541
22851
|
if (e instanceof ReposError) fail(e.message);
|
|
22542
22852
|
throw e;
|
|
@@ -22871,7 +23181,7 @@ watermark: ${state.watermark()}
|
|
|
22871
23181
|
}
|
|
22872
23182
|
for (const r of repos) {
|
|
22873
23183
|
const remote = r.config.branch.remote;
|
|
22874
|
-
if (
|
|
23184
|
+
if (existsSync37(r.dir)) {
|
|
22875
23185
|
pushAuth.push({
|
|
22876
23186
|
route: c.route,
|
|
22877
23187
|
repo: r.name,
|
|
@@ -22948,7 +23258,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22948
23258
|
}
|
|
22949
23259
|
try {
|
|
22950
23260
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
22951
|
-
const evSize =
|
|
23261
|
+
const evSize = existsSync37(evFile) ? statSync11(evFile).size : 0;
|
|
22952
23262
|
process.stdout.write(
|
|
22953
23263
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
22954
23264
|
` : "events log: ok\n"
|
|
@@ -22959,7 +23269,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22959
23269
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
22960
23270
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
22961
23271
|
try {
|
|
22962
|
-
return
|
|
23272
|
+
return readFileSync33(p, "utf8");
|
|
22963
23273
|
} catch {
|
|
22964
23274
|
return null;
|
|
22965
23275
|
}
|
package/package.json
CHANGED