@theagilemonkeys/facility 0.3.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 +201 -0
- package/README.md +68 -0
- package/bin/facility.mjs +10 -0
- package/modules/README.md +35 -0
- package/modules/ai-queryability/agents/queryability-reviewer.md +35 -0
- package/modules/ai-queryability/module.json +9 -0
- package/modules/ai-queryability/standard-section.md +22 -0
- package/modules/analytics/agents/analytics-reviewer.md +32 -0
- package/modules/analytics/commands/add-telemetry.md +23 -0
- package/modules/analytics/module.json +10 -0
- package/modules/analytics/standard-section.md +23 -0
- package/modules/database/agents/data-security-reviewer.md +38 -0
- package/modules/database/commands/new-migration.md +24 -0
- package/modules/database/guards/migration-versions.mjs +41 -0
- package/modules/database/guards/migrations-immutable.mjs +57 -0
- package/modules/database/hooks/protect-migrations.fragment.mjs +10 -0
- package/modules/database/module.json +25 -0
- package/modules/database/standard-section.md +20 -0
- package/modules/design-system/agents/design-reviewer.md +37 -0
- package/modules/design-system/module.json +9 -0
- package/modules/design-system/standard-section.md +15 -0
- package/package.json +42 -0
- package/src/add.mjs +77 -0
- package/src/cli.mjs +352 -0
- package/src/detect.mjs +127 -0
- package/src/doctor.mjs +582 -0
- package/src/init.mjs +572 -0
- package/src/instance.mjs +114 -0
- package/src/platform-admin.mjs +1542 -0
- package/src/platform-config.mjs +39 -0
- package/src/platform.mjs +1759 -0
- package/src/prompts.mjs +64 -0
- package/src/render.mjs +66 -0
- package/src/ui.mjs +30 -0
- package/templates/claude/agents/security-reviewer.md +41 -0
- package/templates/claude/agents/standards-reviewer.md +31 -0
- package/templates/claude/commands/open-pr.md +21 -0
- package/templates/claude/commands/verify.md +16 -0
- package/templates/claude/hooks/protect-branch.mjs +58 -0
- package/templates/claude/hooks/protect-files.mjs +35 -0
- package/templates/claude/settings.json +71 -0
- package/templates/claude/skills/maintainable-software/SKILL.md +67 -0
- package/templates/claude/skills/reviewing-to-standard/SKILL.md +49 -0
- package/templates/claude/skills/working-to-standard/SKILL.md +45 -0
- package/templates/delivery/verify.mjs +157 -0
- package/templates/doctor/resolve.mjs +144 -0
- package/templates/guards/README.md +30 -0
- package/templates/guards/_kit.mjs +81 -0
- package/templates/guards/actions-pinned.mjs +38 -0
- package/templates/guards/run.mjs +111 -0
- package/templates/guards/watchtower-locked.mjs +66 -0
- package/templates/prompts/address-review.md +14 -0
- package/templates/prompts/architect.md +62 -0
- package/templates/prompts/builder.md +71 -0
- package/templates/prompts/doctor.md +64 -0
- package/templates/prompts/review.md +14 -0
- package/templates/prompts/sweep.md +75 -0
- package/templates/receipts/collect.mjs +289 -0
- package/templates/review/finalize.mjs +38 -0
- package/templates/scripts/move-board-status.sh +155 -0
- package/templates/security/sync-findings.mjs +226 -0
- package/templates/standard/STANDARD.md +141 -0
- package/templates/standard/agents-block.md +25 -0
- package/templates/watchtower/budgets.json +12 -0
- package/templates/watchtower/canary.mjs +216 -0
- package/templates/watchtower/health.mjs +148 -0
- package/templates/watchtower/outcomes.mjs +188 -0
- package/templates/workflows/facility-address-review.yml +153 -0
- package/templates/workflows/facility-canary.yml +61 -0
- package/templates/workflows/facility-codex.yml +326 -0
- package/templates/workflows/facility-crew.yml +350 -0
- package/templates/workflows/facility-doctor.yml +155 -0
- package/templates/workflows/facility-review.yml +134 -0
- package/templates/workflows/facility-security-sweep.yml +204 -0
- package/templates/workflows/facility-watchtower.yml +87 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "database",
|
|
3
|
+
"title": "Database",
|
|
4
|
+
"description": "Migrations are immutable and append-only; access control by default; privileged credentials stay out of user-facing paths.",
|
|
5
|
+
"standardSection": "standard-section.md",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"from": "agents/data-security-reviewer.md",
|
|
9
|
+
"to": ".claude/agents/data-security-reviewer.md"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"from": "guards/migrations-immutable.mjs",
|
|
13
|
+
"to": "guards/migrations-immutable.mjs"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"from": "guards/migration-versions.mjs",
|
|
17
|
+
"to": "guards/migration-versions.mjs"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"from": "commands/new-migration.md",
|
|
21
|
+
"to": ".claude/commands/new-migration.md"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"hookRules": "hooks/protect-migrations.fragment.mjs"
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
### Database (facility module)
|
|
2
|
+
|
|
3
|
+
Migrations are the durable state of the database. They are **append-only**:
|
|
4
|
+
a change is a NEW migration, never an edit to one that may have been applied
|
|
5
|
+
anywhere. The `migrations-immutable` guard and the `.claude/hooks` rules
|
|
6
|
+
enforce this mechanically.
|
|
7
|
+
|
|
8
|
+
- New tables and collections start closed: enable row/document-level access
|
|
9
|
+
control where the platform supports it, deny by default, grant only the
|
|
10
|
+
minimum required operations.
|
|
11
|
+
- Authorization checks route through one shared helper layer — never inline
|
|
12
|
+
token/claim parsing inside individual policies or queries.
|
|
13
|
+
- Privileged credentials (service role, admin connections) stay out of
|
|
14
|
+
user-facing and agent-facing read paths. Any privileged action lives in a
|
|
15
|
+
reviewed application service with explicit permission checks and tests.
|
|
16
|
+
- Update seed data whenever a feature needs realistic data to test locally.
|
|
17
|
+
Seeds follow the domain: realistic names, states, relationships, and edge
|
|
18
|
+
cases — not toy rows that only satisfy a test selector.
|
|
19
|
+
- Empty results must be permission-safe: do not imply hidden data exists when
|
|
20
|
+
access control returns nothing.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-reviewer
|
|
3
|
+
description: Reviews UI changes against the repo's design system and checks that critical UI flows carry browser evidence. Use when a change adds or modifies UI, layout, components, or styling.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You review product UI against this repository's design system and confirm
|
|
8
|
+
critical flows are verified in a browser. A UI that conflicts with the design
|
|
9
|
+
system is a product-quality issue.
|
|
10
|
+
|
|
11
|
+
## Authoritative references
|
|
12
|
+
- The design system source of truth declared in `STANDARD.md` (Design system
|
|
13
|
+
section) — read it before judging.
|
|
14
|
+
- The repo's design tokens / shared component layer — existing values come
|
|
15
|
+
first.
|
|
16
|
+
|
|
17
|
+
## What to check
|
|
18
|
+
1. Existing design tokens and helpers are reused before any new value; no
|
|
19
|
+
ad-hoc colors, spacing, or radii that bypass the token layer.
|
|
20
|
+
2. Components match the system's catalog: shapes, sizes, states, and
|
|
21
|
+
hierarchy expressed the way the system prescribes.
|
|
22
|
+
3. **Evidence**: UI flow changes include a browser verification note
|
|
23
|
+
(Playwright run, screenshots), or an explicit reason UI verification did
|
|
24
|
+
not apply.
|
|
25
|
+
4. Responsive behavior holds at the product's breakpoints when layout, text
|
|
26
|
+
fit, navigation, modals, or tables can be affected.
|
|
27
|
+
5. Empty, error, and loading states are designed, not accidental.
|
|
28
|
+
|
|
29
|
+
## How to verify
|
|
30
|
+
- `grep` the diff for raw color/spacing literals that should be tokens.
|
|
31
|
+
- Recommend the smallest browser run that covers the affected flows.
|
|
32
|
+
|
|
33
|
+
## Output contract
|
|
34
|
+
List design-system violations and missing evidence by severity with file:line
|
|
35
|
+
and the token/helper to use instead. Confirm which critical flows are covered
|
|
36
|
+
by browser evidence. Flag only design-system/UX-correctness gaps, not
|
|
37
|
+
subjective taste.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "design-system",
|
|
3
|
+
"title": "Design system",
|
|
4
|
+
"description": "UI changes conform to your design system and critical flows carry browser evidence.",
|
|
5
|
+
"standardSection": "standard-section.md",
|
|
6
|
+
"files": [
|
|
7
|
+
{ "from": "agents/design-reviewer.md", "to": ".claude/agents/design-reviewer.md" }
|
|
8
|
+
]
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
### Design system (facility module)
|
|
2
|
+
|
|
3
|
+
A UI that conflicts with the design system is a product-quality issue, not a
|
|
4
|
+
matter of taste. Declare your source of truth here (a spec doc, a tokens
|
|
5
|
+
file, a published package) — the design reviewer reads it before judging.
|
|
6
|
+
|
|
7
|
+
- Use existing design tokens and shared components before inventing values.
|
|
8
|
+
Ad-hoc colors, spacing, and radii that bypass the token layer are defects.
|
|
9
|
+
- UI flow changes include browser evidence — a Playwright run, screenshots,
|
|
10
|
+
or a recorded check — or an explicit reason why UI verification did not
|
|
11
|
+
apply. "It compiles" is not evidence for layout.
|
|
12
|
+
- Verify responsive behavior at the breakpoints your product supports when a
|
|
13
|
+
change can affect layout, text fit, navigation, modals, or tables.
|
|
14
|
+
- Empty states, error states, and loading states are part of the surface —
|
|
15
|
+
design them, don't let them fall out of the happy path.
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@theagilemonkeys/facility",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Run an AI crew on your GitHub repo: agents that plan before building, build to your standard, verify on a provisioned environment, and never merge their own work.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"agents",
|
|
8
|
+
"sdlc",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"github-actions",
|
|
11
|
+
"code-review",
|
|
12
|
+
"automation"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/theam/facility#readme",
|
|
15
|
+
"bugs": "https://github.com/theam/facility/issues",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/theam/facility.git",
|
|
19
|
+
"directory": "packages/cli"
|
|
20
|
+
},
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"author": "The Agile Monkeys (https://theagilemonkeys.com)",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"facility": "bin/facility.mjs"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin",
|
|
29
|
+
"src",
|
|
30
|
+
"templates",
|
|
31
|
+
"modules"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"postgres": "^3.4.7"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "node --test test/*.test.mjs"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/add.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// `facility add <module>` — install a quality module: its STANDARD.md section,
|
|
2
|
+
// its reviewer subagent, its guards, and its hook rules.
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { insertHookRules, insertModuleSection } from "./render.mjs";
|
|
6
|
+
import { fail, heading, ok, skip, warn } from "./ui.mjs";
|
|
7
|
+
|
|
8
|
+
export async function addModule(name, { dir, pkgRoot, banner = true }) {
|
|
9
|
+
const moduleDir =
|
|
10
|
+
name.startsWith(".") || isAbsolute(name) ? resolve(dir, name) : join(pkgRoot, "modules", name);
|
|
11
|
+
|
|
12
|
+
const manifestPath = join(moduleDir, "module.json");
|
|
13
|
+
if (!existsSync(manifestPath)) {
|
|
14
|
+
const available = readdirSync(join(pkgRoot, "modules"), { withFileTypes: true })
|
|
15
|
+
.filter((entry) => entry.isDirectory())
|
|
16
|
+
.map((entry) => entry.name)
|
|
17
|
+
.join(", ");
|
|
18
|
+
fail(`Unknown module "${name}". Available: ${available} (or a local path).`);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const module = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
22
|
+
if (banner) heading(`Adding module: ${module.title}`);
|
|
23
|
+
|
|
24
|
+
// 1. Files (reviewer subagents, guards) — never overwrite.
|
|
25
|
+
for (const file of module.files ?? []) {
|
|
26
|
+
const target = join(dir, file.to);
|
|
27
|
+
if (existsSync(target)) {
|
|
28
|
+
skip(`${file.to} exists — left untouched`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
32
|
+
writeFileSync(target, readFileSync(join(moduleDir, file.from), "utf8"));
|
|
33
|
+
ok(file.to);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. STANDARD.md section between the facility:modules markers.
|
|
37
|
+
const standardPath = join(dir, "STANDARD.md");
|
|
38
|
+
if (module.standardSection && existsSync(standardPath)) {
|
|
39
|
+
const section = readFileSync(join(moduleDir, module.standardSection), "utf8");
|
|
40
|
+
const { content, inserted } = insertModuleSection(readFileSync(standardPath, "utf8"), section, module.title);
|
|
41
|
+
if (inserted) {
|
|
42
|
+
writeFileSync(standardPath, content);
|
|
43
|
+
ok(`STANDARD.md — "${module.title}" section`);
|
|
44
|
+
} else {
|
|
45
|
+
skip(`STANDARD.md already has the "${module.title}" section`);
|
|
46
|
+
}
|
|
47
|
+
} else if (module.standardSection) {
|
|
48
|
+
warn("STANDARD.md not found — run `facility init` first.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Hook rules spliced into protect-files.mjs at the module marker.
|
|
52
|
+
if (module.hookRules) {
|
|
53
|
+
const hookPath = join(dir, ".claude/hooks/protect-files.mjs");
|
|
54
|
+
if (existsSync(hookPath)) {
|
|
55
|
+
const fragment = readFileSync(join(moduleDir, module.hookRules), "utf8");
|
|
56
|
+
const { content, inserted } = insertHookRules(readFileSync(hookPath, "utf8"), fragment, module.name);
|
|
57
|
+
if (inserted) {
|
|
58
|
+
writeFileSync(hookPath, content);
|
|
59
|
+
ok(".claude/hooks/protect-files.mjs — rules spliced");
|
|
60
|
+
} else {
|
|
61
|
+
skip(".claude/hooks/protect-files.mjs already has this module's rules");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Record it in the manifest.
|
|
67
|
+
const facilityManifestPath = join(dir, ".facility.json");
|
|
68
|
+
if (existsSync(facilityManifestPath)) {
|
|
69
|
+
const manifest = JSON.parse(readFileSync(facilityManifestPath, "utf8"));
|
|
70
|
+
if (!manifest.modules?.includes(module.name)) {
|
|
71
|
+
manifest.modules = [...(manifest.modules ?? []), module.name];
|
|
72
|
+
writeFileSync(facilityManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// facility CLI entry: init · add · doctor.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { init } from "./init.mjs";
|
|
6
|
+
import { addModule } from "./add.mjs";
|
|
7
|
+
import { doctor } from "./doctor.mjs";
|
|
8
|
+
import { bootstrapInstance } from "./instance.mjs";
|
|
9
|
+
import { runPlatformCommand } from "./platform.mjs";
|
|
10
|
+
import { banner, bold, dim, item } from "./ui.mjs";
|
|
11
|
+
|
|
12
|
+
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const version = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")).version;
|
|
14
|
+
|
|
15
|
+
function parseFlags(args) {
|
|
16
|
+
const flags = {};
|
|
17
|
+
const positional = [];
|
|
18
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
19
|
+
const arg = args[index];
|
|
20
|
+
if (arg === "--yes" || arg === "-y") flags.yes = true;
|
|
21
|
+
else if (arg === "--json") flags.json = true;
|
|
22
|
+
else if (arg === "--force") flags.force = true;
|
|
23
|
+
else if (["--help", "--local", "--platform"].includes(arg)) flags[arg.slice(2)] = true;
|
|
24
|
+
else if (arg.startsWith("--") && arg.includes("=")) {
|
|
25
|
+
const [key, ...rest] = arg.slice(2).split("=");
|
|
26
|
+
flags[key] = rest.join("=");
|
|
27
|
+
} else if (arg.startsWith("--")) {
|
|
28
|
+
const key = arg.slice(2);
|
|
29
|
+
const next = args[index + 1];
|
|
30
|
+
if (next && !next.startsWith("-")) {
|
|
31
|
+
flags[key] = next;
|
|
32
|
+
index += 1;
|
|
33
|
+
} else flags[key] = true;
|
|
34
|
+
} else if (!arg.startsWith("-")) positional.push(arg);
|
|
35
|
+
}
|
|
36
|
+
return { flags, positional };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function help() {
|
|
40
|
+
banner(version);
|
|
41
|
+
helpGroup("Build your repo", [
|
|
42
|
+
["init", "install the Facility method"],
|
|
43
|
+
["add <module>", "add a quality module"],
|
|
44
|
+
["doctor", "verify local and platform readiness"],
|
|
45
|
+
]);
|
|
46
|
+
helpGroup("Operate", [
|
|
47
|
+
["status", "runs, approvals, issues, and spend at a glance"],
|
|
48
|
+
["projects", "create, inspect, update, or archive projects"],
|
|
49
|
+
["repos", "connect or create GitHub repositories"],
|
|
50
|
+
["agents", "configure agent definitions and schedules"],
|
|
51
|
+
["sessions", "trigger, watch, steer, or cancel governed sessions"],
|
|
52
|
+
["conversations", "continue durable agent conversations"],
|
|
53
|
+
["github", "discover repositories and act on GitHub issues"],
|
|
54
|
+
["inbox", "review and decide proposals"],
|
|
55
|
+
["issues", "acknowledge and resolve platform issues"],
|
|
56
|
+
["kickstart", "preview and open a governed installation PR"],
|
|
57
|
+
["upgrade", "open a governed template upgrade PR"],
|
|
58
|
+
]);
|
|
59
|
+
helpGroup("Knowledge & policy", [
|
|
60
|
+
["kb", "manage the project knowledge base"],
|
|
61
|
+
["tasks", "manage the product-owner task queue"],
|
|
62
|
+
["registry", "version contracts, harnesses, and policies"],
|
|
63
|
+
["proposals", "create, inspect, and execute approval gates"],
|
|
64
|
+
["action-types", "discover proposal payload contracts"],
|
|
65
|
+
["budgets", "enforce org, project, and agent budgets"],
|
|
66
|
+
["health", "inspect project readiness"],
|
|
67
|
+
["outcomes", "inspect pull-request delivery outcomes"],
|
|
68
|
+
["catalog", "discover engines, models, permissions, and triggers"],
|
|
69
|
+
]);
|
|
70
|
+
helpGroup("Platform administration", [
|
|
71
|
+
["instance bootstrap", "bootstrap a dedicated Facility instance"],
|
|
72
|
+
["org", "organization settings"],
|
|
73
|
+
["members", "organization membership"],
|
|
74
|
+
["roles", "permission policy"],
|
|
75
|
+
["keys", "control-plane API credentials"],
|
|
76
|
+
["virtual-keys", "project-scoped model credentials"],
|
|
77
|
+
["providers", "model-provider credentials"],
|
|
78
|
+
["sandboxes", "execution profiles and isolation"],
|
|
79
|
+
["integrations", "inbound and outbound webhooks"],
|
|
80
|
+
["analytics", "run and reliability trends"],
|
|
81
|
+
["spend", "cost attribution"],
|
|
82
|
+
["audit", "tamper-evident change ledger"],
|
|
83
|
+
["llm-requests", "inspect metering and request envelopes"],
|
|
84
|
+
]);
|
|
85
|
+
helpGroup("Account", [
|
|
86
|
+
["login", "verify and save an environment profile"],
|
|
87
|
+
["logout", "remove saved profile credentials"],
|
|
88
|
+
["profiles", "switch between Facility environments"],
|
|
89
|
+
]);
|
|
90
|
+
console.log("");
|
|
91
|
+
item(dim("init flags: --yes --force --dir=<path> --branch=<name> --provision=<cmd>"));
|
|
92
|
+
item(dim(' --checks="cmd1, cmd2" --auth=<api-key|oauth|wif|bedrock|vertex>'));
|
|
93
|
+
item(dim(' --build-model=<id> --review-model=<id> --plan-model=<id>'));
|
|
94
|
+
item(dim(' --codex-build-model=<id> --codex-plan-model=<id> --org=<org> --project=<n>'));
|
|
95
|
+
item(dim(' --preview-image=<image> --preview-command=<cmd> --preview-port=<n> --preview-readiness-path=</health> --preview-ttl-hours=<n>'));
|
|
96
|
+
item(dim("Run facility <command> --help for precise usage."));
|
|
97
|
+
item(dim("Global platform flags: --profile <name> --json --timeout <seconds>"));
|
|
98
|
+
console.log("");
|
|
99
|
+
item(dim("Docs: https://github.com/theam/facility"));
|
|
100
|
+
console.log("");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function helpGroup(label, commands) {
|
|
104
|
+
item(bold(label));
|
|
105
|
+
const width = Math.max(...commands.map(([command]) => command.length));
|
|
106
|
+
for (const [command, description] of commands) {
|
|
107
|
+
item(` ${bold(`facility ${command.padEnd(width)}`)} ${dim(description)}`);
|
|
108
|
+
}
|
|
109
|
+
console.log("");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function main(argv) {
|
|
113
|
+
const { command, rest } = extractInvocation(argv);
|
|
114
|
+
const { flags, positional } = parseFlags(rest);
|
|
115
|
+
const localFlagError = validateLocalFlags(command, flags);
|
|
116
|
+
if (localFlagError) {
|
|
117
|
+
if (flags.json) {
|
|
118
|
+
console.log(JSON.stringify({ error: { code: "invalid_flag", message: localFlagError } }));
|
|
119
|
+
} else console.error(localFlagError);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
if (flags.dir) flags.dir = resolve(flags.dir);
|
|
123
|
+
|
|
124
|
+
if (flags.help && ["init", "add", "doctor"].includes(command)) {
|
|
125
|
+
localHelp(command);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
switch (command) {
|
|
130
|
+
case "init":
|
|
131
|
+
return init(flags, pkgRoot, version);
|
|
132
|
+
case "add": {
|
|
133
|
+
if (!positional[0]) {
|
|
134
|
+
help();
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
return addModule(positional[0], { dir: flags.dir || process.cwd(), pkgRoot });
|
|
138
|
+
}
|
|
139
|
+
case "doctor":
|
|
140
|
+
return doctor(flags, version);
|
|
141
|
+
case "instance":
|
|
142
|
+
if (positional[0] === "bootstrap") return bootstrapInstance(flags);
|
|
143
|
+
console.error("Usage: facility instance bootstrap [options]");
|
|
144
|
+
return 1;
|
|
145
|
+
case "login":
|
|
146
|
+
case "logout":
|
|
147
|
+
case "profiles":
|
|
148
|
+
case "status":
|
|
149
|
+
case "projects":
|
|
150
|
+
case "sessions":
|
|
151
|
+
case "runs":
|
|
152
|
+
case "conversations":
|
|
153
|
+
case "github":
|
|
154
|
+
case "outcomes":
|
|
155
|
+
case "catalog":
|
|
156
|
+
case "inbox":
|
|
157
|
+
case "issues":
|
|
158
|
+
case "kickstart":
|
|
159
|
+
case "upgrade":
|
|
160
|
+
case "keys":
|
|
161
|
+
case "llm-requests":
|
|
162
|
+
case "org":
|
|
163
|
+
case "members":
|
|
164
|
+
case "roles":
|
|
165
|
+
case "repos":
|
|
166
|
+
case "agents":
|
|
167
|
+
case "providers":
|
|
168
|
+
case "budgets":
|
|
169
|
+
case "registry":
|
|
170
|
+
case "sandboxes":
|
|
171
|
+
case "tasks":
|
|
172
|
+
case "virtual-keys":
|
|
173
|
+
case "kb":
|
|
174
|
+
case "analytics":
|
|
175
|
+
case "audit":
|
|
176
|
+
case "integrations":
|
|
177
|
+
case "spend":
|
|
178
|
+
case "proposals":
|
|
179
|
+
case "action-types":
|
|
180
|
+
case "health":
|
|
181
|
+
return runPlatformCommand(command, rest);
|
|
182
|
+
case "--version":
|
|
183
|
+
case "-v":
|
|
184
|
+
console.log(version);
|
|
185
|
+
return 0;
|
|
186
|
+
default:
|
|
187
|
+
if (command && !["help", "--help"].includes(command)) {
|
|
188
|
+
console.error(`Unknown command: ${command}`);
|
|
189
|
+
}
|
|
190
|
+
help();
|
|
191
|
+
return command === undefined || command === "help" || command === "--help" ? 0 : 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validateLocalFlags(command, flags) {
|
|
196
|
+
const allowed = {
|
|
197
|
+
init: new Set([
|
|
198
|
+
"yes",
|
|
199
|
+
"force",
|
|
200
|
+
"dir",
|
|
201
|
+
"branch",
|
|
202
|
+
"provision",
|
|
203
|
+
"checks",
|
|
204
|
+
"project",
|
|
205
|
+
"org",
|
|
206
|
+
"modules",
|
|
207
|
+
"auth",
|
|
208
|
+
"build-model",
|
|
209
|
+
"review-model",
|
|
210
|
+
"plan-model",
|
|
211
|
+
"codex-build-model",
|
|
212
|
+
"codex-plan-model",
|
|
213
|
+
"preview-image",
|
|
214
|
+
"preview-command",
|
|
215
|
+
"preview-port",
|
|
216
|
+
"preview-readiness-path",
|
|
217
|
+
"preview-ttl-hours",
|
|
218
|
+
"help",
|
|
219
|
+
]),
|
|
220
|
+
add: new Set(["dir", "help"]),
|
|
221
|
+
doctor: new Set([
|
|
222
|
+
"dir",
|
|
223
|
+
"local",
|
|
224
|
+
"platform",
|
|
225
|
+
"profile",
|
|
226
|
+
"url",
|
|
227
|
+
"key",
|
|
228
|
+
"allow-insecure",
|
|
229
|
+
"timeout",
|
|
230
|
+
"run-guards",
|
|
231
|
+
"github",
|
|
232
|
+
"json",
|
|
233
|
+
"help",
|
|
234
|
+
]),
|
|
235
|
+
instance: new Set([
|
|
236
|
+
"org-name",
|
|
237
|
+
"org-slug",
|
|
238
|
+
"owner-email",
|
|
239
|
+
"owner-name",
|
|
240
|
+
"github-user-id",
|
|
241
|
+
"github-login",
|
|
242
|
+
"github-account-id",
|
|
243
|
+
"github-installation-id",
|
|
244
|
+
"github-account-login",
|
|
245
|
+
"github-account-type",
|
|
246
|
+
"json",
|
|
247
|
+
"help",
|
|
248
|
+
]),
|
|
249
|
+
}[command];
|
|
250
|
+
if (!allowed) return null;
|
|
251
|
+
const unknown = Object.keys(flags).filter((flag) => !allowed.has(flag));
|
|
252
|
+
if (unknown.length) {
|
|
253
|
+
return `Unknown option${unknown.length === 1 ? "" : "s"}: ${unknown.map((flag) => `--${flag}`).join(", ")}`;
|
|
254
|
+
}
|
|
255
|
+
const valueNames =
|
|
256
|
+
command === "doctor"
|
|
257
|
+
? ["dir", "profile", "url", "key", "timeout"]
|
|
258
|
+
: command === "instance"
|
|
259
|
+
? [...allowed].filter((name) => !["json", "help"].includes(name))
|
|
260
|
+
: command === "init"
|
|
261
|
+
? [
|
|
262
|
+
"dir",
|
|
263
|
+
"branch",
|
|
264
|
+
"provision",
|
|
265
|
+
"checks",
|
|
266
|
+
"project",
|
|
267
|
+
"org",
|
|
268
|
+
"modules",
|
|
269
|
+
"auth",
|
|
270
|
+
"build-model",
|
|
271
|
+
"review-model",
|
|
272
|
+
"plan-model",
|
|
273
|
+
"codex-build-model",
|
|
274
|
+
"codex-plan-model",
|
|
275
|
+
"preview-image",
|
|
276
|
+
"preview-command",
|
|
277
|
+
"preview-port",
|
|
278
|
+
"preview-readiness-path",
|
|
279
|
+
"preview-ttl-hours",
|
|
280
|
+
]
|
|
281
|
+
: ["dir"];
|
|
282
|
+
for (const name of valueNames) {
|
|
283
|
+
if (name in flags && (typeof flags[name] !== "string" || flags[name].trim() === "")) {
|
|
284
|
+
return `--${name} requires a value`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (
|
|
288
|
+
command === "doctor" &&
|
|
289
|
+
flags.local &&
|
|
290
|
+
(flags.platform || flags.profile || flags.url || flags.key)
|
|
291
|
+
) {
|
|
292
|
+
return "--local cannot be combined with platform target options";
|
|
293
|
+
}
|
|
294
|
+
if (
|
|
295
|
+
command === "doctor" &&
|
|
296
|
+
(flags.platform || flags.profile || flags.url || flags.key) &&
|
|
297
|
+
(flags["run-guards"] || flags.github)
|
|
298
|
+
) {
|
|
299
|
+
return "--run-guards and --github are local-doctor options";
|
|
300
|
+
}
|
|
301
|
+
for (const name of [
|
|
302
|
+
"yes",
|
|
303
|
+
"force",
|
|
304
|
+
"help",
|
|
305
|
+
"local",
|
|
306
|
+
"platform",
|
|
307
|
+
"allow-insecure",
|
|
308
|
+
"run-guards",
|
|
309
|
+
"github",
|
|
310
|
+
"json",
|
|
311
|
+
]) {
|
|
312
|
+
if (name in flags && flags[name] !== true) return `--${name} does not take a value`;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function extractInvocation(argv) {
|
|
318
|
+
const leading = [];
|
|
319
|
+
const valueFlags = new Set(["--profile", "--timeout", "--dir", "--url", "--key"]);
|
|
320
|
+
let index = 0;
|
|
321
|
+
while (index < argv.length && argv[index]?.startsWith("-") && !["--help", "--version", "-v"].includes(argv[index])) {
|
|
322
|
+
const flag = argv[index];
|
|
323
|
+
leading.push(flag);
|
|
324
|
+
index += 1;
|
|
325
|
+
if (valueFlags.has(flag) && index < argv.length) {
|
|
326
|
+
leading.push(argv[index]);
|
|
327
|
+
index += 1;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { command: argv[index], rest: [...leading, ...argv.slice(index + 1)] };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function localHelp(command) {
|
|
334
|
+
const lines = {
|
|
335
|
+
init: [
|
|
336
|
+
"facility init [--dir <path>] [--yes] [--force]",
|
|
337
|
+
"Detect the repository and install Facility workflows, guards, agents, and policy.",
|
|
338
|
+
],
|
|
339
|
+
add: [
|
|
340
|
+
"facility add <module> [--dir <path>]",
|
|
341
|
+
"Add a Facility quality module to an existing installation.",
|
|
342
|
+
],
|
|
343
|
+
doctor: [
|
|
344
|
+
"facility doctor [--dir <path>] [--run-guards] [--github] [--json] | --platform [--profile <name>] [--timeout <seconds>] [--json] | --url <url> --key <key> [--allow-insecure] [--timeout <seconds>] [--json]",
|
|
345
|
+
"Verify a local installation or an explicitly selected platform deployment; JSON works in either mode.",
|
|
346
|
+
],
|
|
347
|
+
};
|
|
348
|
+
const [usage, description] = lines[command];
|
|
349
|
+
console.log(`\n ${bold(`facility ${command}`)}`);
|
|
350
|
+
console.log(` ${dim(description)}\n`);
|
|
351
|
+
console.log(` ${bold("Usage")}\n ${usage}\n`);
|
|
352
|
+
}
|