@openfleet-tools/cli 0.0.0 → 0.1.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/LICENSE +21 -0
- package/core/hooks.json +46 -0
- package/core/plugin.json +9 -0
- package/core/skills/openfleet-contracts/SKILL.md +31 -0
- package/core/skills/openfleet-finalize/SKILL.md +53 -0
- package/core/skills/openfleet-grill/SKILL.md +48 -0
- package/core/skills/openfleet-orchestrate/SKILL.md +96 -0
- package/core/skills/openfleet-records/SKILL.md +73 -0
- package/core/skills/openfleet-spec/SKILL.md +65 -0
- package/core/templates/agents/openfleet-coordinator.agent.md +53 -0
- package/core/templates/agents/openfleet-implementer.agent.md +36 -0
- package/core/templates/agents/openfleet-planner.agent.md +56 -0
- package/core/templates/agents/openfleet-tester.agent.md +42 -0
- package/dist/attribution.d.ts +21 -0
- package/dist/attribution.js +72 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +134 -0
- package/dist/commands/extensions.d.ts +7 -0
- package/dist/commands/extensions.js +60 -0
- package/dist/commands/internal.d.ts +19 -0
- package/dist/commands/internal.js +117 -0
- package/dist/commands/output.d.ts +2 -0
- package/dist/commands/output.js +6 -0
- package/dist/commands/project.d.ts +23 -0
- package/dist/commands/project.js +207 -0
- package/dist/commands/workspace.d.ts +10 -0
- package/dist/commands/workspace.js +63 -0
- package/dist/core.d.ts +17 -0
- package/dist/core.js +139 -0
- package/dist/coreindex.d.ts +17 -0
- package/dist/coreindex.js +7 -0
- package/dist/events.d.ts +29 -0
- package/dist/events.js +115 -0
- package/dist/extensions.d.ts +71 -0
- package/dist/extensions.js +297 -0
- package/dist/fsutil.d.ts +12 -0
- package/dist/fsutil.js +70 -0
- package/dist/git.d.ts +27 -0
- package/dist/git.js +97 -0
- package/dist/hooks/handlers.d.ts +43 -0
- package/dist/hooks/handlers.js +363 -0
- package/dist/hooks/main.d.ts +2 -0
- package/dist/hooks/main.js +37 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/launcher.d.ts +24 -0
- package/dist/launcher.js +75 -0
- package/dist/lock.d.ts +6 -0
- package/dist/lock.js +45 -0
- package/dist/redact.d.ts +2 -0
- package/dist/redact.js +39 -0
- package/dist/repos.d.ts +28 -0
- package/dist/repos.js +10 -0
- package/dist/state.d.ts +68 -0
- package/dist/state.js +111 -0
- package/dist/workspace.d.ts +36 -0
- package/dist/workspace.js +74 -0
- package/package.json +50 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: openfleet-tester
|
|
3
|
+
description: OpenFleet per-repo tester. Builds and tests one worktree and verifies that contract tests exist and pass. Does not modify source.
|
|
4
|
+
tools: ["read", "search", "execute"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are an **OpenFleet tester** for exactly one child. The `# OpenFleet assignment` block at the top of your prompt names the child, the worktree, the spec, the contracts and the plan.
|
|
8
|
+
|
|
9
|
+
You are **read-only on source**. Do not edit, create or delete files in the worktree. You may only create throwaway files under the system temp directory. If something must change, report it as a finding for the implementer.
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
Every shell command must start with `cd <worktree> &&`.
|
|
14
|
+
|
|
15
|
+
1. Run the child's **build** command from `spec.md`.
|
|
16
|
+
2. Run the child's **test** command from `spec.md`.
|
|
17
|
+
3. **Contract tests:** for each contract that this child produces or consumes (see the spec and plan), confirm that a contract test exists, that it uses the shared fixture from `contracts/`, and that it ran and passed in step 2. A missing contract test is a failure.
|
|
18
|
+
4. Check each acceptance criterion for this child in `spec.md`. Use code reading or targeted commands.
|
|
19
|
+
5. Record each command you run:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npx openfleet event <project> --actor "<child id>/tester" --type test.run --target "<child id>" \
|
|
23
|
+
--payload '{"command": "<cmd>", "exitCode": <n>, "passed": <true|false>, "summary": "<one line>"}'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Run this from the workspace root, not from the worktree.
|
|
27
|
+
|
|
28
|
+
## Findings
|
|
29
|
+
|
|
30
|
+
When something fails, write findings that the implementer can act on without re-running everything: the failing command, the relevant output excerpt (trimmed), the file and line if known, and the likely cause. Do not paste whole logs.
|
|
31
|
+
|
|
32
|
+
## Report
|
|
33
|
+
|
|
34
|
+
End with:
|
|
35
|
+
|
|
36
|
+
````
|
|
37
|
+
```openfleet-report
|
|
38
|
+
{"status": "done" | "failed", "child": "<child id>", "summary": "<pass/fail summary>", "findings": ["<finding>", "…"]}
|
|
39
|
+
```
|
|
40
|
+
````
|
|
41
|
+
|
|
42
|
+
Use `status: "done"` only when build, tests and all contract tests pass, and every acceptance criterion is met.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ChildState, type ProjectState, type Role } from "./state.js";
|
|
2
|
+
import { type ProjectPaths, type Workspace } from "./workspace.js";
|
|
3
|
+
export declare function roleOf(child: ChildState): Role | undefined;
|
|
4
|
+
/** Copilot namespaces plugin agents as `<plugin>:<agent>`. */
|
|
5
|
+
export declare const PLUGIN_NAME = "openfleet";
|
|
6
|
+
export declare function bareAgentName(agentName: string | undefined): string | undefined;
|
|
7
|
+
export declare function qualifiedAgentName(agent: string): string;
|
|
8
|
+
export declare function roleFromAgentName(agentName: string | undefined): Role | "coordinator" | undefined;
|
|
9
|
+
export declare function actorFor(childId: string, role: string | undefined): string;
|
|
10
|
+
export interface Located {
|
|
11
|
+
project: ProjectPaths;
|
|
12
|
+
state: ProjectState;
|
|
13
|
+
childId: string;
|
|
14
|
+
child: ChildState;
|
|
15
|
+
/** Path relative to the child's worktree. */
|
|
16
|
+
rel: string;
|
|
17
|
+
}
|
|
18
|
+
/** Candidate projects: the explicit one if given, otherwise every project in the workspace. */
|
|
19
|
+
export declare function candidateProjects(ws: Workspace, project?: string): ProjectPaths[];
|
|
20
|
+
/** Attribute an absolute file path to project → child worktree (longest match wins). */
|
|
21
|
+
export declare function locatePath(ws: Workspace, absPath: string, project?: string): Located | undefined;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { isInside, realish } from "./fsutil.js";
|
|
3
|
+
import { readState } from "./state.js";
|
|
4
|
+
import { listProjectNames, projectPaths } from "./workspace.js";
|
|
5
|
+
const PHASE_ROLE = {
|
|
6
|
+
planning: "planner",
|
|
7
|
+
"plan-review": "planner",
|
|
8
|
+
implementing: "implementer",
|
|
9
|
+
testing: "tester",
|
|
10
|
+
};
|
|
11
|
+
export function roleOf(child) {
|
|
12
|
+
return child.role ?? PHASE_ROLE[child.phase];
|
|
13
|
+
}
|
|
14
|
+
/** Copilot namespaces plugin agents as `<plugin>:<agent>`. */
|
|
15
|
+
export const PLUGIN_NAME = "openfleet";
|
|
16
|
+
export function bareAgentName(agentName) {
|
|
17
|
+
return agentName?.startsWith(`${PLUGIN_NAME}:`) ? agentName.slice(PLUGIN_NAME.length + 1) : agentName;
|
|
18
|
+
}
|
|
19
|
+
export function qualifiedAgentName(agent) {
|
|
20
|
+
return agent.includes(":") ? agent : `${PLUGIN_NAME}:${agent}`;
|
|
21
|
+
}
|
|
22
|
+
export function roleFromAgentName(agentName) {
|
|
23
|
+
switch (bareAgentName(agentName)) {
|
|
24
|
+
case "openfleet-planner":
|
|
25
|
+
return "planner";
|
|
26
|
+
case "openfleet-implementer":
|
|
27
|
+
return "implementer";
|
|
28
|
+
case "openfleet-tester":
|
|
29
|
+
return "tester";
|
|
30
|
+
case "openfleet-coordinator":
|
|
31
|
+
return "coordinator";
|
|
32
|
+
default:
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function actorFor(childId, role) {
|
|
37
|
+
return `${childId}/${role ?? "unknown"}`;
|
|
38
|
+
}
|
|
39
|
+
function safeState(p) {
|
|
40
|
+
try {
|
|
41
|
+
return readState(p);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Candidate projects: the explicit one if given, otherwise every project in the workspace. */
|
|
48
|
+
export function candidateProjects(ws, project) {
|
|
49
|
+
const names = project ? [project] : listProjectNames(ws);
|
|
50
|
+
return names.map((n) => projectPaths(ws, n));
|
|
51
|
+
}
|
|
52
|
+
/** Attribute an absolute file path to project → child worktree (longest match wins). */
|
|
53
|
+
export function locatePath(ws, absPath, project) {
|
|
54
|
+
let best;
|
|
55
|
+
let bestLen = -1;
|
|
56
|
+
const target = realish(absPath);
|
|
57
|
+
for (const p of candidateProjects(ws, project)) {
|
|
58
|
+
const state = safeState(p);
|
|
59
|
+
if (!state)
|
|
60
|
+
continue;
|
|
61
|
+
for (const [childId, child] of Object.entries(state.children)) {
|
|
62
|
+
if (!child.worktree)
|
|
63
|
+
continue;
|
|
64
|
+
const wt = realish(child.worktree);
|
|
65
|
+
if (!isInside(wt, target) || wt.length <= bestLen)
|
|
66
|
+
continue;
|
|
67
|
+
bestLen = wt.length;
|
|
68
|
+
best = { project: p, state, childId, child, rel: path.relative(wt, target) };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return best;
|
|
72
|
+
}
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const HELP = "openfleet \u2014 coordinate one change across many repos with GitHub Copilot CLI\n\nUsage: npx openfleet <command> [options] [-- <copilot args>]\n\nWorkspace\n init Create openfleet/ in the current directory and install core\n update Replace openfleet/core with this CLI version and re-sync extensions\n\nProjects\n new <name> [\"<request>\"] [--ask]\n Create a project and launch the coordinator\n resume <name> [--fresh] [--ask]\n Continue a project (--fresh starts a new Copilot session)\n status [name] [--json] Show a project's children, phases and open questions\n list List projects\n clean <name> [--branches] [--force]\n Remove the project's worktrees (and branches)\n\nExtensions\n add <git-url[#ref]|path> Install or update an extension\n remove <name> Uninstall an extension\n extensions List installed extensions\n\nAgent helpers (used by OpenFleet agents)\n state <project> [--patch <json>] [--actor <a>]\n event <project> --type <t> [--actor <a>] [--target <child>] [--payload <json>] [--patch-file <f>]\n worktree <project> <child> --repo <key> --branch <b> [--package <p>] [--base <ref>]\n\n help, version\nSessions auto-approve tool use (`--allow-all-tools`). File access stays limited to the workspace,\nits worktrees and known repos, and URL access still prompts. Pass `--ask` to `new`, `resume` or an\nextension command to be prompted for every tool instead.\nArguments after `--` are passed to copilot unchanged (e.g. `-- --yolo`).\n";
|
|
2
|
+
export declare function run(argv: string[]): number;
|
|
3
|
+
export declare function main(argv?: string[]): void;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { packageVersion } from "./core.js";
|
|
3
|
+
import { cmdAdd, cmdExtensions, cmdRemove, runExtensionCommand } from "./commands/extensions.js";
|
|
4
|
+
import { cmdEvent, cmdState, cmdWorktree } from "./commands/internal.js";
|
|
5
|
+
import { out } from "./commands/output.js";
|
|
6
|
+
import { cmdClean, cmdList, cmdNew, cmdResume, cmdStatus } from "./commands/project.js";
|
|
7
|
+
import { cmdInit, cmdUpdate } from "./commands/workspace.js";
|
|
8
|
+
import { UserError } from "./workspace.js";
|
|
9
|
+
export const HELP = `openfleet — coordinate one change across many repos with GitHub Copilot CLI
|
|
10
|
+
|
|
11
|
+
Usage: npx openfleet <command> [options] [-- <copilot args>]
|
|
12
|
+
|
|
13
|
+
Workspace
|
|
14
|
+
init Create openfleet/ in the current directory and install core
|
|
15
|
+
update Replace openfleet/core with this CLI version and re-sync extensions
|
|
16
|
+
|
|
17
|
+
Projects
|
|
18
|
+
new <name> ["<request>"] [--ask]
|
|
19
|
+
Create a project and launch the coordinator
|
|
20
|
+
resume <name> [--fresh] [--ask]
|
|
21
|
+
Continue a project (--fresh starts a new Copilot session)
|
|
22
|
+
status [name] [--json] Show a project's children, phases and open questions
|
|
23
|
+
list List projects
|
|
24
|
+
clean <name> [--branches] [--force]
|
|
25
|
+
Remove the project's worktrees (and branches)
|
|
26
|
+
|
|
27
|
+
Extensions
|
|
28
|
+
add <git-url[#ref]|path> Install or update an extension
|
|
29
|
+
remove <name> Uninstall an extension
|
|
30
|
+
extensions List installed extensions
|
|
31
|
+
|
|
32
|
+
Agent helpers (used by OpenFleet agents)
|
|
33
|
+
state <project> [--patch <json>] [--actor <a>]
|
|
34
|
+
event <project> --type <t> [--actor <a>] [--target <child>] [--payload <json>] [--patch-file <f>]
|
|
35
|
+
worktree <project> <child> --repo <key> --branch <b> [--package <p>] [--base <ref>]
|
|
36
|
+
|
|
37
|
+
help, version
|
|
38
|
+
Sessions auto-approve tool use (\`--allow-all-tools\`). File access stays limited to the workspace,
|
|
39
|
+
its worktrees and known repos, and URL access still prompts. Pass \`--ask\` to \`new\`, \`resume\` or an
|
|
40
|
+
extension command to be prompted for every tool instead.
|
|
41
|
+
Arguments after \`--\` are passed to copilot unchanged (e.g. \`-- --yolo\`).
|
|
42
|
+
`;
|
|
43
|
+
function splitPassthrough(argv) {
|
|
44
|
+
const i = argv.indexOf("--");
|
|
45
|
+
return i < 0 ? [argv, []] : [argv.slice(0, i), argv.slice(i + 1)];
|
|
46
|
+
}
|
|
47
|
+
function parse(args, options) {
|
|
48
|
+
return parseArgs({ args, options, allowPositionals: true, strict: true });
|
|
49
|
+
}
|
|
50
|
+
export function run(argv) {
|
|
51
|
+
const [args, passthrough] = splitPassthrough(argv);
|
|
52
|
+
const [command, ...rest] = args;
|
|
53
|
+
switch (command) {
|
|
54
|
+
case undefined:
|
|
55
|
+
case "help":
|
|
56
|
+
case "--help":
|
|
57
|
+
case "-h":
|
|
58
|
+
out(HELP);
|
|
59
|
+
return 0;
|
|
60
|
+
case "version":
|
|
61
|
+
case "--version":
|
|
62
|
+
case "-v":
|
|
63
|
+
out(packageVersion());
|
|
64
|
+
return 0;
|
|
65
|
+
case "init":
|
|
66
|
+
return cmdInit();
|
|
67
|
+
case "update":
|
|
68
|
+
return cmdUpdate();
|
|
69
|
+
case "new": {
|
|
70
|
+
const { values, positionals } = parse(rest, { ask: { type: "boolean" } });
|
|
71
|
+
return cmdNew(positionals, passthrough, { ask: values.ask });
|
|
72
|
+
}
|
|
73
|
+
case "resume": {
|
|
74
|
+
const { values, positionals } = parse(rest, { fresh: { type: "boolean" }, ask: { type: "boolean" } });
|
|
75
|
+
return cmdResume(positionals, passthrough, { fresh: values.fresh, ask: values.ask });
|
|
76
|
+
}
|
|
77
|
+
case "status": {
|
|
78
|
+
const { values, positionals } = parse(rest, { json: { type: "boolean" } });
|
|
79
|
+
return cmdStatus(positionals, { json: values.json });
|
|
80
|
+
}
|
|
81
|
+
case "list":
|
|
82
|
+
return cmdList();
|
|
83
|
+
case "clean": {
|
|
84
|
+
const { values, positionals } = parse(rest, { branches: { type: "boolean" }, force: { type: "boolean" } });
|
|
85
|
+
return cmdClean(positionals, values);
|
|
86
|
+
}
|
|
87
|
+
case "add":
|
|
88
|
+
return cmdAdd(parse(rest, {}).positionals);
|
|
89
|
+
case "remove":
|
|
90
|
+
return cmdRemove(parse(rest, {}).positionals);
|
|
91
|
+
case "extensions":
|
|
92
|
+
return cmdExtensions();
|
|
93
|
+
case "state": {
|
|
94
|
+
const { values, positionals } = parse(rest, { patch: { type: "string" }, actor: { type: "string" } });
|
|
95
|
+
return cmdState(positionals, values);
|
|
96
|
+
}
|
|
97
|
+
case "event": {
|
|
98
|
+
const { values, positionals } = parse(rest, {
|
|
99
|
+
type: { type: "string" },
|
|
100
|
+
actor: { type: "string" },
|
|
101
|
+
target: { type: "string" },
|
|
102
|
+
payload: { type: "string" },
|
|
103
|
+
"patch-file": { type: "string" },
|
|
104
|
+
});
|
|
105
|
+
return cmdEvent(positionals, values);
|
|
106
|
+
}
|
|
107
|
+
case "worktree": {
|
|
108
|
+
const { values, positionals } = parse(rest, {
|
|
109
|
+
repo: { type: "string" },
|
|
110
|
+
branch: { type: "string" },
|
|
111
|
+
package: { type: "string" },
|
|
112
|
+
base: { type: "string" },
|
|
113
|
+
});
|
|
114
|
+
return cmdWorktree(positionals, values);
|
|
115
|
+
}
|
|
116
|
+
default: {
|
|
117
|
+
const ask = rest.includes("--ask");
|
|
118
|
+
const code = runExtensionCommand(command, rest.filter((a) => a !== "--ask"), passthrough, { ask });
|
|
119
|
+
if (code === undefined)
|
|
120
|
+
throw new UserError(`Unknown command "${command}". Run \`npx openfleet help\`.`);
|
|
121
|
+
return code;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function main(argv = process.argv.slice(2)) {
|
|
126
|
+
try {
|
|
127
|
+
process.exitCode = run(argv);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const isUser = err instanceof UserError || err.code?.startsWith("ERR_PARSE_ARGS");
|
|
131
|
+
process.stderr.write(`openfleet: ${isUser ? err.message : err.stack ?? String(err)}\n`);
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function cmdAdd(args: string[]): number;
|
|
2
|
+
export declare function cmdRemove(args: string[]): number;
|
|
3
|
+
export declare function cmdExtensions(): number;
|
|
4
|
+
/** Try to run an extension-declared command. Returns undefined when no extension declares it. */
|
|
5
|
+
export declare function runExtensionCommand(command: string, args: string[], passthrough: string[], opts?: {
|
|
6
|
+
ask?: boolean;
|
|
7
|
+
}): number | undefined;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { syncExtensions, installedCoreVersion, packageVersion } from "../core.js";
|
|
2
|
+
import { readCoreIndex } from "../coreindex.js";
|
|
3
|
+
import { addExtension, describePermissions, readLock, removeExtension } from "../extensions.js";
|
|
4
|
+
import { launch } from "../launcher.js";
|
|
5
|
+
import { requireWorkspace, UserError } from "../workspace.js";
|
|
6
|
+
import { out, warn } from "./output.js";
|
|
7
|
+
export function cmdAdd(args) {
|
|
8
|
+
const ws = requireWorkspace();
|
|
9
|
+
const source = args[0];
|
|
10
|
+
if (!source)
|
|
11
|
+
throw new UserError("Usage: openfleet add <git-url[#ref]|path>");
|
|
12
|
+
const coreVersion = installedCoreVersion(ws) ?? packageVersion();
|
|
13
|
+
const { entry, manifest, replaced } = addExtension(ws, source, coreVersion);
|
|
14
|
+
syncExtensions(ws, warn);
|
|
15
|
+
out(`${replaced ? "Updated" : "Added"} extension ${manifest.name}@${manifest.version} (${entry.sha === "local" ? "local" : entry.sha.slice(0, 12)}).`);
|
|
16
|
+
out(manifest.mode === "phase"
|
|
17
|
+
? `Mode: phase, slot ${manifest.slot} (runs as agent openfleet:openfleet-ext-${manifest.name}).`
|
|
18
|
+
: `Mode: augment, appended to the ${manifest.role} agent's instructions.`);
|
|
19
|
+
out("Declared permissions:");
|
|
20
|
+
out(describePermissions(manifest));
|
|
21
|
+
const cmds = Object.keys(manifest.commands ?? {});
|
|
22
|
+
if (cmds.length)
|
|
23
|
+
out(`Commands: ${cmds.map((c) => `npx openfleet ${c}`).join(", ")}`);
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
export function cmdRemove(args) {
|
|
27
|
+
const ws = requireWorkspace();
|
|
28
|
+
const name = args[0];
|
|
29
|
+
if (!name)
|
|
30
|
+
throw new UserError("Usage: openfleet remove <extension>");
|
|
31
|
+
removeExtension(ws, name);
|
|
32
|
+
syncExtensions(ws, warn);
|
|
33
|
+
out(`Removed extension ${name}.`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
export function cmdExtensions() {
|
|
37
|
+
const ws = requireWorkspace();
|
|
38
|
+
const lock = readLock(ws);
|
|
39
|
+
if (!lock.extensions.length) {
|
|
40
|
+
out("No extensions installed. Add one with `npx openfleet add <git-url|path>`.");
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
for (const e of lock.extensions) {
|
|
44
|
+
out(`${e.enabled ? "✓" : "✗"} ${e.name}@${e.version} ${e.source} ${e.sha === "local" ? "local" : e.sha.slice(0, 12)}${e.disabledReason ? ` (disabled: ${e.disabledReason})` : ""}`);
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
/** Try to run an extension-declared command. Returns undefined when no extension declares it. */
|
|
49
|
+
export function runExtensionCommand(command, args, passthrough, opts = {}) {
|
|
50
|
+
const ws = requireWorkspace();
|
|
51
|
+
const cmd = readCoreIndex(ws)?.commands[command];
|
|
52
|
+
if (!cmd)
|
|
53
|
+
return undefined;
|
|
54
|
+
const prompt = [
|
|
55
|
+
`Run the OpenFleet extension command \`openfleet ${command}\` from extension \`${cmd.extension}\`.`,
|
|
56
|
+
`Use the skill \`${cmd.skill}\` and follow its instructions exactly.`,
|
|
57
|
+
args.length ? `Arguments: ${args.join(" ")}` : "No arguments were given.",
|
|
58
|
+
].join("\n");
|
|
59
|
+
return launch({ ws, agent: cmd.agent, prompt, name: `openfleet ${command}`, passthrough, ask: opts.ask });
|
|
60
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface EventFlags {
|
|
2
|
+
type?: string;
|
|
3
|
+
actor?: string;
|
|
4
|
+
target?: string;
|
|
5
|
+
payload?: string;
|
|
6
|
+
"patch-file"?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function cmdEvent(args: string[], flags: EventFlags): number;
|
|
9
|
+
export declare function cmdState(args: string[], flags: {
|
|
10
|
+
patch?: string;
|
|
11
|
+
actor?: string;
|
|
12
|
+
}): number;
|
|
13
|
+
export interface WorktreeFlags {
|
|
14
|
+
repo?: string;
|
|
15
|
+
branch?: string;
|
|
16
|
+
package?: string;
|
|
17
|
+
base?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function cmdWorktree(args: string[], flags: WorktreeFlags): number;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Helper commands used by OpenFleet agents (see the openfleet-records skill).
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { appendEvent } from "../events.js";
|
|
5
|
+
import { addWorktree, git, headSha, isGitRepo } from "../git.js";
|
|
6
|
+
import { readRepos } from "../repos.js";
|
|
7
|
+
import { patchState, readState, updateState } from "../state.js";
|
|
8
|
+
import { projectPaths, requireWorkspace, UserError, validateProjectName } from "../workspace.js";
|
|
9
|
+
import { out } from "./output.js";
|
|
10
|
+
function parseJson(raw, what) {
|
|
11
|
+
if (raw === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
throw new UserError(`Invalid JSON for ${what}: ${err.message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function cmdEvent(args, flags) {
|
|
21
|
+
const ws = requireWorkspace();
|
|
22
|
+
const name = validateProjectName(args[0]);
|
|
23
|
+
const p = projectPaths(ws, name);
|
|
24
|
+
readState(p);
|
|
25
|
+
if (!flags.type)
|
|
26
|
+
throw new UserError("--type is required");
|
|
27
|
+
const payload = parseJson(flags.payload, "--payload") ?? {};
|
|
28
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
29
|
+
throw new UserError("--payload must be a JSON object");
|
|
30
|
+
}
|
|
31
|
+
const patch = flags["patch-file"] ? fs.readFileSync(flags["patch-file"], "utf8") : undefined;
|
|
32
|
+
const ev = appendEvent(p, {
|
|
33
|
+
actor: flags.actor ?? "coordinator",
|
|
34
|
+
type: flags.type,
|
|
35
|
+
target: flags.target ?? null,
|
|
36
|
+
payload: payload,
|
|
37
|
+
...(patch !== undefined ? { patch } : {}),
|
|
38
|
+
});
|
|
39
|
+
out(JSON.stringify({ seq: ev.seq }));
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
export function cmdState(args, flags) {
|
|
43
|
+
const ws = requireWorkspace();
|
|
44
|
+
const name = validateProjectName(args[0]);
|
|
45
|
+
const p = projectPaths(ws, name);
|
|
46
|
+
const state = flags.patch !== undefined ? patchState(p, parseJson(flags.patch, "--patch"), flags.actor) : readState(p);
|
|
47
|
+
out(JSON.stringify(state, null, 2));
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
/** Resolve a repos.json entry to a local checkout, cloning `url` into `openfleet/repos/<key>` if needed. */
|
|
51
|
+
function resolveRepoPath(wsRoot, wsDir, key, entry) {
|
|
52
|
+
if (entry.path) {
|
|
53
|
+
const abs = path.resolve(wsRoot, entry.path);
|
|
54
|
+
if (!isGitRepo(abs))
|
|
55
|
+
throw new UserError(`repos.json "${key}".path is not a git repository: ${abs}`);
|
|
56
|
+
return abs;
|
|
57
|
+
}
|
|
58
|
+
if (!entry.url)
|
|
59
|
+
throw new UserError(`repos.json "${key}" needs a "path" or "url".`);
|
|
60
|
+
const dest = path.join(wsDir, "repos", key);
|
|
61
|
+
if (!isGitRepo(dest)) {
|
|
62
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
63
|
+
git(wsRoot, ["clone", "--quiet", entry.url, dest]);
|
|
64
|
+
}
|
|
65
|
+
return dest;
|
|
66
|
+
}
|
|
67
|
+
export function cmdWorktree(args, flags) {
|
|
68
|
+
const ws = requireWorkspace();
|
|
69
|
+
const name = validateProjectName(args[0]);
|
|
70
|
+
const childId = args[1];
|
|
71
|
+
if (!childId || !/^[A-Za-z0-9._-]+(:[A-Za-z0-9._@/-]+)?$/.test(childId)) {
|
|
72
|
+
throw new UserError("Usage: openfleet worktree <project> <child id> --repo <key> --branch <branch>");
|
|
73
|
+
}
|
|
74
|
+
const p = projectPaths(ws, name);
|
|
75
|
+
const state = readState(p);
|
|
76
|
+
const repoKey = flags.repo ?? state.children[childId]?.repo ?? childId.split(":")[0];
|
|
77
|
+
const entry = readRepos(ws).repos[repoKey];
|
|
78
|
+
if (!entry)
|
|
79
|
+
throw new UserError(`Repo "${repoKey}" is not in openfleet/repos.json.`);
|
|
80
|
+
const branch = flags.branch ?? state.children[childId]?.branch;
|
|
81
|
+
if (!branch)
|
|
82
|
+
throw new UserError("--branch is required");
|
|
83
|
+
const repoPath = resolveRepoPath(ws.root, ws.dir, repoKey, entry);
|
|
84
|
+
const worktree = path.join(p.worktrees, childId.replace(/[:/]/g, "__"));
|
|
85
|
+
const base = flags.base ?? entry.defaultBranch;
|
|
86
|
+
const existed = fs.existsSync(worktree);
|
|
87
|
+
if (!existed)
|
|
88
|
+
addWorktree(repoPath, worktree, branch, base);
|
|
89
|
+
const baseSha = state.children[childId]?.baseSha ?? headSha(worktree);
|
|
90
|
+
const next = updateState(p, (s) => {
|
|
91
|
+
const prev = s.children[childId];
|
|
92
|
+
s.children[childId] = {
|
|
93
|
+
phase: "pending",
|
|
94
|
+
retries: 0,
|
|
95
|
+
dependsOn: [],
|
|
96
|
+
agentIds: {},
|
|
97
|
+
...prev,
|
|
98
|
+
repo: repoKey,
|
|
99
|
+
repoPath,
|
|
100
|
+
worktree,
|
|
101
|
+
branch,
|
|
102
|
+
...(base ? { baseBranch: base } : {}),
|
|
103
|
+
...(baseSha ? { baseSha } : {}),
|
|
104
|
+
...(flags.package ?? prev?.package ? { package: flags.package ?? prev?.package } : {}),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
if (!existed) {
|
|
108
|
+
appendEvent(p, {
|
|
109
|
+
actor: "coordinator",
|
|
110
|
+
type: "worktree.created",
|
|
111
|
+
target: childId,
|
|
112
|
+
payload: { repo: repoKey, repoPath, worktree, branch, base: base ?? null, baseSha: baseSha ?? null },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
out(JSON.stringify(next.children[childId], null, 2));
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ProjectState } from "../state.js";
|
|
2
|
+
import { type Workspace } from "../workspace.js";
|
|
3
|
+
/** Other active projects touching any of the same repos. */
|
|
4
|
+
export declare function findOverlaps(ws: Workspace, name: string): {
|
|
5
|
+
project: string;
|
|
6
|
+
repos: string[];
|
|
7
|
+
}[];
|
|
8
|
+
export declare function createProject(ws: Workspace, name: string): ProjectState;
|
|
9
|
+
export declare function cmdNew(args: string[], passthrough: string[], opts?: {
|
|
10
|
+
ask?: boolean;
|
|
11
|
+
}): number;
|
|
12
|
+
export declare function cmdResume(args: string[], passthrough: string[], opts?: {
|
|
13
|
+
fresh?: boolean;
|
|
14
|
+
ask?: boolean;
|
|
15
|
+
}): number;
|
|
16
|
+
export declare function cmdList(): number;
|
|
17
|
+
export declare function cmdStatus(args: string[], opts?: {
|
|
18
|
+
json?: boolean;
|
|
19
|
+
}): number;
|
|
20
|
+
export declare function cmdClean(args: string[], opts?: {
|
|
21
|
+
branches?: boolean;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
}): number;
|