@goodtek/vibeops 0.2.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/dist/agent/loader.js +71 -0
- package/dist/agent/prompt.js +66 -0
- package/dist/bootstrap/installer.js +149 -0
- package/dist/bootstrap/manifest.js +15 -0
- package/dist/bootstrap/substitute.js +35 -0
- package/dist/cli.js +241 -0
- package/dist/commands/agent-list.js +32 -0
- package/dist/commands/agent-prompt.js +59 -0
- package/dist/commands/agent-show.js +26 -0
- package/dist/commands/github-init.js +554 -0
- package/dist/commands/github-status.js +164 -0
- package/dist/commands/init.js +179 -0
- package/dist/commands/notion-init.js +764 -0
- package/dist/commands/notion-sync.js +405 -0
- package/dist/commands/notion-test.js +595 -0
- package/dist/commands/plan.js +114 -0
- package/dist/commands/status.js +17 -0
- package/dist/commands/task-check.js +155 -0
- package/dist/commands/task-done.js +98 -0
- package/dist/commands/task-generate.js +206 -0
- package/dist/commands/task-pull.js +277 -0
- package/dist/commands/task-rollback.js +174 -0
- package/dist/commands/task-start.js +90 -0
- package/dist/lib/brief.js +349 -0
- package/dist/lib/config.js +158 -0
- package/dist/lib/filesystem.js +67 -0
- package/dist/lib/git.js +237 -0
- package/dist/lib/github-cli.js +247 -0
- package/dist/lib/inquirer-helpers.js +111 -0
- package/dist/lib/logger.js +42 -0
- package/dist/lib/notion-client.js +459 -0
- package/dist/lib/notion-discovery.js +671 -0
- package/dist/lib/notion-env.js +140 -0
- package/dist/lib/notion-mappers.js +148 -0
- package/dist/lib/notion-schema.js +272 -0
- package/dist/lib/notion-sync.js +337 -0
- package/dist/lib/notion-target.js +247 -0
- package/dist/lib/package-json.js +133 -0
- package/dist/lib/paths.js +26 -0
- package/dist/lib/project-docs.js +95 -0
- package/dist/lib/prompt-builder.js +125 -0
- package/dist/lib/task-generator.js +183 -0
- package/dist/lib/task-prompt.js +23 -0
- package/dist/lib/task-pull.js +354 -0
- package/dist/lib/task-scaffold.js +128 -0
- package/dist/lib/task-summary.js +276 -0
- package/dist/lib/task.js +364 -0
- package/dist/status/collector.js +103 -0
- package/dist/status/format.js +177 -0
- package/dist/types/brief.js +126 -0
- package/dist/types/config.js +17 -0
- package/dist/types/task.js +1 -0
- package/dist/version.js +8 -0
- package/package.json +61 -0
- package/templates/.cursor/rules/00-project-governance.mdc +28 -0
- package/templates/.cursor/rules/01-agent-orchestration.mdc +48 -0
- package/templates/.cursor/rules/02-task-workflow.mdc +38 -0
- package/templates/.cursor/rules/03-git-safety.mdc +30 -0
- package/templates/.cursor/rules/04-docs-update.mdc +22 -0
- package/templates/.vibeops/agents/architect.md +47 -0
- package/templates/.vibeops/agents/builder.md +38 -0
- package/templates/.vibeops/agents/docs.md +54 -0
- package/templates/.vibeops/agents/orchestrator.md +40 -0
- package/templates/.vibeops/agents/planner.md +60 -0
- package/templates/.vibeops/agents/recovery.md +49 -0
- package/templates/.vibeops/agents/reviewer.md +47 -0
- package/templates/.vibeops/agents/tester.md +43 -0
- package/templates/.vibeops/prompts/create-plan.md +33 -0
- package/templates/.vibeops/prompts/generate-tasks.md +41 -0
- package/templates/.vibeops/prompts/implement-task.md +39 -0
- package/templates/.vibeops/prompts/review-task.md +34 -0
- package/templates/.vibeops/prompts/rollback.md +32 -0
- package/templates/.vibeops/prompts/start-project.md +39 -0
- package/templates/.vibeops/workflows/notion-sync.md +53 -0
- package/templates/.vibeops/workflows/project-start.md +73 -0
- package/templates/.vibeops/workflows/rollback.md +45 -0
- package/templates/.vibeops/workflows/task-lifecycle.md +71 -0
- package/templates/AGENTS.md +98 -0
- package/templates/docs/logs/README.md +38 -0
- package/templates/docs/project/00-overview.md +27 -0
- package/templates/docs/project/01-requirements.md +30 -0
- package/templates/docs/project/02-mvp-scope.md +36 -0
- package/templates/docs/project/03-architecture.md +34 -0
- package/templates/docs/project/04-tech-stack.md +29 -0
- package/templates/docs/project/05-current-state.md +35 -0
- package/templates/docs/project/06-decisions.md +20 -0
- package/templates/docs/project/07-backlog.md +23 -0
- package/templates/docs/project/08-env.md +29 -0
- package/templates/docs/project/09-deployment.md +28 -0
- package/templates/docs/tasks/TASK-000-template.md +72 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { ensureDir, pathExists, readText, readTextOrNull, writeText } from "../lib/filesystem.js";
|
|
3
|
+
import { dim, green, log, yellow } from "../lib/logger.js";
|
|
4
|
+
import { projectPaths, VIBEOPS_ENV_FILE } from "../lib/paths.js";
|
|
5
|
+
import { loadManifest, resolveDestination } from "./manifest.js";
|
|
6
|
+
import { applySubstitutions, buildSubstitutions, isTextPath, } from "./substitute.js";
|
|
7
|
+
import { configToJson } from "../lib/config.js";
|
|
8
|
+
function envExampleContents() {
|
|
9
|
+
// Modern VibeOps reads exactly one Notion secret: NOTION_TOKEN.
|
|
10
|
+
// Notion Projects / Tasks target IDs live in `.vibeops.json` under
|
|
11
|
+
// `notion.projectsTargetId` / `notion.tasksTargetId` (configured by
|
|
12
|
+
// `vibeops notion init`), so they are NOT environment variables.
|
|
13
|
+
//
|
|
14
|
+
// We intentionally do NOT seed GITHUB_TOKEN / OPENAI_* here:
|
|
15
|
+
// - GitHub integration uses `gh` CLI auth.
|
|
16
|
+
// - VibeOps' default runner is prompt mode and does not call LLM APIs.
|
|
17
|
+
return [
|
|
18
|
+
"# VibeOps · environment example",
|
|
19
|
+
"# Copy this file to .vibeops.env and fill in the value.",
|
|
20
|
+
"# Never commit .vibeops.env — it is added to .gitignore by `vibeops init`.",
|
|
21
|
+
"#",
|
|
22
|
+
"# NOTION_TOKEN is the only secret VibeOps reads. Get it from",
|
|
23
|
+
"# https://www.notion.so/profile/integrations after creating an internal",
|
|
24
|
+
"# integration and sharing the target databases with it.",
|
|
25
|
+
"",
|
|
26
|
+
"NOTION_TOKEN=",
|
|
27
|
+
"",
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
async function copyOne(entry, dest, subs, options) {
|
|
31
|
+
const exists = await pathExists(dest);
|
|
32
|
+
const relPath = entry.relativePath;
|
|
33
|
+
if (exists && !options.force) {
|
|
34
|
+
return { action: "skipped", relativePath: relPath };
|
|
35
|
+
}
|
|
36
|
+
if (options.dryRun) {
|
|
37
|
+
return {
|
|
38
|
+
action: exists ? "would-overwrite" : "would-create",
|
|
39
|
+
relativePath: relPath,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const raw = await readText(entry.sourceAbs);
|
|
43
|
+
const out = isTextPath(entry.sourceAbs) ? applySubstitutions(raw, subs) : raw;
|
|
44
|
+
await writeText(dest, out);
|
|
45
|
+
return {
|
|
46
|
+
action: exists ? "overwritten" : "created",
|
|
47
|
+
relativePath: relPath,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function writeBlobIfNeeded(dest, relPath, contents, options) {
|
|
51
|
+
const exists = await pathExists(dest);
|
|
52
|
+
if (exists && !options.force) {
|
|
53
|
+
return { action: "skipped", relativePath: relPath };
|
|
54
|
+
}
|
|
55
|
+
if (options.dryRun) {
|
|
56
|
+
return {
|
|
57
|
+
action: exists ? "would-overwrite" : "would-create",
|
|
58
|
+
relativePath: relPath,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
await writeText(dest, contents);
|
|
62
|
+
return {
|
|
63
|
+
action: exists ? "overwritten" : "created",
|
|
64
|
+
relativePath: relPath,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function ensureGitignoreEntry(projectRoot, dryRun) {
|
|
68
|
+
const path = join(projectRoot, ".gitignore");
|
|
69
|
+
const existing = await readTextOrNull(path);
|
|
70
|
+
const line = VIBEOPS_ENV_FILE;
|
|
71
|
+
if (existing && existing.split("\n").some((l) => l.trim() === line)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (dryRun) {
|
|
75
|
+
return {
|
|
76
|
+
action: existing ? "would-overwrite" : "would-create",
|
|
77
|
+
relativePath: ".gitignore",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const next = existing
|
|
81
|
+
? `${existing.endsWith("\n") ? existing : `${existing}\n`}${line}\n`
|
|
82
|
+
: `${line}\n`;
|
|
83
|
+
await writeText(path, next);
|
|
84
|
+
return {
|
|
85
|
+
action: existing ? "overwritten" : "created",
|
|
86
|
+
relativePath: ".gitignore",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export async function install(options) {
|
|
90
|
+
const paths = projectPaths(options.projectRoot);
|
|
91
|
+
const subs = buildSubstitutions(options.config);
|
|
92
|
+
if (!options.dryRun) {
|
|
93
|
+
await ensureDir(paths.root);
|
|
94
|
+
}
|
|
95
|
+
const manifest = await loadManifest();
|
|
96
|
+
const outcomes = [];
|
|
97
|
+
for (const entry of manifest) {
|
|
98
|
+
const dest = resolveDestination(entry, paths.root);
|
|
99
|
+
const outcome = await copyOne(entry, dest, subs, options);
|
|
100
|
+
outcomes.push(outcome);
|
|
101
|
+
}
|
|
102
|
+
const configOutcome = await writeBlobIfNeeded(paths.config, ".vibeops.json", configToJson(options.config), options);
|
|
103
|
+
outcomes.push(configOutcome);
|
|
104
|
+
const envExampleOutcome = await writeBlobIfNeeded(paths.envExample, ".vibeops.env.example", envExampleContents(), options);
|
|
105
|
+
outcomes.push(envExampleOutcome);
|
|
106
|
+
const gitignoreOutcome = await ensureGitignoreEntry(paths.root, options.dryRun);
|
|
107
|
+
if (gitignoreOutcome)
|
|
108
|
+
outcomes.push(gitignoreOutcome);
|
|
109
|
+
let created = 0;
|
|
110
|
+
let overwritten = 0;
|
|
111
|
+
let skipped = 0;
|
|
112
|
+
for (const o of outcomes) {
|
|
113
|
+
if (o.action === "created" || o.action === "would-create")
|
|
114
|
+
created++;
|
|
115
|
+
else if (o.action === "overwritten" || o.action === "would-overwrite")
|
|
116
|
+
overwritten++;
|
|
117
|
+
else
|
|
118
|
+
skipped++;
|
|
119
|
+
}
|
|
120
|
+
return { files: outcomes, created, overwritten, skipped };
|
|
121
|
+
}
|
|
122
|
+
export function printReport(report, dryRun) {
|
|
123
|
+
for (const f of report.files) {
|
|
124
|
+
switch (f.action) {
|
|
125
|
+
case "created":
|
|
126
|
+
log.ok(`created ${f.relativePath}`);
|
|
127
|
+
break;
|
|
128
|
+
case "overwritten":
|
|
129
|
+
log.ok(`${yellow("overwrote")} ${f.relativePath}`);
|
|
130
|
+
break;
|
|
131
|
+
case "skipped":
|
|
132
|
+
log.skip(`skipped ${f.relativePath} (already exists)`);
|
|
133
|
+
break;
|
|
134
|
+
case "would-create":
|
|
135
|
+
log.info(`${dim("would create ")}${f.relativePath}`);
|
|
136
|
+
break;
|
|
137
|
+
case "would-overwrite":
|
|
138
|
+
log.info(`${dim("would overwrite")} ${f.relativePath}`);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
log.blank();
|
|
143
|
+
if (dryRun) {
|
|
144
|
+
log.info(`${green("dry-run")}: ${report.created} would be created, ${report.overwritten} would be overwritten, ${report.skipped} already exist.`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
log.info(`${green("done")}: ${report.created} created, ${report.overwritten} overwritten, ${report.skipped} skipped.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { join, relative } from "node:path";
|
|
2
|
+
import { walk } from "../lib/filesystem.js";
|
|
3
|
+
import { TEMPLATES_ROOT } from "../lib/paths.js";
|
|
4
|
+
export async function loadManifest() {
|
|
5
|
+
const files = await walk(TEMPLATES_ROOT);
|
|
6
|
+
return files
|
|
7
|
+
.map((abs) => ({
|
|
8
|
+
relativePath: relative(TEMPLATES_ROOT, abs),
|
|
9
|
+
sourceAbs: abs,
|
|
10
|
+
}))
|
|
11
|
+
.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
12
|
+
}
|
|
13
|
+
export function resolveDestination(entry, projectRoot) {
|
|
14
|
+
return join(projectRoot, entry.relativePath);
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const TEXT_EXTENSIONS = new Set([
|
|
2
|
+
".md",
|
|
3
|
+
".mdc",
|
|
4
|
+
".txt",
|
|
5
|
+
".json",
|
|
6
|
+
".yaml",
|
|
7
|
+
".yml",
|
|
8
|
+
".env",
|
|
9
|
+
".example",
|
|
10
|
+
]);
|
|
11
|
+
export function isTextPath(path) {
|
|
12
|
+
const lower = path.toLowerCase();
|
|
13
|
+
for (const ext of TEXT_EXTENSIONS) {
|
|
14
|
+
if (lower.endsWith(ext))
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (lower.endsWith("/readme"))
|
|
18
|
+
return true;
|
|
19
|
+
if (lower.endsWith("/agents.md"))
|
|
20
|
+
return true;
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
export function buildSubstitutions(config) {
|
|
24
|
+
return {
|
|
25
|
+
PROJECT_NAME: config.name,
|
|
26
|
+
VIBEOPS_VERSION: config.vibeopsVersion,
|
|
27
|
+
CREATED_AT: config.createdAt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function applySubstitutions(content, subs) {
|
|
31
|
+
return content
|
|
32
|
+
.replaceAll("{{PROJECT_NAME}}", subs.PROJECT_NAME)
|
|
33
|
+
.replaceAll("{{VIBEOPS_VERSION}}", subs.VIBEOPS_VERSION)
|
|
34
|
+
.replaceAll("{{CREATED_AT}}", subs.CREATED_AT);
|
|
35
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { agentListCommand } from "./commands/agent-list.js";
|
|
4
|
+
import { agentPromptCommand } from "./commands/agent-prompt.js";
|
|
5
|
+
import { agentShowCommand } from "./commands/agent-show.js";
|
|
6
|
+
import { githubInitCommand } from "./commands/github-init.js";
|
|
7
|
+
import { githubStatusCommand } from "./commands/github-status.js";
|
|
8
|
+
import { initCommand } from "./commands/init.js";
|
|
9
|
+
import { notionInitCommand } from "./commands/notion-init.js";
|
|
10
|
+
import { notionSyncCommand } from "./commands/notion-sync.js";
|
|
11
|
+
import { notionTestCommand } from "./commands/notion-test.js";
|
|
12
|
+
import { planCommand } from "./commands/plan.js";
|
|
13
|
+
import { statusCommand } from "./commands/status.js";
|
|
14
|
+
import { taskCheckCommand } from "./commands/task-check.js";
|
|
15
|
+
import { taskDoneCommand } from "./commands/task-done.js";
|
|
16
|
+
import { taskGenerateCommand } from "./commands/task-generate.js";
|
|
17
|
+
import { taskPullCommand } from "./commands/task-pull.js";
|
|
18
|
+
import { taskRollbackCommand } from "./commands/task-rollback.js";
|
|
19
|
+
import { taskStartCommand } from "./commands/task-start.js";
|
|
20
|
+
import { VERSION } from "./version.js";
|
|
21
|
+
const program = new Command();
|
|
22
|
+
program
|
|
23
|
+
.name("vibeops")
|
|
24
|
+
.description("VibeOps — a local CLI that keeps Cursor-based vibe coding on rails.\n" +
|
|
25
|
+
" It installs docs, Cursor rules, AGENTS.md, agents, TASK templates,\n" +
|
|
26
|
+
" and Git/Notion workflows into a project, then drives work one TASK at a time.")
|
|
27
|
+
.version(VERSION, "-v, --version", "Print the VibeOps version");
|
|
28
|
+
program
|
|
29
|
+
.command("init")
|
|
30
|
+
.description("Install the VibeOps workflow files into the current directory")
|
|
31
|
+
.option("--dry-run", "Show what would be created without writing any files")
|
|
32
|
+
.option("--force", "Overwrite existing files (use with care)")
|
|
33
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
34
|
+
.option("--name <projectName>", "Project name written into .vibeops.json")
|
|
35
|
+
.option("--git", "Initialize a Git repository without prompting")
|
|
36
|
+
.option("--no-git", "Skip Git initialization and commits")
|
|
37
|
+
.option("--initial-commit", "Run `git add .` and create an initial commit")
|
|
38
|
+
.option("--no-initial-commit", "Do not create an initial commit")
|
|
39
|
+
.option("--default-branch <name>", "Default Git branch name (default `main`)")
|
|
40
|
+
.option("--commit-message <message>", "Initial commit message (default 'chore: initialize vibeops project')")
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
await initCommand(options);
|
|
43
|
+
});
|
|
44
|
+
program
|
|
45
|
+
.command("status")
|
|
46
|
+
.description("Show VibeOps installation, TASK counts, and integration state")
|
|
47
|
+
.option("--json", "Print machine-readable JSON")
|
|
48
|
+
.option("--cwd <path>", "Inspect a different directory")
|
|
49
|
+
.action(async (options) => {
|
|
50
|
+
await statusCommand(options);
|
|
51
|
+
});
|
|
52
|
+
program
|
|
53
|
+
.command("plan")
|
|
54
|
+
.description("Run 20 interactive questions and produce a ProjectBrief + Cursor planning prompt")
|
|
55
|
+
.option("--idea <text>", "One-line idea default (use `Name: idea` to extract the project name)")
|
|
56
|
+
.option("--from <path>", "Read an existing brief markdown and regenerate the prompt")
|
|
57
|
+
.option("--output <path>", "Output path for the Cursor planning prompt (default `.vibeops/generated/plan-prompt.md`)")
|
|
58
|
+
.option("--non-interactive", "Skip prompts and use the supplied values plus safe placeholders")
|
|
59
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
await planCommand(options);
|
|
62
|
+
});
|
|
63
|
+
const agent = program
|
|
64
|
+
.command("agent")
|
|
65
|
+
.description("Inspect the `.vibeops/agents/*` agent definitions");
|
|
66
|
+
agent
|
|
67
|
+
.command("list")
|
|
68
|
+
.description("List available agents")
|
|
69
|
+
.option("--json", "Print machine-readable JSON")
|
|
70
|
+
.option("--cwd <path>", "Inspect a different directory")
|
|
71
|
+
.action(async (options) => {
|
|
72
|
+
await agentListCommand(options);
|
|
73
|
+
});
|
|
74
|
+
agent
|
|
75
|
+
.command("show <name>")
|
|
76
|
+
.description("Print an agent definition body")
|
|
77
|
+
.option("--raw", "Include frontmatter in the output")
|
|
78
|
+
.option("--cwd <path>", "Inspect a different directory")
|
|
79
|
+
.action(async (name, options) => {
|
|
80
|
+
await agentShowCommand(name, options);
|
|
81
|
+
});
|
|
82
|
+
agent
|
|
83
|
+
.command("prompt <name> <taskId>")
|
|
84
|
+
.description("Print a Cursor-ready prompt built from the agent + TASK context")
|
|
85
|
+
.option("--context <path...>", "Additional context file paths")
|
|
86
|
+
.option("--cwd <path>", "Inspect a different directory")
|
|
87
|
+
.action(async (name, taskId, options) => {
|
|
88
|
+
await agentPromptCommand(name, taskId, options);
|
|
89
|
+
});
|
|
90
|
+
const task = program
|
|
91
|
+
.command("task")
|
|
92
|
+
.description("TASK lifecycle commands");
|
|
93
|
+
task
|
|
94
|
+
.command("generate")
|
|
95
|
+
.description("Build a Cursor prompt for generating TASK files, or with --scaffold write skeleton TASK markdown")
|
|
96
|
+
.option("--from <path>", "Primary backlog/brief markdown to feed into the prompt")
|
|
97
|
+
.option("--output <path>", "Output path for the generated prompt (default `.vibeops/generated/task-generate-prompt.md`)")
|
|
98
|
+
.option("--count <number>", "Suggested TASK count for Cursor (default 8, warns above 20)")
|
|
99
|
+
.option("--phase <name>", "Generate TASKs for a specific phase label only (e.g. 'MVP 4')")
|
|
100
|
+
.option("--scaffold", "Write skeleton TASK markdown files directly, without an LLM")
|
|
101
|
+
.option("--dry-run", "Print the plan without writing or modifying files")
|
|
102
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
103
|
+
.action(async (options) => {
|
|
104
|
+
await taskGenerateCommand(options);
|
|
105
|
+
});
|
|
106
|
+
task
|
|
107
|
+
.command("start <taskId>")
|
|
108
|
+
.description("Confirm a clean working tree, create the task branch, record Status/Git context, and print a Builder prompt")
|
|
109
|
+
.option("--dry-run", "Print the plan without touching files or Git")
|
|
110
|
+
.option("--allow-dirty", "Proceed even if the Git working tree is dirty")
|
|
111
|
+
.option("--agent <name>", "Agent to build the prompt with (default `builder`)")
|
|
112
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
113
|
+
.action(async (taskId, options) => {
|
|
114
|
+
await taskStartCommand(taskId, options);
|
|
115
|
+
});
|
|
116
|
+
task
|
|
117
|
+
.command("prompt <taskId>")
|
|
118
|
+
.description("Print a Cursor-ready prompt built from the TASK + agent context")
|
|
119
|
+
.option("--agent <name>", "Agent name (orchestrator / planner / architect / builder / reviewer / tester / docs / recovery)")
|
|
120
|
+
.option("--context <path...>", "Additional context file paths")
|
|
121
|
+
.option("--cwd <path>", "Inspect a different directory")
|
|
122
|
+
.action(async (taskId, options) => {
|
|
123
|
+
await agentPromptCommand(options.agent ?? "builder", taskId, {
|
|
124
|
+
cwd: options.cwd,
|
|
125
|
+
context: options.context,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
task
|
|
129
|
+
.command("check <taskId>")
|
|
130
|
+
.description("Read-only check: git diff/log + acceptance criteria + doc updates + Result fields + a Reviewer prompt")
|
|
131
|
+
.option("--strict", "Exit with code 1 if any required item is missing")
|
|
132
|
+
.option("--agent <name>", "Agent to build the prompt with (default `reviewer`)")
|
|
133
|
+
.option("--cwd <path>", "Inspect a different directory")
|
|
134
|
+
.action(async (taskId, options) => {
|
|
135
|
+
await taskCheckCommand(taskId, options);
|
|
136
|
+
});
|
|
137
|
+
task
|
|
138
|
+
.command("done <taskId>")
|
|
139
|
+
.description("Validate Result/Test Result, move Status to Review, and print a commit message (no auto-commit)")
|
|
140
|
+
.option("--dry-run", "Print the plan without touching files")
|
|
141
|
+
.option("--finalize", "Move Status to Done instead of Review (use after human review)")
|
|
142
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
143
|
+
.action(async (taskId, options) => {
|
|
144
|
+
await taskDoneCommand(taskId, options);
|
|
145
|
+
});
|
|
146
|
+
task
|
|
147
|
+
.command("rollback <taskId>")
|
|
148
|
+
.description("Default: advisory only. --confirm: non-destructive rollback. --confirm-destructive: hard reset.")
|
|
149
|
+
.option("--confirm", "Allow non-destructive rollback execution (branch-delete etc.)")
|
|
150
|
+
.option("--confirm-destructive", "Allow destructive rollback execution (reset --hard etc.)")
|
|
151
|
+
.option("--strategy <name>", "branch-delete | reset-base | revert-merge (default branch-delete)")
|
|
152
|
+
.option("--keep-branch", "Keep the task branch even when using branch-delete")
|
|
153
|
+
.option("--dry-run", "Print the plan without running any git commands, even with --confirm")
|
|
154
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
155
|
+
.action(async (taskId, options) => {
|
|
156
|
+
await taskRollbackCommand(taskId, options);
|
|
157
|
+
});
|
|
158
|
+
task
|
|
159
|
+
.command("pull")
|
|
160
|
+
.description("Generate `docs/tasks/*.md` skeletons from Notion Tasks DB rows (defaults to Status = Planned)")
|
|
161
|
+
.option("--dry-run", "Print the plan without touching files or Notion")
|
|
162
|
+
.option("--json", "Print machine-readable JSON")
|
|
163
|
+
.option("--status <name>", "Notion Status values to pull (comma-separated, e.g. 'Planned,Ready'). Default `Planned`")
|
|
164
|
+
.option("--limit <number>", "Maximum rows to pull from Notion (default 20, max 100)")
|
|
165
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
166
|
+
.option("--verbose", "Print a decision trace per considered row (taskId / pageId / docsPath / reason)")
|
|
167
|
+
.action(async (options) => {
|
|
168
|
+
await taskPullCommand(options);
|
|
169
|
+
});
|
|
170
|
+
const notion = program
|
|
171
|
+
.command("notion")
|
|
172
|
+
.description("Notion dashboard sync");
|
|
173
|
+
notion
|
|
174
|
+
.command("init")
|
|
175
|
+
.description("Interactive setup: use arrow keys + Enter to pick Yes/No, then write `.vibeops.json` `notion` section and `.vibeops.env(.example)`")
|
|
176
|
+
.option("--dry-run", "Print the plan without changing files (no interactive prompts)")
|
|
177
|
+
.option("--enable", "Set `notion.enabled = true` and skip the first prompt")
|
|
178
|
+
.option("--projects-db <id>", "Set `notion.projectsDatabaseId` (skip the prompt)")
|
|
179
|
+
.option("--tasks-db <id>", "Set `notion.tasksDatabaseId` (skip the prompt)")
|
|
180
|
+
.option("--non-interactive", "Force non-interactive mode in a TTY (use flag values + safe defaults only)")
|
|
181
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
182
|
+
.action(async (options) => {
|
|
183
|
+
await notionInitCommand(options);
|
|
184
|
+
});
|
|
185
|
+
notion
|
|
186
|
+
.command("test")
|
|
187
|
+
.description("Read-only check: Notion API auth + access to Projects/Tasks DBs + required-property schema validation")
|
|
188
|
+
.option("--json", "Print machine-readable JSON")
|
|
189
|
+
.option("--debug-shape", "Also print a token-safe diagnostic of the Projects/Tasks retrieve responses (top-level keys, data_sources, etc.)")
|
|
190
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
191
|
+
.action(async (options) => {
|
|
192
|
+
await notionTestCommand(options);
|
|
193
|
+
});
|
|
194
|
+
notion
|
|
195
|
+
.command("sync")
|
|
196
|
+
.description("Push `docs/project` + `docs/tasks` metadata into Notion Projects/Tasks DBs (read-only on local files)")
|
|
197
|
+
.option("--dry-run", "Print the plan without any Notion mutation (queries only)")
|
|
198
|
+
.option("--json", "Print machine-readable JSON")
|
|
199
|
+
.option("--only-tasks", "Sync the Tasks DB only (leave the Project row alone)")
|
|
200
|
+
.option("--only-project", "Sync the Project DB only (leave Task rows alone)")
|
|
201
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
202
|
+
.action(async (options) => {
|
|
203
|
+
await notionSyncCommand(options);
|
|
204
|
+
});
|
|
205
|
+
const github = program
|
|
206
|
+
.command("github")
|
|
207
|
+
.description("GitHub repository integration");
|
|
208
|
+
github
|
|
209
|
+
.command("status")
|
|
210
|
+
.description("Read-only check: gh install/auth + git remotes + `.vibeops.json` github section + `package.json` repo fields")
|
|
211
|
+
.option("--json", "Print machine-readable JSON")
|
|
212
|
+
.option("--cwd <path>", "Inspect a different directory")
|
|
213
|
+
.action(async (options) => {
|
|
214
|
+
await githubStatusCommand(options);
|
|
215
|
+
});
|
|
216
|
+
github
|
|
217
|
+
.command("init")
|
|
218
|
+
.description("Interactive setup: check gh auth, manage the git remote, optionally `gh repo create`, then update `.vibeops.json` and `package.json`")
|
|
219
|
+
.option("--dry-run", "Print the plan without running gh / git or writing files")
|
|
220
|
+
.option("--yes", "Skip interactive prompts and use safe defaults")
|
|
221
|
+
.option("--owner <owner>", "GitHub owner (user or org)")
|
|
222
|
+
.option("--repo <repo>", "GitHub repo name")
|
|
223
|
+
.option("--public", "Force visibility = public (skip the prompt)")
|
|
224
|
+
.option("--private", "Force visibility = private (skip the prompt)")
|
|
225
|
+
.option("--remote <name>", "Git remote name (default `origin`)")
|
|
226
|
+
.option("--connect <ownerOrUrl>", "Connect to an existing repo instead of creating one (owner/repo or https/ssh URL)")
|
|
227
|
+
.option("--no-package-update", "Do not modify `package.json` repository/homepage/bugs fields")
|
|
228
|
+
.option("--cwd <path>", "Run against a different directory")
|
|
229
|
+
.action(async (options) => {
|
|
230
|
+
await githubInitCommand({
|
|
231
|
+
...options,
|
|
232
|
+
// Commander turns `--no-package-update` into `packageUpdate: false`.
|
|
233
|
+
// Our command type still accepts the legacy `noPackageUpdate`
|
|
234
|
+
// shape for tests / direct invocation.
|
|
235
|
+
noPackageUpdate: options.packageUpdate === false,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
239
|
+
console.error("[vibeops] error:", err);
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { listAgents } from "../agent/loader.js";
|
|
3
|
+
import { bold, dim, log } from "../lib/logger.js";
|
|
4
|
+
import { projectPaths } from "../lib/paths.js";
|
|
5
|
+
export async function agentListCommand(options = {}) {
|
|
6
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
7
|
+
const paths = projectPaths(cwd);
|
|
8
|
+
const agents = await listAgents(paths.vibeopsAgents);
|
|
9
|
+
if (options.json) {
|
|
10
|
+
const payload = agents.map((a) => ({
|
|
11
|
+
name: a.meta.name,
|
|
12
|
+
role: a.meta.role,
|
|
13
|
+
description: a.meta.description ?? null,
|
|
14
|
+
filePath: a.meta.filePath,
|
|
15
|
+
}));
|
|
16
|
+
log.raw(`${JSON.stringify(payload, null, 2)}\n`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (agents.length === 0) {
|
|
20
|
+
log.warn(`No agents found in ${paths.vibeopsAgents}`);
|
|
21
|
+
log.info(dim("Run `vibeops init` first."));
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
log.info(bold("Agents"));
|
|
26
|
+
const nameWidth = Math.max(...agents.map((a) => a.meta.name.length));
|
|
27
|
+
for (const a of agents) {
|
|
28
|
+
const padded = a.meta.name.padEnd(nameWidth);
|
|
29
|
+
const summary = a.meta.description ?? a.meta.role;
|
|
30
|
+
log.info(` ${padded} ${dim(summary)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { findAgent, listAgents } from "../agent/loader.js";
|
|
3
|
+
import { buildPrompt } from "../agent/prompt.js";
|
|
4
|
+
import { readConfig } from "../lib/config.js";
|
|
5
|
+
import { readText } from "../lib/filesystem.js";
|
|
6
|
+
import { log } from "../lib/logger.js";
|
|
7
|
+
import { projectPaths } from "../lib/paths.js";
|
|
8
|
+
import { readTaskFile, scanTasks } from "../lib/task.js";
|
|
9
|
+
async function locateTaskFile(tasksDir, taskId) {
|
|
10
|
+
const all = await scanTasks(tasksDir);
|
|
11
|
+
const target = taskId.toUpperCase();
|
|
12
|
+
for (const t of all) {
|
|
13
|
+
if (t.id.toUpperCase() === target)
|
|
14
|
+
return t.filePath;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
export async function agentPromptCommand(name, taskId, options = {}) {
|
|
19
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
20
|
+
const paths = projectPaths(cwd);
|
|
21
|
+
const agent = await findAgent(paths.vibeopsAgents, name);
|
|
22
|
+
if (!agent) {
|
|
23
|
+
const available = (await listAgents(paths.vibeopsAgents)).map((a) => a.meta.name);
|
|
24
|
+
log.error(`Unknown agent: "${name}".`);
|
|
25
|
+
if (available.length > 0) {
|
|
26
|
+
log.info(`Available: ${available.join(", ")}`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
log.info(`No agents installed in ${paths.vibeopsAgents}. Run \`vibeops init\` first.`);
|
|
30
|
+
}
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const config = await readConfig(paths.root);
|
|
35
|
+
let task;
|
|
36
|
+
const looksLikeTaskId = /^TASK-\d+/i.test(taskId);
|
|
37
|
+
if (looksLikeTaskId) {
|
|
38
|
+
const filePath = await locateTaskFile(paths.docsTasks, taskId);
|
|
39
|
+
if (!filePath) {
|
|
40
|
+
log.error(`TASK not found: ${taskId} (looked in ${paths.docsTasks})`);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const meta = await readTaskFile(filePath);
|
|
45
|
+
const body = await readText(filePath);
|
|
46
|
+
task = { meta, body };
|
|
47
|
+
}
|
|
48
|
+
else if (taskId !== "(unspecified)" && taskId.length > 0) {
|
|
49
|
+
log.warn(`"${taskId}" does not look like a TASK id (expected TASK-NNN). Continuing without TASK context.`);
|
|
50
|
+
}
|
|
51
|
+
const prompt = await buildPrompt({
|
|
52
|
+
agent,
|
|
53
|
+
config,
|
|
54
|
+
task,
|
|
55
|
+
projectRoot: paths.root,
|
|
56
|
+
contextPaths: options.context ?? [],
|
|
57
|
+
});
|
|
58
|
+
log.raw(prompt.endsWith("\n") ? prompt : `${prompt}\n`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { findAgent, listAgents } from "../agent/loader.js";
|
|
3
|
+
import { log } from "../lib/logger.js";
|
|
4
|
+
import { projectPaths } from "../lib/paths.js";
|
|
5
|
+
export async function agentShowCommand(name, options = {}) {
|
|
6
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
7
|
+
const paths = projectPaths(cwd);
|
|
8
|
+
const agent = await findAgent(paths.vibeopsAgents, name);
|
|
9
|
+
if (!agent) {
|
|
10
|
+
const available = (await listAgents(paths.vibeopsAgents)).map((a) => a.meta.name);
|
|
11
|
+
log.error(`Unknown agent: "${name}".`);
|
|
12
|
+
if (available.length > 0) {
|
|
13
|
+
log.info(`Available: ${available.join(", ")}`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
log.info(`No agents installed in ${paths.vibeopsAgents}. Run \`vibeops init\` first.`);
|
|
17
|
+
}
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (options.raw) {
|
|
22
|
+
log.raw(agent.raw.endsWith("\n") ? agent.raw : `${agent.raw}\n`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
log.raw(agent.body.endsWith("\n") ? agent.body : `${agent.body}\n`);
|
|
26
|
+
}
|