team 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/dist/approve/approval.d.ts +38 -0
- package/dist/approve/approval.js +78 -0
- package/dist/approve/diff.d.ts +13 -0
- package/dist/approve/diff.js +47 -0
- package/dist/approve/fingerprint.d.ts +44 -0
- package/dist/approve/fingerprint.js +109 -0
- package/dist/args.d.ts +7 -0
- package/dist/args.js +22 -0
- package/dist/budgets/checks.d.ts +35 -0
- package/dist/budgets/checks.js +77 -0
- package/dist/budgets/gate.d.ts +24 -0
- package/dist/budgets/gate.js +127 -0
- package/dist/budgets/readings.d.ts +69 -0
- package/dist/budgets/readings.js +156 -0
- package/dist/budgets/run.d.ts +66 -0
- package/dist/budgets/run.js +119 -0
- package/dist/budgets/table.d.ts +24 -0
- package/dist/budgets/table.js +90 -0
- package/dist/caller.d.ts +48 -0
- package/dist/caller.js +126 -0
- package/dist/check/config.d.ts +51 -0
- package/dist/check/config.js +50 -0
- package/dist/check/git.d.ts +27 -0
- package/dist/check/git.js +97 -0
- package/dist/check/message.d.ts +27 -0
- package/dist/check/message.js +101 -0
- package/dist/check/run.d.ts +31 -0
- package/dist/check/run.js +77 -0
- package/dist/check/signature.d.ts +10 -0
- package/dist/check/signature.js +22 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +76 -0
- package/dist/clis.d.ts +7 -0
- package/dist/clis.js +9 -0
- package/dist/commands/add.d.ts +23 -0
- package/dist/commands/add.js +445 -0
- package/dist/commands/approve.d.ts +11 -0
- package/dist/commands/approve.js +146 -0
- package/dist/commands/check.d.ts +17 -0
- package/dist/commands/check.js +103 -0
- package/dist/commands/doctor.d.ts +38 -0
- package/dist/commands/doctor.js +232 -0
- package/dist/commands/down.d.ts +37 -0
- package/dist/commands/down.js +258 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +138 -0
- package/dist/commands/remove.d.ts +23 -0
- package/dist/commands/remove.js +235 -0
- package/dist/commands/status.d.ts +41 -0
- package/dist/commands/status.js +147 -0
- package/dist/commands/up.d.ts +49 -0
- package/dist/commands/up.js +362 -0
- package/dist/commands/watch.d.ts +35 -0
- package/dist/commands/watch.js +324 -0
- package/dist/commands/worktree.d.ts +12 -0
- package/dist/commands/worktree.js +388 -0
- package/dist/end/condition.d.ts +28 -0
- package/dist/end/condition.js +77 -0
- package/dist/file/current.d.ts +13 -0
- package/dist/file/current.js +53 -0
- package/dist/file/lines.d.ts +25 -0
- package/dist/file/lines.js +277 -0
- package/dist/file/load.d.ts +7 -0
- package/dist/file/load.js +90 -0
- package/dist/file/paths.d.ts +6 -0
- package/dist/file/paths.js +77 -0
- package/dist/file/secrets.d.ts +6 -0
- package/dist/file/secrets.js +54 -0
- package/dist/file/signature.d.ts +13 -0
- package/dist/file/signature.js +36 -0
- package/dist/file/types.d.ts +130 -0
- package/dist/file/types.js +1 -0
- package/dist/file/validate.d.ts +13 -0
- package/dist/file/validate.js +702 -0
- package/dist/file/write.d.ts +11 -0
- package/dist/file/write.js +13 -0
- package/dist/herdr.d.ts +38 -0
- package/dist/herdr.js +227 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/io.d.ts +10 -0
- package/dist/io.js +1 -0
- package/dist/launch/deliver.d.ts +10 -0
- package/dist/launch/deliver.js +39 -0
- package/dist/launch/execute.d.ts +46 -0
- package/dist/launch/execute.js +284 -0
- package/dist/launch/plan.d.ts +185 -0
- package/dist/launch/plan.js +280 -0
- package/dist/launch/rules.d.ts +25 -0
- package/dist/launch/rules.js +36 -0
- package/dist/log.d.ts +1 -0
- package/dist/log.js +24 -0
- package/dist/profiles/antigravity.yaml +66 -0
- package/dist/profiles/claude-code.yaml +69 -0
- package/dist/profiles/codex.yaml +77 -0
- package/dist/profiles/cursor.yaml +68 -0
- package/dist/profiles/index.d.ts +1 -0
- package/dist/profiles/index.js +1 -0
- package/dist/profiles/profile.d.ts +60 -0
- package/dist/profiles/profile.js +313 -0
- package/dist/profiles/quota.d.ts +27 -0
- package/dist/profiles/quota.js +57 -0
- package/dist/state.d.ts +61 -0
- package/dist/state.js +127 -0
- package/dist/status/compare.d.ts +26 -0
- package/dist/status/compare.js +110 -0
- package/dist/status/statusline.d.ts +9 -0
- package/dist/status/statusline.js +26 -0
- package/dist/store/store.d.ts +62 -0
- package/dist/store/store.js +116 -0
- package/dist/watch/check.d.ts +82 -0
- package/dist/watch/check.js +29 -0
- package/dist/watch/checks/approval.d.ts +2 -0
- package/dist/watch/checks/approval.js +16 -0
- package/dist/watch/checks/attention.d.ts +2 -0
- package/dist/watch/checks/attention.js +23 -0
- package/dist/watch/checks/budget.d.ts +2 -0
- package/dist/watch/checks/budget.js +141 -0
- package/dist/watch/checks/disk.d.ts +2 -0
- package/dist/watch/checks/disk.js +13 -0
- package/dist/watch/checks/extra.d.ts +2 -0
- package/dist/watch/checks/extra.js +18 -0
- package/dist/watch/checks/idle.d.ts +2 -0
- package/dist/watch/checks/idle.js +32 -0
- package/dist/watch/checks/load.d.ts +2 -0
- package/dist/watch/checks/load.js +11 -0
- package/dist/watch/checks/memory.d.ts +2 -0
- package/dist/watch/checks/memory.js +11 -0
- package/dist/watch/checks/missing.d.ts +2 -0
- package/dist/watch/checks/missing.js +12 -0
- package/dist/watch/checks/model-drift.d.ts +2 -0
- package/dist/watch/checks/model-drift.js +14 -0
- package/dist/watch/checks/swap-free.d.ts +2 -0
- package/dist/watch/checks/swap-free.js +12 -0
- package/dist/watch/checks/swap-growth.d.ts +2 -0
- package/dist/watch/checks/swap-growth.js +18 -0
- package/dist/watch/checks/team-idle.d.ts +2 -0
- package/dist/watch/checks/team-idle.js +18 -0
- package/dist/watch/checks/unsent.d.ts +2 -0
- package/dist/watch/checks/unsent.js +18 -0
- package/dist/watch/close.d.ts +19 -0
- package/dist/watch/close.js +40 -0
- package/dist/watch/dialect.d.ts +5 -0
- package/dist/watch/dialect.js +430 -0
- package/dist/watch/end.d.ts +11 -0
- package/dist/watch/end.js +17 -0
- package/dist/watch/machine.d.ts +38 -0
- package/dist/watch/machine.js +137 -0
- package/dist/watch/notify.d.ts +1 -0
- package/dist/watch/notify.js +15 -0
- package/dist/watch/pass.d.ts +42 -0
- package/dist/watch/pass.js +242 -0
- package/dist/watch/screen-core.d.ts +20 -0
- package/dist/watch/screen-core.js +356 -0
- package/dist/watch/screen-data.d.ts +65 -0
- package/dist/watch/screen-data.js +1 -0
- package/dist/watch/screen-file.d.ts +2 -0
- package/dist/watch/screen-file.js +269 -0
- package/dist/watch/screen.d.ts +24 -0
- package/dist/watch/screen.js +47 -0
- package/dist/worktree/place.d.ts +18 -0
- package/dist/worktree/place.js +126 -0
- package/dist/yaml.d.ts +30 -0
- package/dist/yaml.js +380 -0
- package/examples/checks/codex-quota +217 -0
- package/examples/team.yaml +76 -0
- package/package.json +29 -10
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { fromTeamFile } from "../check/config.js";
|
|
5
|
+
import { GitError } from "../check/git.js";
|
|
6
|
+
import { formatReport, runCheck } from "../check/run.js";
|
|
7
|
+
import { loadTeamFile } from "../file/load.js";
|
|
8
|
+
import { readLedger, storePath } from "../store/store.js";
|
|
9
|
+
export const USAGE = `Usage: team check <ref> [--pr <file>] [--since <ref>] [--file <path>]
|
|
10
|
+
|
|
11
|
+
<ref> a range when it holds "..", passed to git as given
|
|
12
|
+
(origin/main..HEAD); otherwise that one commit
|
|
13
|
+
--pr <file> also check a pull request's body ("-" reads standard input)
|
|
14
|
+
--since <ref> skip this commit and everything reachable from it, for this
|
|
15
|
+
run; overrides identity.since
|
|
16
|
+
--file <path> the team file, instead of .agents/team.yaml
|
|
17
|
+
|
|
18
|
+
Exits 0 when every commit passes, 1 when one is refused, 2 when the check
|
|
19
|
+
can't run.
|
|
20
|
+
`;
|
|
21
|
+
const VALUE_OPTIONS = ['pr', 'since', 'file'];
|
|
22
|
+
function parse(argv) {
|
|
23
|
+
const values = {};
|
|
24
|
+
const positional = [];
|
|
25
|
+
for (let index = 0; index < argv.length; index++) {
|
|
26
|
+
const argument = argv[index];
|
|
27
|
+
if (!argument.startsWith('--')) {
|
|
28
|
+
positional.push(argument);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const [name, inline] = argument.slice(2).split(/=(.*)/s, 2);
|
|
32
|
+
const option = VALUE_OPTIONS.find((known) => known === name);
|
|
33
|
+
if (!option)
|
|
34
|
+
return `unknown option ${argument}`;
|
|
35
|
+
const value = inline ?? argv[++index];
|
|
36
|
+
if (value === undefined || value === '')
|
|
37
|
+
return `--${option} needs a value`;
|
|
38
|
+
values[option] = value;
|
|
39
|
+
}
|
|
40
|
+
const [ref, ...rest] = positional;
|
|
41
|
+
if (ref === undefined)
|
|
42
|
+
return 'a <ref> is required';
|
|
43
|
+
if (rest.length > 0)
|
|
44
|
+
return 'one <ref> at most';
|
|
45
|
+
return { ref, ...values };
|
|
46
|
+
}
|
|
47
|
+
/** The file's rules, with the ledger of this machine's store when the owner has approved a file here. */
|
|
48
|
+
export function loadConfig(cwd, file, home = homedir()) {
|
|
49
|
+
const loaded = loadTeamFile(cwd, { file });
|
|
50
|
+
if (!loaded.ok)
|
|
51
|
+
return loaded;
|
|
52
|
+
const store = storePath(loaded.team.project, loaded.root, home);
|
|
53
|
+
const ledgerFile = join(store, 'ledger.json');
|
|
54
|
+
try {
|
|
55
|
+
const ledger = readLedger(store);
|
|
56
|
+
return { ok: true, config: fromTeamFile(loaded.team, ledger), warnings: loaded.warnings };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
60
|
+
return { ok: false, errors: [{ line: 0, message: `can't read ${ledgerFile}: ${detail}` }] };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function place(problem, path) {
|
|
64
|
+
const where = [path, problem.line > 0 ? `line ${problem.line}` : ''].filter(Boolean).join(', ');
|
|
65
|
+
return `${where ? `${where}: ` : ''}${problem.message}`;
|
|
66
|
+
}
|
|
67
|
+
export async function check(argv, io, load = loadConfig) {
|
|
68
|
+
const args = parse(argv);
|
|
69
|
+
if (typeof args === 'string') {
|
|
70
|
+
io.stderr(`team check: ${args}\n\n${USAGE}`);
|
|
71
|
+
return 2;
|
|
72
|
+
}
|
|
73
|
+
const loaded = load(io.cwd, args.file);
|
|
74
|
+
if (!loaded.ok) {
|
|
75
|
+
for (const problem of loaded.errors)
|
|
76
|
+
io.stderr(`team check: ${place(problem, loaded.path)}\n`);
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
for (const warning of loaded.warnings)
|
|
80
|
+
io.stderr(`team check: warning: ${place(warning)}\n`);
|
|
81
|
+
let pullRequestBody;
|
|
82
|
+
if (args.pr !== undefined) {
|
|
83
|
+
try {
|
|
84
|
+
pullRequestBody = readFileSync(args.pr === '-' ? 0 : resolve(io.cwd, args.pr), 'utf8');
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
io.stderr(`team check: can't read the pull request body: ${error.message}\n`);
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const report = runCheck(loaded.config, { cwd: io.cwd, ref: args.ref, since: args.since, pullRequestBody });
|
|
93
|
+
io.stdout(formatReport(report));
|
|
94
|
+
return report.ok ? 0 : 1;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!(error instanceof GitError))
|
|
98
|
+
throw error;
|
|
99
|
+
io.stderr(`team check: ${error.message}\n`);
|
|
100
|
+
return 2;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export default check;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TeamFile } from '../file/types.ts';
|
|
2
|
+
import type { Command, Io } from '../io.ts';
|
|
3
|
+
import { type Profile } from '../profiles/profile.ts';
|
|
4
|
+
export type DoctorSources = {
|
|
5
|
+
version(binary: string): string | null;
|
|
6
|
+
onPath(binary: string): boolean;
|
|
7
|
+
loggedIn(profile: Profile): boolean | null;
|
|
8
|
+
herdrVersion(): string | null;
|
|
9
|
+
sessionRunning(session: string): boolean | null;
|
|
10
|
+
now(): Date;
|
|
11
|
+
home: string;
|
|
12
|
+
};
|
|
13
|
+
export type CommandRunner = (binary: string, args: string[]) => {
|
|
14
|
+
status: number | null;
|
|
15
|
+
stdout: string;
|
|
16
|
+
} | null;
|
|
17
|
+
export declare function run(binary: string, args: string[]): {
|
|
18
|
+
status: number | null;
|
|
19
|
+
stdout: string;
|
|
20
|
+
} | null;
|
|
21
|
+
export declare const realCommandRunner: typeof run;
|
|
22
|
+
export declare function checkLogin(profile: Profile, runner?: CommandRunner): boolean | null;
|
|
23
|
+
export declare const realSources: DoctorSources;
|
|
24
|
+
export declare const USAGE = "Usage: team doctor [--session <name>] [--file <path>] [--login]\n";
|
|
25
|
+
export type Level = 'ok' | 'warn' | 'miss' | 'note';
|
|
26
|
+
export type Finding = {
|
|
27
|
+
level: Level;
|
|
28
|
+
text: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function blocksLaunch(finding: Finding): boolean;
|
|
31
|
+
export declare const doctor: Command;
|
|
32
|
+
export default doctor;
|
|
33
|
+
export declare function doctorFindings(team: TeamFile, root: string, dir: string, session: string, sources: DoctorSources, warnings: {
|
|
34
|
+
line: number;
|
|
35
|
+
message: string;
|
|
36
|
+
}[]): Finding[];
|
|
37
|
+
export declare function doctorLoginFindings(team: TeamFile, sources: DoctorSources): Finding[];
|
|
38
|
+
export declare function runDoctor(argv: string[], io: Io, sources: DoctorSources): Promise<number>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { accessSync, constants } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join } from 'node:path';
|
|
5
|
+
import { approvalDifferences, budgetsInForce, watchInForce } from "../approve/approval.js";
|
|
6
|
+
import { readArgs } from "../args.js";
|
|
7
|
+
import { checkReadings } from "../budgets/checks.js";
|
|
8
|
+
import { loadTeamFile } from "../file/load.js";
|
|
9
|
+
import { HERDR_TESTED, herdrVersion, sessionRunning } from "../herdr.js";
|
|
10
|
+
import { profileFor } from "../profiles/index.js";
|
|
11
|
+
import { versionVerdict } from "../profiles/profile.js";
|
|
12
|
+
import { readState } from "../state.js";
|
|
13
|
+
import { readApproval, storePath } from "../store/store.js";
|
|
14
|
+
export function run(binary, args) {
|
|
15
|
+
const result = spawnSync(binary, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 15_000 });
|
|
16
|
+
return result.error ? null : { status: result.status, stdout: result.stdout };
|
|
17
|
+
}
|
|
18
|
+
export const realCommandRunner = run;
|
|
19
|
+
export function checkLogin(profile, runner = run) {
|
|
20
|
+
if (!profile.loginCheck)
|
|
21
|
+
return null;
|
|
22
|
+
// Only the exit code is read: the output names the account.
|
|
23
|
+
const result = runner(profile.binary, [...profile.loginCheck]);
|
|
24
|
+
return result ? result.status === 0 : null;
|
|
25
|
+
}
|
|
26
|
+
export const realSources = {
|
|
27
|
+
version(binary) {
|
|
28
|
+
const result = run(binary, ['--version']);
|
|
29
|
+
return result && result.status === 0 ? result.stdout.trim() : null;
|
|
30
|
+
},
|
|
31
|
+
onPath(binary) {
|
|
32
|
+
const folders = isAbsolute(binary) ? [''] : (process.env.PATH ?? '').split(delimiter).filter(Boolean);
|
|
33
|
+
return folders.some((folder) => {
|
|
34
|
+
try {
|
|
35
|
+
accessSync(folder ? join(folder, binary) : binary, constants.X_OK);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
loggedIn(profile) {
|
|
44
|
+
return checkLogin(profile, run);
|
|
45
|
+
},
|
|
46
|
+
herdrVersion,
|
|
47
|
+
sessionRunning,
|
|
48
|
+
now: () => new Date(),
|
|
49
|
+
home: homedir(),
|
|
50
|
+
};
|
|
51
|
+
export const USAGE = 'Usage: team doctor [--session <name>] [--file <path>] [--login]\n';
|
|
52
|
+
// A missing or stale watch is not a reason for `up` to refuse: `up` starts the watch itself.
|
|
53
|
+
export function blocksLaunch(finding) {
|
|
54
|
+
if (finding.level !== 'miss')
|
|
55
|
+
return false;
|
|
56
|
+
return !finding.text.startsWith('no watch has run') && !finding.text.startsWith("the watch's heartbeat");
|
|
57
|
+
}
|
|
58
|
+
export const doctor = (argv, io) => runDoctor(argv, io, realSources);
|
|
59
|
+
export default doctor;
|
|
60
|
+
function range(tested) {
|
|
61
|
+
return tested.from === tested.to ? tested.from : `${tested.from} to ${tested.to}`;
|
|
62
|
+
}
|
|
63
|
+
function versionFinding(name, printed, tested) {
|
|
64
|
+
const verdict = versionVerdict(printed, tested);
|
|
65
|
+
if (verdict === 'tested')
|
|
66
|
+
return { level: 'ok', text: `${name} ${printed}` };
|
|
67
|
+
if (verdict === 'unread')
|
|
68
|
+
return { level: 'warn', text: `${name}: its version can't be read from "${printed}"` };
|
|
69
|
+
return { level: 'warn', text: `${name} ${printed} is ${verdict} than the tested ${range(tested)}` };
|
|
70
|
+
}
|
|
71
|
+
// The command a launch line starts: its first word that is not a variable assignment.
|
|
72
|
+
function launchBinary(launch) {
|
|
73
|
+
return (launch
|
|
74
|
+
.trim()
|
|
75
|
+
.split(/\s+/)
|
|
76
|
+
.find((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) ?? null);
|
|
77
|
+
}
|
|
78
|
+
function checkFindings(team, root, home) {
|
|
79
|
+
const record = readApproval(storePath(team.project, root, home));
|
|
80
|
+
if (!record)
|
|
81
|
+
return [];
|
|
82
|
+
return checkReadings(budgetsInForce(team, root, home), record.approval.checks).flatMap((reading) => reading.state === 'unknown'
|
|
83
|
+
? [{ level: 'warn', text: `the check for ${reading.account} is unapproved; that account reads unknown` }]
|
|
84
|
+
: []);
|
|
85
|
+
}
|
|
86
|
+
function approvalFindings(team, root, home) {
|
|
87
|
+
const differences = approvalDifferences(team, root, home);
|
|
88
|
+
if (differences === null) {
|
|
89
|
+
return [{ level: 'miss', text: 'run `team approve`: this file was never approved on this machine' }];
|
|
90
|
+
}
|
|
91
|
+
if (differences.length)
|
|
92
|
+
return [{ level: 'miss', text: `run \`team approve\`: ${differences.join('; ')}` }];
|
|
93
|
+
return [{ level: 'ok', text: 'the file is the one the owner approved' }];
|
|
94
|
+
}
|
|
95
|
+
function cliFindings(cli, seats, sources) {
|
|
96
|
+
const names = seats.map((seat) => seat.name).join(', ');
|
|
97
|
+
const profile = profileFor(cli);
|
|
98
|
+
if (!profile) {
|
|
99
|
+
return [{ level: 'warn', text: `${cli}: no launch profile in this version; \`up\` leaves out ${names}` }];
|
|
100
|
+
}
|
|
101
|
+
const printed = sources.version(profile.binary);
|
|
102
|
+
if (printed === null) {
|
|
103
|
+
return [{ level: 'miss', text: `install \`${profile.binary}\`: it is not on the PATH (${cli}: ${names})` }];
|
|
104
|
+
}
|
|
105
|
+
const findings = [versionFinding(profile.binary, printed, profile.tested)];
|
|
106
|
+
const loggedIn = sources.loggedIn(profile);
|
|
107
|
+
if (loggedIn === false)
|
|
108
|
+
findings.push({ level: 'miss', text: `log in to ${cli}: \`${profile.loginHint}\`` });
|
|
109
|
+
else if (loggedIn === null)
|
|
110
|
+
findings.push({ level: 'note', text: `${cli}: the login is not checked in this version` });
|
|
111
|
+
else
|
|
112
|
+
findings.push({ level: 'ok', text: `${cli}: logged in` });
|
|
113
|
+
for (const seat of seats) {
|
|
114
|
+
const binary = launchBinary(seat.launch);
|
|
115
|
+
if (binary !== null && binary !== profile.binary && !sources.onPath(binary)) {
|
|
116
|
+
findings.push({ level: 'miss', text: `${seat.name}: its launcher \`${binary}\` is not on the PATH` });
|
|
117
|
+
}
|
|
118
|
+
const named = profile.modelOf(seat.launch);
|
|
119
|
+
if (named === null) {
|
|
120
|
+
findings.push({
|
|
121
|
+
level: 'warn',
|
|
122
|
+
text: `${seat.name}: the launch names no model this version knows; the file says ${seat.model} ${seat.version}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else if (named.model !== seat.model || named.version !== seat.version) {
|
|
126
|
+
findings.push({
|
|
127
|
+
level: 'warn',
|
|
128
|
+
text: `${seat.name}: the launch starts ${named.model} ${named.version}, the file says ${seat.model} ${seat.version}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return findings;
|
|
133
|
+
}
|
|
134
|
+
// `watch` is the watch values in force — the approved ones — so an unapproved interval edit can't
|
|
135
|
+
// move the verdict either way.
|
|
136
|
+
function watchFindings(watch, dir, session, running, now) {
|
|
137
|
+
const recorded = readState(dir).sessions[session]?.watch;
|
|
138
|
+
if (!running)
|
|
139
|
+
return [];
|
|
140
|
+
if (!recorded)
|
|
141
|
+
return [{ level: 'miss', text: `no watch has run for session ${session}: start \`team watch\`` }];
|
|
142
|
+
const age = (now.getTime() - Date.parse(recorded.heartbeat)) / 1000;
|
|
143
|
+
if (!(age <= 2 * watch.interval)) {
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
level: 'miss',
|
|
147
|
+
text: `the watch's heartbeat is ${Math.round(age / 60)} min old (two intervals are ${(2 * watch.interval) / 60} min): start \`team watch\``,
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
return [{ level: 'ok', text: 'the watch is running' }];
|
|
152
|
+
}
|
|
153
|
+
export function doctorFindings(team, root, dir, session, sources, warnings) {
|
|
154
|
+
const findings = warnings.map((warning) => ({
|
|
155
|
+
level: 'warn',
|
|
156
|
+
text: `the file, line ${warning.line}: ${warning.message}`,
|
|
157
|
+
}));
|
|
158
|
+
findings.push(...approvalFindings(team, root, sources.home), ...checkFindings(team, root, sources.home));
|
|
159
|
+
const herdr = sources.herdrVersion();
|
|
160
|
+
const running = herdr === null ? null : sources.sessionRunning(session);
|
|
161
|
+
if (herdr === null)
|
|
162
|
+
findings.push({ level: 'miss', text: 'install herdr: it is not on the PATH' });
|
|
163
|
+
else {
|
|
164
|
+
findings.push(versionFinding('herdr', herdr, HERDR_TESTED));
|
|
165
|
+
if (running === null)
|
|
166
|
+
findings.push({ level: 'miss', text: "herdr doesn't answer" });
|
|
167
|
+
else
|
|
168
|
+
findings.push({ level: 'note', text: `session ${session} is ${running ? 'running' : 'not running'}` });
|
|
169
|
+
}
|
|
170
|
+
const launched = team.seats.filter((seat) => !seat.stopped);
|
|
171
|
+
for (const cli of new Set(launched.map((seat) => seat.cli))) {
|
|
172
|
+
findings.push(...cliFindings(cli, launched.filter((seat) => seat.cli === cli), sources));
|
|
173
|
+
}
|
|
174
|
+
findings.push(...watchFindings(watchInForce(team, root, sources.home), dir, session, running, sources.now()));
|
|
175
|
+
if (team.trust.length) {
|
|
176
|
+
findings.push({ level: 'note', text: 'trust: not applied or checked by this version; trust each folder by hand' });
|
|
177
|
+
}
|
|
178
|
+
return findings;
|
|
179
|
+
}
|
|
180
|
+
export function doctorLoginFindings(team, sources) {
|
|
181
|
+
const clis = [];
|
|
182
|
+
for (const seat of team.seats) {
|
|
183
|
+
if (!clis.includes(seat.cli))
|
|
184
|
+
clis.push(seat.cli);
|
|
185
|
+
}
|
|
186
|
+
const findings = [];
|
|
187
|
+
for (const cli of clis) {
|
|
188
|
+
const profile = profileFor(cli);
|
|
189
|
+
if (!profile) {
|
|
190
|
+
findings.push({ level: 'note', text: `${cli}: no launch profile in this version` });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const loggedIn = sources.loggedIn(profile);
|
|
194
|
+
if (loggedIn === false) {
|
|
195
|
+
findings.push({ level: 'miss', text: `log in to ${cli}: \`${profile.loginHint}\`` });
|
|
196
|
+
}
|
|
197
|
+
else if (loggedIn === null) {
|
|
198
|
+
findings.push({ level: 'note', text: `${cli}: the login is not checked in this version` });
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
findings.push({ level: 'ok', text: `${cli}: logged in` });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return findings;
|
|
205
|
+
}
|
|
206
|
+
export async function runDoctor(argv, io, sources) {
|
|
207
|
+
const args = readArgs(argv, ['session', 'file'], ['login']);
|
|
208
|
+
if (args.error || args.rest.length) {
|
|
209
|
+
io.stderr(`team doctor: ${args.error ?? `unexpected "${args.rest[0]}"`}\n${USAGE}`);
|
|
210
|
+
return 2;
|
|
211
|
+
}
|
|
212
|
+
const loaded = loadTeamFile(io.cwd, args.values.file ? { file: args.values.file } : {});
|
|
213
|
+
if (!loaded.ok) {
|
|
214
|
+
for (const problem of loaded.errors) {
|
|
215
|
+
io.stderr(`team doctor: ${problem.line ? `line ${problem.line}: ` : ''}${problem.message}\n`);
|
|
216
|
+
}
|
|
217
|
+
return 2;
|
|
218
|
+
}
|
|
219
|
+
const { team, root } = loaded;
|
|
220
|
+
const session = args.values.session ?? team.session;
|
|
221
|
+
const findings = args.flags.has('login')
|
|
222
|
+
? doctorLoginFindings(team, sources)
|
|
223
|
+
: doctorFindings(team, root, dirname(loaded.path), session, sources, loaded.warnings);
|
|
224
|
+
const label = { ok: 'ok ', warn: 'warn', miss: 'MISS', note: '-- ' };
|
|
225
|
+
io.stdout(findings.map((finding) => `${label[finding.level]} ${finding.text}\n`).join(''));
|
|
226
|
+
const missing = findings.filter((finding) => finding.level === 'miss').length;
|
|
227
|
+
const warnings = findings.filter((finding) => finding.level === 'warn').length;
|
|
228
|
+
io.stdout(missing
|
|
229
|
+
? `team doctor: ${missing} missing, ${warnings} warning${warnings === 1 ? '' : 's'}: \`up\` and \`add\` refuse until the missing ones are done\n`
|
|
230
|
+
: `team doctor: nothing missing, ${warnings} warning${warnings === 1 ? '' : 's'}\n`);
|
|
231
|
+
return missing ? 1 : 0;
|
|
232
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type HerdrAgent } from '../herdr.ts';
|
|
2
|
+
import type { Command, Io } from '../io.ts';
|
|
3
|
+
import { type DownSeat } from '../launch/plan.ts';
|
|
4
|
+
import { type Screen } from '../watch/screen.ts';
|
|
5
|
+
export type DownSources = {
|
|
6
|
+
sessionRunning(session: string): boolean | null;
|
|
7
|
+
agents(session: string): HerdrAgent[] | null;
|
|
8
|
+
alive(pid: number): boolean;
|
|
9
|
+
/** Herdr's status is not enough: a permission prompt is reported as idle. */
|
|
10
|
+
screen(session: string, pane: string, cli: string): Screen;
|
|
11
|
+
/** Herdr's own status for the pane. The Enter waits for idle or done. */
|
|
12
|
+
status(session: string, pane: string): string | null;
|
|
13
|
+
/** Foreground process names in the pane, or null when the pane can't be read. */
|
|
14
|
+
foreground?(session: string, pane: string): string[] | null;
|
|
15
|
+
now(): Date;
|
|
16
|
+
sleep?(ms: number): Promise<void>;
|
|
17
|
+
launch?: DownLaunch;
|
|
18
|
+
};
|
|
19
|
+
export type DownLaunch = {
|
|
20
|
+
typeText(session: string, pane: string, text: string): boolean;
|
|
21
|
+
pressEnter(session: string, pane: string): boolean;
|
|
22
|
+
agentPanes(session: string): string[] | null;
|
|
23
|
+
closeWorkspace(session: string, workspace: string): boolean;
|
|
24
|
+
stopSession(session: string): boolean;
|
|
25
|
+
kill(pid: number): boolean;
|
|
26
|
+
sleep(ms: number): Promise<void>;
|
|
27
|
+
now(): Date;
|
|
28
|
+
};
|
|
29
|
+
export declare const realSources: DownSources;
|
|
30
|
+
/** How long `--wait` gives a working seat, in seconds. */
|
|
31
|
+
export declare const WAIT_SECONDS = 120;
|
|
32
|
+
export declare const USAGE = "Usage: team down [--dry-run] [--wait] [--abandon] [--session <name>] [--file <path>]\n";
|
|
33
|
+
export declare const down: Command;
|
|
34
|
+
export default down;
|
|
35
|
+
export declare function paneStillRunning(foreground: readonly string[] | null, processNames: readonly string[]): boolean;
|
|
36
|
+
export declare function stateOf(status: string, screen: Screen): DownSeat['state'];
|
|
37
|
+
export declare function runDown(argv: string[], io: Io, sources: DownSources): Promise<number>;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { readArgs } from "../args.js";
|
|
2
|
+
import { callerOf, describeCaller, mayChangeTeam } from "../caller.js";
|
|
3
|
+
import { currentTeam } from "../file/current.js";
|
|
4
|
+
import { agentList, agentStatus, paneForeground, paneRead, sessionRunning, pressEnter, sessionStop, typeText, workspaceClose, } from "../herdr.js";
|
|
5
|
+
import { executePlan } from "../launch/execute.js";
|
|
6
|
+
import { downPlan, formatPlan } from "../launch/plan.js";
|
|
7
|
+
import { profileFor } from "../profiles/index.js";
|
|
8
|
+
import { logLine } from "../log.js";
|
|
9
|
+
import { emptySession, readState, updateState } from "../state.js";
|
|
10
|
+
import { readScreen } from "../watch/screen.js";
|
|
11
|
+
function aim(session) {
|
|
12
|
+
return session === 'default' ? undefined : session;
|
|
13
|
+
}
|
|
14
|
+
const realLaunch = {
|
|
15
|
+
typeText: (session, pane, text) => typeText(pane, text, aim(session)),
|
|
16
|
+
pressEnter: (session, pane) => pressEnter(pane, aim(session)),
|
|
17
|
+
agentPanes(session) {
|
|
18
|
+
const agents = agentList(aim(session));
|
|
19
|
+
return agents === null ? null : agents.map((agent) => agent.pane);
|
|
20
|
+
},
|
|
21
|
+
closeWorkspace: (session, workspace) => workspaceClose(workspace, aim(session)),
|
|
22
|
+
stopSession: sessionStop,
|
|
23
|
+
kill(pid) {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 'SIGTERM');
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
33
|
+
now: () => new Date(),
|
|
34
|
+
};
|
|
35
|
+
export const realSources = {
|
|
36
|
+
sessionRunning,
|
|
37
|
+
agents: (session) => agentList(aim(session)),
|
|
38
|
+
alive(pid) {
|
|
39
|
+
try {
|
|
40
|
+
process.kill(pid, 0);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
screen(session, pane, cli) {
|
|
48
|
+
return readScreen(cli, paneRead(pane, 200, aim(session)) ?? undefined);
|
|
49
|
+
},
|
|
50
|
+
status: (session, pane) => agentStatus(pane, aim(session)),
|
|
51
|
+
foreground: (session, pane) => paneForeground(pane, aim(session)),
|
|
52
|
+
now: () => new Date(),
|
|
53
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
54
|
+
launch: realLaunch,
|
|
55
|
+
};
|
|
56
|
+
/** How long `--wait` gives a working seat, in seconds. */
|
|
57
|
+
export const WAIT_SECONDS = 120;
|
|
58
|
+
export const USAGE = 'Usage: team down [--dry-run] [--wait] [--abandon] [--session <name>] [--file <path>]\n';
|
|
59
|
+
export const down = (argv, io) => runDown(argv, io, realSources);
|
|
60
|
+
export default down;
|
|
61
|
+
// Free only when herdr says idle or done and the screen is an empty idle prompt. A permission
|
|
62
|
+
// prompt is idle to herdr, and typing `/exit` there would answer it.
|
|
63
|
+
// Herdr keeps a pane after the CLI exits, so the pane id staying listed is not "still there".
|
|
64
|
+
// The seat has left when no foreground process is its CLI. An unreadable list keeps the wait.
|
|
65
|
+
export function paneStillRunning(foreground, processNames) {
|
|
66
|
+
if (!foreground)
|
|
67
|
+
return true;
|
|
68
|
+
return foreground.some((name) => processNames.includes(name));
|
|
69
|
+
}
|
|
70
|
+
export function stateOf(status, screen) {
|
|
71
|
+
if (screen.kind === 'unsent')
|
|
72
|
+
return 'unsent';
|
|
73
|
+
if (screen.kind === 'permission' || screen.kind === 'trust' || screen.kind === 'question')
|
|
74
|
+
return 'blocked';
|
|
75
|
+
// A screen showing a running turn is working even when herdr's status has not caught up:
|
|
76
|
+
// `--wait` waits for it, and it is never typed into.
|
|
77
|
+
if (screen.kind === 'working')
|
|
78
|
+
return 'working';
|
|
79
|
+
if (screen.kind === 'unknown')
|
|
80
|
+
return 'unknown';
|
|
81
|
+
if (status === 'idle' || status === 'done')
|
|
82
|
+
return 'free';
|
|
83
|
+
if (status === 'working' || status === 'blocked')
|
|
84
|
+
return status;
|
|
85
|
+
return 'unknown';
|
|
86
|
+
}
|
|
87
|
+
export async function runDown(argv, io, sources) {
|
|
88
|
+
const args = readArgs(argv, ['session', 'file'], ['dry-run', 'wait', 'abandon']);
|
|
89
|
+
if (args.error || args.rest.length) {
|
|
90
|
+
io.stderr(`team down: ${args.error ?? `unexpected "${args.rest[0]}"`}\n${USAGE}`);
|
|
91
|
+
return 2;
|
|
92
|
+
}
|
|
93
|
+
const dry = args.flags.has('dry-run');
|
|
94
|
+
const current = currentTeam(io.cwd, args.values.file, sources.now());
|
|
95
|
+
if (!current.ok) {
|
|
96
|
+
for (const problem of current.errors) {
|
|
97
|
+
io.stderr(`team down: ${problem.line ? `line ${problem.line}: ` : ''}${problem.message}\n`);
|
|
98
|
+
}
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
if (current.notice)
|
|
102
|
+
io.stdout(`${current.notice}\n`);
|
|
103
|
+
const { team, dir } = current;
|
|
104
|
+
const session = args.values.session ?? team.session;
|
|
105
|
+
const running = sources.sessionRunning(session);
|
|
106
|
+
if (running === null) {
|
|
107
|
+
io.stderr("team down: herdr doesn't answer; is it installed and running?\n");
|
|
108
|
+
return 2;
|
|
109
|
+
}
|
|
110
|
+
if (!running) {
|
|
111
|
+
io.stdout(`session ${session} is not running: nothing to stop${dry ? '\ndry run: nothing was run' : ''}\n`);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
let agents = sources.agents(session);
|
|
115
|
+
if (agents === null) {
|
|
116
|
+
io.stderr(`team down: the agents of session ${session} can't be read\n`);
|
|
117
|
+
return 2;
|
|
118
|
+
}
|
|
119
|
+
const caller = callerOf(io, session === 'default' ? undefined : session);
|
|
120
|
+
const refusals = [];
|
|
121
|
+
if (!mayChangeTeam(caller, team)) {
|
|
122
|
+
refusals.push(`only the owner, the coordinator or the operator stops the team; this call is ${describeCaller(caller)}`);
|
|
123
|
+
}
|
|
124
|
+
const abandon = args.flags.has('abandon');
|
|
125
|
+
if (abandon && !callerOwns(caller)) {
|
|
126
|
+
refusals.push('only the owner abandons a team, from a terminal outside herdr');
|
|
127
|
+
}
|
|
128
|
+
if (!dry && args.flags.has('wait') && refusals.length === 0) {
|
|
129
|
+
const deadline = sources.now().getTime() + WAIT_SECONDS * 1000;
|
|
130
|
+
const sleep = sources.sleep ?? sources.launch?.sleep;
|
|
131
|
+
while (sleep && sources.now().getTime() < deadline) {
|
|
132
|
+
const waiting = agents.some((agent) => {
|
|
133
|
+
if (!agent.name)
|
|
134
|
+
return false;
|
|
135
|
+
const cli = team.seats.find((seat) => seat.name === agent.name)?.cli ?? 'claude-code';
|
|
136
|
+
return stateOf(agent.status, sources.screen(session, agent.pane, cli)) === 'working';
|
|
137
|
+
});
|
|
138
|
+
if (!waiting)
|
|
139
|
+
break;
|
|
140
|
+
const before = sources.now().getTime();
|
|
141
|
+
await sleep(2000);
|
|
142
|
+
if (sources.now().getTime() <= before)
|
|
143
|
+
break;
|
|
144
|
+
const again = sources.agents(session);
|
|
145
|
+
if (again === null)
|
|
146
|
+
break;
|
|
147
|
+
agents = again;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const state = readState(dir).sessions[session];
|
|
151
|
+
const known = new Set([...team.seats.map((seat) => seat.name), ...Object.keys(state?.seats ?? {})]);
|
|
152
|
+
const cliOf = new Map(team.seats.map((seat) => [seat.name, seat.cli]));
|
|
153
|
+
const seats = [];
|
|
154
|
+
let extra = 0;
|
|
155
|
+
for (const agent of agents) {
|
|
156
|
+
if (!agent.name || !known.has(agent.name)) {
|
|
157
|
+
extra++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const like = state?.seats[agent.name]?.temporary?.like;
|
|
161
|
+
const cli = cliOf.get(agent.name) ?? (like ? cliOf.get(like) : undefined) ?? 'unknown';
|
|
162
|
+
seats.push({
|
|
163
|
+
name: agent.name,
|
|
164
|
+
cli,
|
|
165
|
+
pane: agent.pane,
|
|
166
|
+
workspace: agent.workspace,
|
|
167
|
+
state: stateOf(agent.status, sources.screen(session, agent.pane, cli)),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const watch = state?.watch;
|
|
171
|
+
const plan = downPlan({
|
|
172
|
+
session,
|
|
173
|
+
seats,
|
|
174
|
+
extra,
|
|
175
|
+
watchPid: watch && sources.alive(watch.pid) ? watch.pid : null,
|
|
176
|
+
keep: caller.kind === 'seat' ? [team.coordinator, team.operator] : [],
|
|
177
|
+
abandon: abandon && callerOwns(caller),
|
|
178
|
+
});
|
|
179
|
+
if (dry) {
|
|
180
|
+
for (const refusal of refusals)
|
|
181
|
+
io.stdout(`! down would refuse: ${refusal}\n`);
|
|
182
|
+
io.stdout(formatPlan(plan));
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
if (refusals.length) {
|
|
186
|
+
for (const refusal of refusals)
|
|
187
|
+
io.stderr(`team down: ${refusal}\n`);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
const launch = sources.launch;
|
|
191
|
+
if (!launch) {
|
|
192
|
+
io.stderr('team down: this call has no way to reach herdr\n');
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
const now = () => sources.now();
|
|
196
|
+
const host = {
|
|
197
|
+
startServer: () => false,
|
|
198
|
+
sessionUp: () => true,
|
|
199
|
+
createWorkspace: () => null,
|
|
200
|
+
paneRun: () => false,
|
|
201
|
+
typeLine(sessionName, pane, text) {
|
|
202
|
+
// Free was decided when the plan was built. Look again, and once more between the text and
|
|
203
|
+
// the Enter: a prompt that appeared would take the key, as the watch's nudge does. Herdr's
|
|
204
|
+
// status is asked both times; Enter waits until it is idle or done and the screen is idle
|
|
205
|
+
// or holding unsent text.
|
|
206
|
+
const cli = seats.find((seat) => seat.pane === pane)?.cli ?? '';
|
|
207
|
+
const look = () => sources.screen(sessionName, pane, cli).kind;
|
|
208
|
+
const resting = () => {
|
|
209
|
+
const status = sources.status(sessionName, pane);
|
|
210
|
+
return status === 'idle' || status === 'done';
|
|
211
|
+
};
|
|
212
|
+
if (!resting() || look() !== 'idle')
|
|
213
|
+
return false;
|
|
214
|
+
if (!launch.typeText(sessionName, pane, text))
|
|
215
|
+
return false;
|
|
216
|
+
const after = look();
|
|
217
|
+
if (!resting() || (after !== 'idle' && after !== 'unsent'))
|
|
218
|
+
return false;
|
|
219
|
+
return launch.pressEnter(sessionName, pane);
|
|
220
|
+
},
|
|
221
|
+
renameAgent: () => false,
|
|
222
|
+
closeWorkspace: launch.closeWorkspace,
|
|
223
|
+
stopSession: launch.stopSession,
|
|
224
|
+
kill: launch.kill,
|
|
225
|
+
agentPanes(sessionName) {
|
|
226
|
+
const listed = launch.agentPanes(sessionName);
|
|
227
|
+
if (!listed)
|
|
228
|
+
return null;
|
|
229
|
+
// A pane back at its shell is no longer the seat. `gone` then finishes and the workspace closes.
|
|
230
|
+
return listed.filter((pane) => {
|
|
231
|
+
const cli = seats.find((seat) => seat.pane === pane)?.cli;
|
|
232
|
+
const names = cli ? profileFor(cli)?.processNames : undefined;
|
|
233
|
+
if (!names)
|
|
234
|
+
return true;
|
|
235
|
+
return paneStillRunning(sources.foreground?.(sessionName, pane) ?? paneForeground(pane, aim(sessionName)), names);
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
classify: () => 'unknown',
|
|
239
|
+
sleep: sources.sleep ?? launch.sleep,
|
|
240
|
+
now: () => now().getTime(),
|
|
241
|
+
allow: () => null,
|
|
242
|
+
record() { },
|
|
243
|
+
running() { },
|
|
244
|
+
drop(name) {
|
|
245
|
+
updateState(dir, (file) => {
|
|
246
|
+
const seatsOf = (file.sessions[session] ??= emptySession()).seats;
|
|
247
|
+
delete seatsOf[name];
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
say: (line) => io.stdout(line),
|
|
251
|
+
log: (who, what) => logLine(dir, 'down', describeCaller(caller), `${who}: ${what}`, now()),
|
|
252
|
+
};
|
|
253
|
+
const report = await executePlan(plan, session, host);
|
|
254
|
+
return report.held ? 1 : 0;
|
|
255
|
+
}
|
|
256
|
+
function callerOwns(caller) {
|
|
257
|
+
return caller.kind === 'owner';
|
|
258
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Command, Io } from '../io.ts';
|
|
2
|
+
export declare const EXCLUDED: string[];
|
|
3
|
+
export declare const PRIVACY = "The team file is private to this clone: it is listed in .git/info/exclude, never in .gitignore,\nso git doesn't see it and a fresh clone doesn't carry it. A collaborator writes their own, or\nreceives one and approves it themselves.";
|
|
4
|
+
export declare function skeleton(project: string, head: string | null): string;
|
|
5
|
+
export declare function exclude(root: string): string[];
|
|
6
|
+
export declare const init: Command;
|
|
7
|
+
export default init;
|
|
8
|
+
export declare const USAGE = "Usage: team init [--restore]\n";
|
|
9
|
+
export declare function runInit(argv: string[], io: Io, home?: string): Promise<number>;
|