@openfleet-tools/cli 0.0.0-stage → 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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/core/hooks.json +46 -0
  3. package/core/plugin.json +9 -0
  4. package/core/skills/openfleet-contracts/SKILL.md +31 -0
  5. package/core/skills/openfleet-finalize/SKILL.md +53 -0
  6. package/core/skills/openfleet-grill/SKILL.md +48 -0
  7. package/core/skills/openfleet-orchestrate/SKILL.md +96 -0
  8. package/core/skills/openfleet-records/SKILL.md +73 -0
  9. package/core/skills/openfleet-spec/SKILL.md +65 -0
  10. package/core/templates/agents/openfleet-coordinator.agent.md +53 -0
  11. package/core/templates/agents/openfleet-implementer.agent.md +36 -0
  12. package/core/templates/agents/openfleet-planner.agent.md +56 -0
  13. package/core/templates/agents/openfleet-tester.agent.md +42 -0
  14. package/dist/attribution.d.ts +21 -0
  15. package/dist/attribution.js +72 -0
  16. package/dist/bin.d.ts +2 -0
  17. package/dist/bin.js +3 -0
  18. package/dist/cli.d.ts +3 -0
  19. package/dist/cli.js +134 -0
  20. package/dist/commands/extensions.d.ts +7 -0
  21. package/dist/commands/extensions.js +60 -0
  22. package/dist/commands/internal.d.ts +19 -0
  23. package/dist/commands/internal.js +117 -0
  24. package/dist/commands/output.d.ts +2 -0
  25. package/dist/commands/output.js +6 -0
  26. package/dist/commands/project.d.ts +23 -0
  27. package/dist/commands/project.js +207 -0
  28. package/dist/commands/workspace.d.ts +10 -0
  29. package/dist/commands/workspace.js +63 -0
  30. package/dist/core.d.ts +17 -0
  31. package/dist/core.js +139 -0
  32. package/dist/coreindex.d.ts +17 -0
  33. package/dist/coreindex.js +7 -0
  34. package/dist/events.d.ts +29 -0
  35. package/dist/events.js +115 -0
  36. package/dist/extensions.d.ts +71 -0
  37. package/dist/extensions.js +297 -0
  38. package/dist/fsutil.d.ts +12 -0
  39. package/dist/fsutil.js +70 -0
  40. package/dist/git.d.ts +27 -0
  41. package/dist/git.js +97 -0
  42. package/dist/hooks/handlers.d.ts +43 -0
  43. package/dist/hooks/handlers.js +363 -0
  44. package/dist/hooks/main.d.ts +2 -0
  45. package/dist/hooks/main.js +37 -0
  46. package/dist/index.d.ts +12 -0
  47. package/dist/index.js +12 -0
  48. package/dist/launcher.d.ts +24 -0
  49. package/dist/launcher.js +75 -0
  50. package/dist/lock.d.ts +6 -0
  51. package/dist/lock.js +45 -0
  52. package/dist/redact.d.ts +2 -0
  53. package/dist/redact.js +39 -0
  54. package/dist/repos.d.ts +28 -0
  55. package/dist/repos.js +10 -0
  56. package/dist/state.d.ts +68 -0
  57. package/dist/state.js +111 -0
  58. package/dist/workspace.d.ts +36 -0
  59. package/dist/workspace.js +74 -0
  60. package/package.json +47 -3
  61. package/README.md +0 -3
