@tablation/crew 0.59.0 → 0.60.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/README.md +1 -0
- package/dist/cli.js +274 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -255,6 +255,7 @@ crew inbox [--member NAME] your tickets across every workspace
|
|
|
255
255
|
crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
|
|
256
256
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
|
|
257
257
|
crew repos list ROUTE the checkouts a route has, and the tracker Repos row each matches
|
|
258
|
+
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo writes them, --json)
|
|
258
259
|
crew install [--replace] write and load this platform's scheduler unit (macOS: clears a crew installed from another path)
|
|
259
260
|
crew uninstall [--all] unload and remove it (--all: every crew launchd unit, whichever path it came from)
|
|
260
261
|
```
|
package/dist/cli.js
CHANGED
|
@@ -5211,7 +5211,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
5211
5211
|
"tail",
|
|
5212
5212
|
"ticket",
|
|
5213
5213
|
"workspace",
|
|
5214
|
-
"workspace-id"
|
|
5214
|
+
"workspace-id",
|
|
5215
|
+
"write"
|
|
5215
5216
|
]);
|
|
5216
5217
|
function positionalArgs(argv2) {
|
|
5217
5218
|
const out = [];
|
|
@@ -12659,77 +12660,12 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
12659
12660
|
};
|
|
12660
12661
|
}
|
|
12661
12662
|
|
|
12662
|
-
// src/
|
|
12663
|
-
import {
|
|
12664
|
-
var SAMPLE_REPO = "crew-sample";
|
|
12665
|
-
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
12666
|
-
var SAMPLE_MARKER = "sample: true";
|
|
12667
|
-
var SAMPLE_CREW_YAML = `version: 1
|
|
12668
|
-
platform: unix
|
|
12669
|
-
branch:
|
|
12670
|
-
base: main
|
|
12671
|
-
hooks:
|
|
12672
|
-
test: "true"
|
|
12673
|
-
build: "true"
|
|
12674
|
-
release:
|
|
12675
|
-
mode: integrate
|
|
12676
|
-
versioning: none
|
|
12677
|
-
changelog: false
|
|
12678
|
-
`;
|
|
12679
|
-
var SAMPLE_README = `# crew-sample
|
|
12680
|
-
|
|
12681
|
-
A throwaway repository created by \`crew sample-ticket\`. It exists so the crew
|
|
12682
|
-
has something safe to work on: its test and build hooks always pass, it has no
|
|
12683
|
-
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
12684
|
-
`;
|
|
12685
|
-
function sampleDir(existing, reposBase, workspace) {
|
|
12686
|
-
if (existing.length > 0) return join20(dirname12(existing[0]), SAMPLE_REPO);
|
|
12687
|
-
return join20(reposBase, workspace, SAMPLE_REPO);
|
|
12688
|
-
}
|
|
12689
|
-
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
12690
|
-
function sampleDescription() {
|
|
12691
|
-
return [
|
|
12692
|
-
SAMPLE_MARKER,
|
|
12693
|
-
"",
|
|
12694
|
-
"> This is a sample ticket filed by `crew sample-ticket` on a throwaway repository. Nothing here is real work.",
|
|
12695
|
-
"",
|
|
12696
|
-
"## What",
|
|
12697
|
-
"",
|
|
12698
|
-
"Add `greet.js` exporting `greet(name)`, which returns `Hello, <name>!` (and `Hello, world!` for no name),",
|
|
12699
|
-
"with a JSDoc comment, and `greet.test.js` covering both cases using `node:test`.",
|
|
12700
|
-
"",
|
|
12701
|
-
"## Acceptance",
|
|
12702
|
-
"",
|
|
12703
|
-
'- `greet("Ada")` returns `Hello, Ada!`; `greet()` returns `Hello, world!`.',
|
|
12704
|
-
"- The test file runs with `node --test` and passes.",
|
|
12705
|
-
"- Add a line to the README saying the function exists.",
|
|
12706
|
-
"",
|
|
12707
|
-
"## Notes",
|
|
12708
|
-
"",
|
|
12709
|
-
"The repo has no remote: leave the branch local. The hooks in `.crew.yaml` always pass, so QA is checking the",
|
|
12710
|
-
"diff against the acceptance list above."
|
|
12711
|
-
].join("\n");
|
|
12712
|
-
}
|
|
12713
|
-
function isSampleTicket(description) {
|
|
12714
|
-
return (description ?? "").trimStart().startsWith(SAMPLE_MARKER);
|
|
12715
|
-
}
|
|
12716
|
-
function planSampleTicket(i) {
|
|
12717
|
-
return {
|
|
12718
|
-
title: SAMPLE_TITLE,
|
|
12719
|
-
description: sampleDescription(),
|
|
12720
|
-
report_type: "feature",
|
|
12721
|
-
severity: "s4",
|
|
12722
|
-
reporter_name: "crew sample-ticket",
|
|
12723
|
-
status: i.approvedStatus,
|
|
12724
|
-
project_id: i.projectId,
|
|
12725
|
-
repo_id: i.repoId,
|
|
12726
|
-
...i.filedBy ? { filed_by_id: i.filedBy } : {}
|
|
12727
|
-
};
|
|
12728
|
-
}
|
|
12663
|
+
// src/repos-hooks.ts
|
|
12664
|
+
import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
|
|
12729
12665
|
|
|
12730
12666
|
// src/repos-cmd.ts
|
|
12731
12667
|
import { homedir as homedir7 } from "os";
|
|
12732
|
-
import { basename as
|
|
12668
|
+
import { basename as basename7, join as join20, resolve as resolve3 } from "path";
|
|
12733
12669
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
12734
12670
|
var ReposError = class extends Error {
|
|
12735
12671
|
};
|
|
@@ -12801,7 +12737,7 @@ function configuredRepos(text, route, base) {
|
|
|
12801
12737
|
});
|
|
12802
12738
|
}
|
|
12803
12739
|
const dir = m.get("dir");
|
|
12804
|
-
return dir ? [{ name:
|
|
12740
|
+
return dir ? [{ name: basename7(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
12805
12741
|
}
|
|
12806
12742
|
function planRepoAdd(i) {
|
|
12807
12743
|
const doc = parseDocument(i.text);
|
|
@@ -12813,7 +12749,7 @@ function planRepoAdd(i) {
|
|
|
12813
12749
|
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
12814
12750
|
}
|
|
12815
12751
|
const matched = matchRepoName(i.origin, i.resolved);
|
|
12816
|
-
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ??
|
|
12752
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename7(target);
|
|
12817
12753
|
let warning;
|
|
12818
12754
|
if (!matched) {
|
|
12819
12755
|
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
@@ -12927,13 +12863,218 @@ function planRepoClone(i) {
|
|
|
12927
12863
|
}
|
|
12928
12864
|
const ship = doc.get("ship", true);
|
|
12929
12865
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
12930
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
12866
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join20(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
12931
12867
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
|
|
12932
12868
|
return { ...add, remote, target };
|
|
12933
12869
|
}
|
|
12934
12870
|
|
|
12871
|
+
// src/repos-hooks.ts
|
|
12872
|
+
function packageManager(f) {
|
|
12873
|
+
if (f.exists("pnpm-lock.yaml")) return "pnpm";
|
|
12874
|
+
if (f.exists("yarn.lock")) return "yarn";
|
|
12875
|
+
if (f.exists("bun.lock") || f.exists("bun.lockb")) return "bun";
|
|
12876
|
+
return "npm";
|
|
12877
|
+
}
|
|
12878
|
+
function fromPackageJson(f) {
|
|
12879
|
+
const text = f.read("package.json");
|
|
12880
|
+
if (text === void 0) return void 0;
|
|
12881
|
+
let pkg;
|
|
12882
|
+
try {
|
|
12883
|
+
pkg = JSON.parse(text);
|
|
12884
|
+
} catch {
|
|
12885
|
+
return void 0;
|
|
12886
|
+
}
|
|
12887
|
+
if (!pkg || typeof pkg !== "object") return void 0;
|
|
12888
|
+
const pm = packageManager(f);
|
|
12889
|
+
const scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
12890
|
+
const out = {};
|
|
12891
|
+
if (typeof scripts.test === "string") out.test = `${pm} test`;
|
|
12892
|
+
if (typeof scripts.build === "string") out.build = pm === "yarn" ? "yarn build" : `${pm} run build`;
|
|
12893
|
+
if (typeof pkg.version === "string") {
|
|
12894
|
+
out.version = `node -p "require('./package.json').version"`;
|
|
12895
|
+
out.bump = `npm version "$CREW_BUMP" --no-git-tag-version | sed 's/^v//'`;
|
|
12896
|
+
}
|
|
12897
|
+
return out;
|
|
12898
|
+
}
|
|
12899
|
+
function fromPackageSwift(f) {
|
|
12900
|
+
return f.exists("Package.swift") ? { test: "swift test", build: "swift build" } : void 0;
|
|
12901
|
+
}
|
|
12902
|
+
function fromPyproject(f) {
|
|
12903
|
+
return f.exists("pyproject.toml") ? { test: "pytest" } : void 0;
|
|
12904
|
+
}
|
|
12905
|
+
function fromMakefile(f) {
|
|
12906
|
+
const text = f.read("Makefile") ?? f.read("makefile") ?? f.read("GNUmakefile");
|
|
12907
|
+
if (text === void 0) return void 0;
|
|
12908
|
+
const out = {};
|
|
12909
|
+
for (const target of ["test", "build"]) {
|
|
12910
|
+
if (new RegExp(`^${target}\\s*:`, "m").test(text)) out[target] = `make ${target}`;
|
|
12911
|
+
}
|
|
12912
|
+
return out;
|
|
12913
|
+
}
|
|
12914
|
+
var DETECTORS = [
|
|
12915
|
+
{ source: "package.json", detect: fromPackageJson },
|
|
12916
|
+
{ source: "Package.swift", detect: fromPackageSwift },
|
|
12917
|
+
{ source: "pyproject.toml", detect: fromPyproject },
|
|
12918
|
+
{ source: "Makefile", detect: fromMakefile }
|
|
12919
|
+
];
|
|
12920
|
+
function suggestHooks(f) {
|
|
12921
|
+
const found = DETECTORS.flatMap((d) => {
|
|
12922
|
+
const hooks = d.detect(f);
|
|
12923
|
+
return hooks && Object.keys(hooks).length > 0 ? [{ source: d.source, hooks }] : [];
|
|
12924
|
+
});
|
|
12925
|
+
if (found.length === 0) {
|
|
12926
|
+
return { source: null, hooks: {}, notes: ["no package.json scripts, Package.swift, pyproject.toml or Makefile test/build targets found \u2014 nothing to propose"] };
|
|
12927
|
+
}
|
|
12928
|
+
const [first, ...others] = found;
|
|
12929
|
+
const notes = others.length > 0 ? [`also found ${others.map((o) => o.source).join(", ")}; proposing from ${first.source} only`] : [];
|
|
12930
|
+
return { source: first.source, hooks: first.hooks, notes };
|
|
12931
|
+
}
|
|
12932
|
+
function parseHooksArgs(args, routes) {
|
|
12933
|
+
const rest = [...args];
|
|
12934
|
+
let route;
|
|
12935
|
+
if (rest.length > 1 && routes.includes(rest[0])) route = rest.shift();
|
|
12936
|
+
if (!route) {
|
|
12937
|
+
if (routes.length === 1) route = routes[0];
|
|
12938
|
+
else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
|
|
12939
|
+
}
|
|
12940
|
+
if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--write route|repo] [--json]");
|
|
12941
|
+
return { route, name: rest[0] };
|
|
12942
|
+
}
|
|
12943
|
+
function repoDirOf(text, base, route, name) {
|
|
12944
|
+
const repos = configuredRepos(text, route, base);
|
|
12945
|
+
const hit = repos.find((r) => r.name === name);
|
|
12946
|
+
if (!hit) {
|
|
12947
|
+
throw new ReposError(`route ${route} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
|
|
12948
|
+
}
|
|
12949
|
+
return hit.dir;
|
|
12950
|
+
}
|
|
12951
|
+
function planRouteHooks(text, route, name, hooks) {
|
|
12952
|
+
const doc = parseDocument2(text);
|
|
12953
|
+
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
12954
|
+
const m = routeMapOf(doc, route);
|
|
12955
|
+
const repos = m.get("repos", true);
|
|
12956
|
+
let target = m;
|
|
12957
|
+
if (isMap2(repos)) {
|
|
12958
|
+
const entry = repos.get(name, true);
|
|
12959
|
+
if (isMap2(entry)) target = entry;
|
|
12960
|
+
else {
|
|
12961
|
+
const created = new YAMLMap();
|
|
12962
|
+
created.set("dir", String(repos.get(name)));
|
|
12963
|
+
repos.set(name, created);
|
|
12964
|
+
target = created;
|
|
12965
|
+
}
|
|
12966
|
+
}
|
|
12967
|
+
const current = target.get("hooks", true);
|
|
12968
|
+
let hooksMap;
|
|
12969
|
+
if (isMap2(current)) hooksMap = current;
|
|
12970
|
+
else {
|
|
12971
|
+
hooksMap = new YAMLMap();
|
|
12972
|
+
target.set("hooks", hooksMap);
|
|
12973
|
+
}
|
|
12974
|
+
const written = [];
|
|
12975
|
+
const kept = [];
|
|
12976
|
+
for (const [k, v] of Object.entries(hooks)) {
|
|
12977
|
+
if (v === void 0) continue;
|
|
12978
|
+
if (hooksMap.has(k)) kept.push(k);
|
|
12979
|
+
else {
|
|
12980
|
+
hooksMap.set(k, v);
|
|
12981
|
+
written.push(k);
|
|
12982
|
+
}
|
|
12983
|
+
}
|
|
12984
|
+
return { text: String(doc), written, kept };
|
|
12985
|
+
}
|
|
12986
|
+
function starterRepoConfig(hooks) {
|
|
12987
|
+
const lines = [
|
|
12988
|
+
"# Starter .crew.yaml proposed by `crew repos hooks --write repo`.",
|
|
12989
|
+
"# Review the hooks, then commit this file; it is the durable home for them",
|
|
12990
|
+
"# (docs/REPO_SPEC.md). A route-level copy in crew.yaml is shadowed by it.",
|
|
12991
|
+
`version: ${SPEC_VERSION}`,
|
|
12992
|
+
"platform: any",
|
|
12993
|
+
"",
|
|
12994
|
+
"hooks:"
|
|
12995
|
+
];
|
|
12996
|
+
const entries = Object.entries(hooks).filter(([, v]) => v !== void 0);
|
|
12997
|
+
if (entries.length === 0) lines.push(' # test: "..."', ' # build: "..."');
|
|
12998
|
+
for (const [k, v] of entries) lines.push(` ${k}: ${JSON.stringify(v)}`);
|
|
12999
|
+
lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
|
|
13000
|
+
const text = lines.join("\n");
|
|
13001
|
+
parseRepoConfig(text, ".crew.yaml");
|
|
13002
|
+
return text;
|
|
13003
|
+
}
|
|
13004
|
+
function existingRepoConfig(f) {
|
|
13005
|
+
return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
|
|
13006
|
+
}
|
|
13007
|
+
|
|
13008
|
+
// src/sample-ticket.ts
|
|
13009
|
+
import { basename as basename8, dirname as dirname13, join as join21 } from "path";
|
|
13010
|
+
var SAMPLE_REPO = "crew-sample";
|
|
13011
|
+
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
13012
|
+
var SAMPLE_MARKER = "sample: true";
|
|
13013
|
+
var SAMPLE_CREW_YAML = `version: 1
|
|
13014
|
+
platform: unix
|
|
13015
|
+
branch:
|
|
13016
|
+
base: main
|
|
13017
|
+
hooks:
|
|
13018
|
+
test: "true"
|
|
13019
|
+
build: "true"
|
|
13020
|
+
release:
|
|
13021
|
+
mode: integrate
|
|
13022
|
+
versioning: none
|
|
13023
|
+
changelog: false
|
|
13024
|
+
`;
|
|
13025
|
+
var SAMPLE_README = `# crew-sample
|
|
13026
|
+
|
|
13027
|
+
A throwaway repository created by \`crew sample-ticket\`. It exists so the crew
|
|
13028
|
+
has something safe to work on: its test and build hooks always pass, it has no
|
|
13029
|
+
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
13030
|
+
`;
|
|
13031
|
+
function sampleDir(existing, reposBase, workspace) {
|
|
13032
|
+
if (existing.length > 0) return join21(dirname13(existing[0]), SAMPLE_REPO);
|
|
13033
|
+
return join21(reposBase, workspace, SAMPLE_REPO);
|
|
13034
|
+
}
|
|
13035
|
+
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
13036
|
+
function sampleDescription() {
|
|
13037
|
+
return [
|
|
13038
|
+
SAMPLE_MARKER,
|
|
13039
|
+
"",
|
|
13040
|
+
"> This is a sample ticket filed by `crew sample-ticket` on a throwaway repository. Nothing here is real work.",
|
|
13041
|
+
"",
|
|
13042
|
+
"## What",
|
|
13043
|
+
"",
|
|
13044
|
+
"Add `greet.js` exporting `greet(name)`, which returns `Hello, <name>!` (and `Hello, world!` for no name),",
|
|
13045
|
+
"with a JSDoc comment, and `greet.test.js` covering both cases using `node:test`.",
|
|
13046
|
+
"",
|
|
13047
|
+
"## Acceptance",
|
|
13048
|
+
"",
|
|
13049
|
+
'- `greet("Ada")` returns `Hello, Ada!`; `greet()` returns `Hello, world!`.',
|
|
13050
|
+
"- The test file runs with `node --test` and passes.",
|
|
13051
|
+
"- Add a line to the README saying the function exists.",
|
|
13052
|
+
"",
|
|
13053
|
+
"## Notes",
|
|
13054
|
+
"",
|
|
13055
|
+
"The repo has no remote: leave the branch local. The hooks in `.crew.yaml` always pass, so QA is checking the",
|
|
13056
|
+
"diff against the acceptance list above."
|
|
13057
|
+
].join("\n");
|
|
13058
|
+
}
|
|
13059
|
+
function isSampleTicket(description) {
|
|
13060
|
+
return (description ?? "").trimStart().startsWith(SAMPLE_MARKER);
|
|
13061
|
+
}
|
|
13062
|
+
function planSampleTicket(i) {
|
|
13063
|
+
return {
|
|
13064
|
+
title: SAMPLE_TITLE,
|
|
13065
|
+
description: sampleDescription(),
|
|
13066
|
+
report_type: "feature",
|
|
13067
|
+
severity: "s4",
|
|
13068
|
+
reporter_name: "crew sample-ticket",
|
|
13069
|
+
status: i.approvedStatus,
|
|
13070
|
+
project_id: i.projectId,
|
|
13071
|
+
repo_id: i.repoId,
|
|
13072
|
+
...i.filedBy ? { filed_by_id: i.filedBy } : {}
|
|
13073
|
+
};
|
|
13074
|
+
}
|
|
13075
|
+
|
|
12935
13076
|
// src/doctor-fix.ts
|
|
12936
|
-
import { parseDocument as
|
|
13077
|
+
import { parseDocument as parseDocument3 } from "yaml";
|
|
12937
13078
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
12938
13079
|
function planFix(f) {
|
|
12939
13080
|
const actions = [];
|
|
@@ -12960,7 +13101,7 @@ function routesInScope(routes, named) {
|
|
|
12960
13101
|
return named ? routes.filter((r) => r.route === named) : routes;
|
|
12961
13102
|
}
|
|
12962
13103
|
function planEnable(text, route) {
|
|
12963
|
-
const doc =
|
|
13104
|
+
const doc = parseDocument3(text);
|
|
12964
13105
|
routeMapOf(doc, route).set("enabled", true);
|
|
12965
13106
|
return String(doc);
|
|
12966
13107
|
}
|
|
@@ -14703,6 +14844,7 @@ function usage() {
|
|
|
14703
14844
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
14704
14845
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
14705
14846
|
crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
|
|
14847
|
+
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo, --json)
|
|
14706
14848
|
crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
|
|
14707
14849
|
crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
|
|
14708
14850
|
crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
|
|
@@ -16382,11 +16524,67 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16382
16524
|
case "repos": {
|
|
16383
16525
|
const sub = positional[1];
|
|
16384
16526
|
const usage2 = () => {
|
|
16385
|
-
process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project>\n");
|
|
16527
|
+
process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project>\ncrew repos hooks [workspace/project] <name> [--suggest] [--write route|repo] [--json]\n");
|
|
16386
16528
|
process.exit(2);
|
|
16387
16529
|
};
|
|
16388
|
-
if (sub !== "add" && sub !== "list" && sub !== "clone") usage2();
|
|
16530
|
+
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
16389
16531
|
const configText = readFileSync22(cfg.configFile, "utf8");
|
|
16532
|
+
if (sub === "hooks") {
|
|
16533
|
+
try {
|
|
16534
|
+
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
16535
|
+
const dir = repoDirOf(configText, dirname16(cfg.configFile), a.route, a.name);
|
|
16536
|
+
if (!existsSync29(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
16537
|
+
const files = { exists: (r) => existsSync29(resolvePath(dir, r)), read: (r) => {
|
|
16538
|
+
try {
|
|
16539
|
+
return readFileSync22(resolvePath(dir, r), "utf8");
|
|
16540
|
+
} catch {
|
|
16541
|
+
return void 0;
|
|
16542
|
+
}
|
|
16543
|
+
} };
|
|
16544
|
+
const suggestion = suggestHooks(files);
|
|
16545
|
+
const write = value("write");
|
|
16546
|
+
if (write !== void 0 && write !== "route" && write !== "repo") throw new ReposError(`--write takes "route" or "repo" (got "${write}")`);
|
|
16547
|
+
let written;
|
|
16548
|
+
if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
16549
|
+
if (write === "route") {
|
|
16550
|
+
const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
|
|
16551
|
+
writeFileSync16(cfg.configFile, plan.text);
|
|
16552
|
+
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
16553
|
+
} else if (write === "repo") {
|
|
16554
|
+
const have = existingRepoConfig(files);
|
|
16555
|
+
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
16556
|
+
const file = resolvePath(dir, ".crew.yaml");
|
|
16557
|
+
writeFileSync16(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
|
|
16558
|
+
written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
|
|
16559
|
+
}
|
|
16560
|
+
if (flag("json")) {
|
|
16561
|
+
process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, written: written ?? null })}
|
|
16562
|
+
`);
|
|
16563
|
+
} else {
|
|
16564
|
+
if (suggestion.source) process.stdout.write(`${a.name}: proposed from ${suggestion.source}
|
|
16565
|
+
`);
|
|
16566
|
+
for (const [k, v] of Object.entries(suggestion.hooks)) process.stdout.write(` ${k}: ${v}
|
|
16567
|
+
`);
|
|
16568
|
+
for (const n of suggestion.notes) process.stdout.write(`note: ${n}
|
|
16569
|
+
`);
|
|
16570
|
+
if (written) {
|
|
16571
|
+
process.stdout.write(`wrote ${written.hooks.join(", ") || "nothing new"} to ${written.file}
|
|
16572
|
+
`);
|
|
16573
|
+
if (written.kept.length) process.stdout.write(`kept existing route hooks: ${written.kept.join(", ")}
|
|
16574
|
+
`);
|
|
16575
|
+
if (written.target === "repo") process.stdout.write("review it and commit it to the repository\n");
|
|
16576
|
+
}
|
|
16577
|
+
}
|
|
16578
|
+
} catch (e) {
|
|
16579
|
+
if (e instanceof ReposError) {
|
|
16580
|
+
process.stderr.write(`crew repos: ${e.message}
|
|
16581
|
+
`);
|
|
16582
|
+
process.exit(2);
|
|
16583
|
+
}
|
|
16584
|
+
throw e;
|
|
16585
|
+
}
|
|
16586
|
+
break;
|
|
16587
|
+
}
|
|
16390
16588
|
let routeName = positional[2];
|
|
16391
16589
|
let addPath = ".";
|
|
16392
16590
|
let addName;
|
package/package.json
CHANGED