@@ -0,0 +1,68 @@
1
+ import { type ProjectPaths } from "./workspace.js";
2
+ export declare const STATE_SCHEMA_VERSION = 1;
3
+ /** `specifying` covers the whole grill, including the final spec review; the user's answer moves it to `running`. */
4
+ export declare const PROJECT_STATUSES: readonly ["specifying", "running", "finalizing", "done", "cancelled"];
5
+ export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
6
+ /** `failed`: retries and the coordinator's guided attempt both failed. `blocked`: a dependency failed. */
7
+ export declare const CHILD_PHASES: readonly ["pending", "planning", "plan-review", "implementing", "testing", "post-test", "done", "failed", "blocked"];
8
+ export type ChildPhase = (typeof CHILD_PHASES)[number];
9
+ export type Role = "planner" | "implementer" | "tester";
10
+ export type CompletionMode = "staged" | "draft-pr";
11
+ export interface ChildState {
12
+ /** Key in repos.json. */
13
+ repo: string;
14
+ /** Package path inside a monorepo, if this child targets one package. */
15
+ package?: string;
16
+ /** Absolute path of the source repository checkout. */
17
+ repoPath?: string;
18
+ worktree?: string;
19
+ branch?: string;
20
+ baseBranch?: string;
21
+ /** Commit the worktree branched from; snapshots diff against it. */
22
+ baseSha?: string;
23
+ phase: ChildPhase;
24
+ /** Role of the currently active agent for this child. */
25
+ role?: Role;
26
+ /** Set by the coordinator just before spawning; consumed by the subagentStart hook. */
27
+ pendingSpawn?: Role;
28
+ retries: number;
29
+ dependsOn: string[];
30
+ agentIds: Partial<Record<Role, string>>;
31
+ plan?: string;
32
+ prUrl?: string;
33
+ lastSnapshot?: string;
34
+ notes?: string;
35
+ }
36
+ export interface Question {
37
+ id: string;
38
+ from: string;
39
+ text: string;
40
+ status: "open" | "answered";
41
+ answer?: string;
42
+ /** `spec`: answered from spec.md/contracts. `coordinator`: decided by the coordinator (logged as an `assumption`). */
43
+ answeredBy?: "spec" | "coordinator";
44
+ }
45
+ export interface ProjectState {
46
+ v: typeof STATE_SCHEMA_VERSION;
47
+ name: string;
48
+ status: ProjectStatus;
49
+ createdAt: string;
50
+ updatedAt: string;
51
+ /** Copilot CLI session id of the coordinator session, used by `resume`. */
52
+ sessionId?: string;
53
+ completionMode?: CompletionMode;
54
+ specApproved?: boolean;
55
+ /** Keyed by child id: `<repo>` or `<repo>:<package>`. */
56
+ children: Record<string, ChildState>;
57
+ questions: Question[];
58
+ }
59
+ export declare function newState(name: string, sessionId?: string): ProjectState;
60
+ export declare function readState(p: ProjectPaths): ProjectState;
61
+ /** RFC 7396 JSON merge patch: objects merge, `null` deletes, everything else replaces. */
62
+ export declare function mergePatch<T>(target: T, patch: unknown): T;
63
+ /**
64
+ * Read-modify-write state.json under the project's state lock. Emits
65
+ * `phase.changed` events for project status and child phase transitions.
66
+ */
67
+ export declare function updateState(p: ProjectPaths, mutate: (s: ProjectState) => ProjectState | void, actor?: string): ProjectState;
68
+ export declare function patchState(p: ProjectPaths, patch: unknown, actor?: string): ProjectState;
package/dist/state.js ADDED
@@ -0,0 +1,111 @@
1
+ import fs from "node:fs";
2
+ import { appendEvent } from "./events.js";
3
+ import { readJson, writeJson } from "./fsutil.js";
4
+ import { withLock } from "./lock.js";
5
+ import { UserError } from "./workspace.js";
6
+ export const STATE_SCHEMA_VERSION = 1;
7
+ /** `specifying` covers the whole grill, including the final spec review; the user's answer moves it to `running`. */
8
+ export const PROJECT_STATUSES = ["specifying", "running", "finalizing", "done", "cancelled"];
9
+ /** `failed`: retries and the coordinator's guided attempt both failed. `blocked`: a dependency failed. */
10
+ export const CHILD_PHASES = [
11
+ "pending",
12
+ "planning",
13
+ "plan-review",
14
+ "implementing",
15
+ "testing",
16
+ "post-test",
17
+ "done",
18
+ "failed",
19
+ "blocked",
20
+ ];
21
+ export function newState(name, sessionId) {
22
+ const now = new Date().toISOString();
23
+ return {
24
+ v: STATE_SCHEMA_VERSION,
25
+ name,
26
+ status: "specifying",
27
+ createdAt: now,
28
+ updatedAt: now,
29
+ ...(sessionId ? { sessionId } : {}),
30
+ children: {},
31
+ questions: [],
32
+ };
33
+ }
34
+ export function readState(p) {
35
+ if (!fs.existsSync(p.state))
36
+ throw new UserError(`Project "${p.name}" not found (${p.state}).`);
37
+ return readJson(p.state);
38
+ }
39
+ function isPlainObject(v) {
40
+ return typeof v === "object" && v !== null && !Array.isArray(v);
41
+ }
42
+ /** RFC 7396 JSON merge patch: objects merge, `null` deletes, everything else replaces. */
43
+ export function mergePatch(target, patch) {
44
+ if (!isPlainObject(patch))
45
+ return patch;
46
+ const base = isPlainObject(target) ? { ...target } : {};
47
+ for (const [k, v] of Object.entries(patch)) {
48
+ if (v === null)
49
+ delete base[k];
50
+ else
51
+ base[k] = mergePatch(base[k], v);
52
+ }
53
+ return base;
54
+ }
55
+ function normalizeChildren(state) {
56
+ for (const [id, child] of Object.entries(state.children)) {
57
+ child.repo ??= id.split(":")[0] ?? id;
58
+ child.phase ??= "pending";
59
+ child.retries ??= 0;
60
+ child.dependsOn ??= [];
61
+ child.agentIds ??= {};
62
+ }
63
+ }
64
+ function validateTransitions(prev, next) {
65
+ if (prev.status !== next.status && !PROJECT_STATUSES.includes(next.status)) {
66
+ throw new UserError(`Unknown project status "${next.status}". Use one of: ${PROJECT_STATUSES.join(", ")}.`);
67
+ }
68
+ for (const [id, child] of Object.entries(next.children)) {
69
+ if (prev.children[id]?.phase !== child.phase && !CHILD_PHASES.includes(child.phase)) {
70
+ throw new UserError(`Unknown phase "${child.phase}" for child ${id}. Use one of: ${CHILD_PHASES.join(", ")}.`);
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * Read-modify-write state.json under the project's state lock. Emits
76
+ * `phase.changed` events for project status and child phase transitions.
77
+ */
78
+ export function updateState(p, mutate, actor = "coordinator") {
79
+ const changes = [];
80
+ const next = withLock(p.stateLock, () => {
81
+ const prev = readState(p);
82
+ const draft = structuredClone(prev);
83
+ const result = mutate(draft) ?? draft;
84
+ normalizeChildren(result);
85
+ validateTransitions(prev, result);
86
+ result.updatedAt = new Date().toISOString();
87
+ if (prev.status !== result.status)
88
+ changes.push({ target: null, from: prev.status, to: result.status });
89
+ for (const [id, child] of Object.entries(result.children)) {
90
+ const before = prev.children[id]?.phase;
91
+ if (before !== child.phase)
92
+ changes.push({ target: id, from: before, to: child.phase });
93
+ }
94
+ writeJson(p.state, result);
95
+ return result;
96
+ });
97
+ for (const c of changes) {
98
+ appendEvent(p, {
99
+ actor,
100
+ type: "phase.changed",
101
+ target: c.target,
102
+ payload: { scope: c.target ? "child" : "project", from: c.from ?? null, to: c.to },
103
+ });
104
+ }
105
+ return next;
106
+ }
107
+ export function patchState(p, patch, actor) {
108
+ if (!isPlainObject(patch))
109
+ throw new UserError("State patch must be a JSON object.");
110
+ return updateState(p, (s) => mergePatch(s, patch), actor);
111
+ }
@@ -0,0 +1,36 @@
1
+ export declare const DIR = "openfleet";
2
+ export interface Workspace {
3
+ /** Directory containing the `openfleet/` folder. */
4
+ root: string;
5
+ /** `<root>/openfleet` */
6
+ dir: string;
7
+ core: string;
8
+ extensions: string;
9
+ projects: string;
10
+ reposFile: string;
11
+ lockFile: string;
12
+ }
13
+ export declare function workspaceAt(root: string): Workspace;
14
+ /** Walk up from `start` to find the nearest OpenFleet workspace. */
15
+ export declare function findWorkspace(start: string): Workspace | undefined;
16
+ export declare function requireWorkspace(start?: string): Workspace;
17
+ export interface ProjectPaths {
18
+ name: string;
19
+ dir: string;
20
+ spec: string;
21
+ state: string;
22
+ contracts: string;
23
+ plans: string;
24
+ events: string;
25
+ seq: string;
26
+ eventsLock: string;
27
+ stateLock: string;
28
+ patches: string;
29
+ worktrees: string;
30
+ }
31
+ export declare function projectPaths(ws: Workspace, name: string): ProjectPaths;
32
+ export declare function validateProjectName(name: string | undefined): string;
33
+ export declare function listProjectNames(ws: Workspace): string[];
34
+ /** Errors that should be printed without a stack trace. */
35
+ export declare class UserError extends Error {
36
+ }
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isDir, isFile, realish } from "./fsutil.js";
4
+ export const DIR = "openfleet";
5
+ const PROJECT_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/;
6
+ export function workspaceAt(root) {
7
+ const dir = path.join(root, DIR);
8
+ return {
9
+ root,
10
+ dir,
11
+ core: path.join(dir, "core"),
12
+ extensions: path.join(dir, "extensions"),
13
+ projects: path.join(dir, "projects"),
14
+ reposFile: path.join(dir, "repos.json"),
15
+ lockFile: path.join(dir, "extensions.lock.json"),
16
+ };
17
+ }
18
+ function isWorkspaceRoot(root) {
19
+ return isFile(path.join(root, DIR, "repos.json"));
20
+ }
21
+ /** Walk up from `start` to find the nearest OpenFleet workspace. */
22
+ export function findWorkspace(start) {
23
+ let cur = realish(start);
24
+ for (;;) {
25
+ if (isWorkspaceRoot(cur))
26
+ return workspaceAt(cur);
27
+ const parent = path.dirname(cur);
28
+ if (parent === cur)
29
+ return undefined;
30
+ cur = parent;
31
+ }
32
+ }
33
+ export function requireWorkspace(start = process.cwd()) {
34
+ const ws = findWorkspace(start);
35
+ if (!ws) {
36
+ throw new UserError(`No OpenFleet workspace found from ${start}. Run \`npm create @openfleet-tools\` (or \`npx openfleet init\`) first.`);
37
+ }
38
+ return ws;
39
+ }
40
+ export function projectPaths(ws, name) {
41
+ const dir = path.join(ws.projects, name);
42
+ return {
43
+ name,
44
+ dir,
45
+ spec: path.join(dir, "spec.md"),
46
+ state: path.join(dir, "state.json"),
47
+ contracts: path.join(dir, "contracts"),
48
+ plans: path.join(dir, "plans"),
49
+ events: path.join(dir, "events.jsonl"),
50
+ seq: path.join(dir, ".events.seq"),
51
+ eventsLock: path.join(dir, ".events.lock"),
52
+ stateLock: path.join(dir, ".state.lock"),
53
+ patches: path.join(dir, "patches"),
54
+ worktrees: path.join(dir, "worktrees"),
55
+ };
56
+ }
57
+ export function validateProjectName(name) {
58
+ if (!name || !PROJECT_NAME.test(name)) {
59
+ throw new UserError(`Invalid project name "${name ?? ""}". Use lowercase letters, digits and dashes (e.g. better-auth).`);
60
+ }
61
+ return name;
62
+ }
63
+ export function listProjectNames(ws) {
64
+ if (!isDir(ws.projects))
65
+ return [];
66
+ return fs
67
+ .readdirSync(ws.projects, { withFileTypes: true })
68
+ .filter((d) => d.isDirectory() && isFile(path.join(ws.projects, d.name, "state.json")))
69
+ .map((d) => d.name)
70
+ .sort();
71
+ }
72
+ /** Errors that should be printed without a stack trace. */
73
+ export class UserError extends Error {
74
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@openfleet-tools/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Coordinate one change across many repos with GitHub Copilot CLI.",
5
+ "license": "MIT",
6
+ "homepage": "https://openfleet.tools",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/yashicon21/openfleet.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/yashicon21/openfleet/issues"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "openfleet": "dist/bin.js"
18
+ },
19
+ "exports": {
20
+ ".": "./dist/index.js",
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "core"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "keywords": [
31
+ "copilot",
32
+ "multi-repo",
33
+ "agents",
34
+ "coordination"
35
+ ],
36
+ "dependencies": {
37
+ "semver": "^7.8.5"
38
+ },
39
+ "devDependencies": {
40
+ "@types/semver": "^7.8.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.json && chmod +x dist/bin.js",
47
+ "typecheck": "tsc -p tsconfig.json --noEmit",
48
+ "clean": "rm -rf dist"
49
+ }
6
50
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.