@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,554 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import { select } from "@inquirer/prompts";
|
|
3
|
+
import { mergeGithubConfig, readConfig, writeConfig } from "../lib/config.js";
|
|
4
|
+
import { pathExists } from "../lib/filesystem.js";
|
|
5
|
+
import { buildGhCreateRepoArgs, ghAuthLoginInteractive, ghAuthStatus, ghCreateRepo, ghCurrentUser, gitRemoteAdd, gitRemoteList, gitRemoteSetUrl, isGhInstalled, parseGitHubRemote, } from "../lib/github-cli.js";
|
|
6
|
+
import { askInput, askYesNo } from "../lib/inquirer-helpers.js";
|
|
7
|
+
import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
8
|
+
import { buildRepositoryFieldsPatch, readPackageJson, readBugsUrl, readHomepage, readRepositoryUrl, updatePackageRepositoryFields, } from "../lib/package-json.js";
|
|
9
|
+
const REMOTE_DEFAULT = "origin";
|
|
10
|
+
export async function githubInitCommand(options = {}) {
|
|
11
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
12
|
+
const dryRun = options.dryRun === true;
|
|
13
|
+
const explicitYes = options.yes === true;
|
|
14
|
+
const isTty = process.stdin.isTTY === true;
|
|
15
|
+
const interactive = !dryRun && !explicitYes && isTty;
|
|
16
|
+
const ctx = {
|
|
17
|
+
cwd,
|
|
18
|
+
dryRun,
|
|
19
|
+
interactive,
|
|
20
|
+
remoteName: options.remote && options.remote.length > 0 ? options.remote : REMOTE_DEFAULT,
|
|
21
|
+
forcedVisibility: options.public === true ? "public" : options.private === true ? "private" : "",
|
|
22
|
+
connectTarget: typeof options.connect === "string" && options.connect.length > 0
|
|
23
|
+
? parseGitHubRemote(options.connect)
|
|
24
|
+
: null,
|
|
25
|
+
packageUpdate: options.noPackageUpdate === true
|
|
26
|
+
? false
|
|
27
|
+
: typeof options.packageUpdate === "boolean"
|
|
28
|
+
? options.packageUpdate
|
|
29
|
+
: true,
|
|
30
|
+
ownerFlag: typeof options.owner === "string" ? options.owner.trim() : "",
|
|
31
|
+
repoFlag: typeof options.repo === "string" ? options.repo.trim() : "",
|
|
32
|
+
configPath: resolve(cwd, ".vibeops.json"),
|
|
33
|
+
packageJsonPath: resolve(cwd, "package.json"),
|
|
34
|
+
};
|
|
35
|
+
log.info(bold("vibeops github init"));
|
|
36
|
+
log.info(` ${dim("cwd")} ${cwd}`);
|
|
37
|
+
log.info(` ${dim("mode")} ${dryRun
|
|
38
|
+
? "dry-run (no gh / git / file mutation)"
|
|
39
|
+
: interactive
|
|
40
|
+
? "interactive (arrow keys + Enter, no y/n typing)"
|
|
41
|
+
: "non-interactive (flags only)"}`);
|
|
42
|
+
if (ctx.connectTarget !== null) {
|
|
43
|
+
if (!ctx.connectTarget.isGithub) {
|
|
44
|
+
log.error(`--connect ${cyan(ctx.connectTarget.url)} is not a GitHub URL or owner/repo slug. Expected 'https://github.com/<owner>/<repo>' or 'owner/repo'.`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
log.info(` ${dim("connect")} ${cyan(`${ctx.connectTarget.owner}/${ctx.connectTarget.repo}`)}`);
|
|
49
|
+
}
|
|
50
|
+
log.blank();
|
|
51
|
+
if (!(await pathExists(ctx.configPath))) {
|
|
52
|
+
log.error(`.vibeops.json is missing. Run ${cyan("vibeops init")} first to install the VibeOps workflow files.`);
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const baseConfig = await readConfig(cwd);
|
|
57
|
+
if (baseConfig === null) {
|
|
58
|
+
log.error(`Failed to read .vibeops.json (schema mismatch or invalid JSON). Inspect the file, or re-run ${cyan("vibeops init")} to recreate it.`);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const plan = {
|
|
63
|
+
ghInstalled: false,
|
|
64
|
+
ghAuthenticated: false,
|
|
65
|
+
remoteUsed: false,
|
|
66
|
+
remoteAdded: false,
|
|
67
|
+
remoteUrl: null,
|
|
68
|
+
remoteSetUrl: false,
|
|
69
|
+
repoCreated: false,
|
|
70
|
+
repoCreationCommand: null,
|
|
71
|
+
owner: ctx.ownerFlag,
|
|
72
|
+
repo: ctx.repoFlag,
|
|
73
|
+
visibility: ctx.forcedVisibility,
|
|
74
|
+
description: "",
|
|
75
|
+
configChanged: false,
|
|
76
|
+
packageWrites: null,
|
|
77
|
+
packageDiffs: [],
|
|
78
|
+
};
|
|
79
|
+
// ── A. gh installed ──────────────────────────────────────────────────────
|
|
80
|
+
plan.ghInstalled = await isGhInstalled();
|
|
81
|
+
if (!plan.ghInstalled) {
|
|
82
|
+
if (dryRun) {
|
|
83
|
+
log.warn(`gh CLI is not installed. Install with ${cyan("brew install gh")} (macOS) or via https://cli.github.com/ before running for real. The dry-run continues so you can review the plan.`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
log.error(`gh CLI is not installed. Install with ${cyan("brew install gh")} (macOS) or via https://cli.github.com/ and retry.`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
log.ok(`gh CLI installed`);
|
|
93
|
+
}
|
|
94
|
+
// ── B. gh authenticated ──────────────────────────────────────────────────
|
|
95
|
+
const auth = plan.ghInstalled
|
|
96
|
+
? await ghAuthStatus()
|
|
97
|
+
: {
|
|
98
|
+
installed: false,
|
|
99
|
+
authenticated: false,
|
|
100
|
+
username: null,
|
|
101
|
+
hosts: [],
|
|
102
|
+
detail: "gh CLI not installed",
|
|
103
|
+
};
|
|
104
|
+
plan.ghAuthenticated = auth.authenticated;
|
|
105
|
+
if (!auth.authenticated) {
|
|
106
|
+
if (dryRun) {
|
|
107
|
+
log.warn(`gh is not authenticated. Run ${cyan("gh auth login")} before applying. The dry-run continues.`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
log.warn(`gh is not authenticated. Run ${cyan("gh auth login")} first.`);
|
|
111
|
+
let runLogin = false;
|
|
112
|
+
if (interactive) {
|
|
113
|
+
runLogin = await askYesNo({
|
|
114
|
+
message: "Run gh auth login now? (TTY is handed off to the child process)",
|
|
115
|
+
nonInteractive: false,
|
|
116
|
+
defaultValue: true,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else if (explicitYes) {
|
|
120
|
+
// --yes alone does not auto-spawn gh auth login (TTY required).
|
|
121
|
+
log.info(dim(` · --yes alone does not run gh auth login. Re-run interactively to authenticate.`));
|
|
122
|
+
}
|
|
123
|
+
if (runLogin) {
|
|
124
|
+
log.info(dim(` · spawning: gh auth login`));
|
|
125
|
+
const code = await ghAuthLoginInteractive();
|
|
126
|
+
if (code !== 0) {
|
|
127
|
+
log.error(`gh auth login exited with code ${code}. Please retry.`);
|
|
128
|
+
process.exitCode = code;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const auth2 = await ghAuthStatus();
|
|
132
|
+
plan.ghAuthenticated = auth2.authenticated;
|
|
133
|
+
if (!plan.ghAuthenticated) {
|
|
134
|
+
log.error("gh auth login finished but the status is still unauthenticated. Inspect with 'gh auth status'.");
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
log.ok("gh authenticated");
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
log.info(dim(` · Cannot create a new GitHub repo without gh auth login. Exiting.`));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
log.ok(`gh authenticated${auth.username !== null ? ` ${dim(`as ${auth.username}`)}` : ""}`);
|
|
149
|
+
}
|
|
150
|
+
// ── C. inspect current remotes ───────────────────────────────────────────
|
|
151
|
+
const remotes = await gitRemoteList(cwd);
|
|
152
|
+
const existingRemote = remotes.find((r) => r.name === ctx.remoteName) ?? null;
|
|
153
|
+
const existingInfo = existingRemote !== null ? parseGitHubRemote(existingRemote.url) : null;
|
|
154
|
+
log.info(`${dim("git remote")} ${cyan(ctx.remoteName)}${existingRemote === null
|
|
155
|
+
? ` ${dim("(none)")}`
|
|
156
|
+
: ` ${existingRemote.url}${existingInfo?.isGithub ? ` ${dim(`(${existingInfo.owner}/${existingInfo.repo})`)}` : ` ${yellow("(not a GitHub URL)")}`}`}`);
|
|
157
|
+
let path;
|
|
158
|
+
if (ctx.connectTarget !== null) {
|
|
159
|
+
path = "connect-existing";
|
|
160
|
+
}
|
|
161
|
+
else if (existingRemote !== null &&
|
|
162
|
+
existingInfo !== null &&
|
|
163
|
+
existingInfo.isGithub === true) {
|
|
164
|
+
if (interactive) {
|
|
165
|
+
const useThis = await askYesNo({
|
|
166
|
+
message: `Use existing remote ${cyan(`${ctx.remoteName} = ${existingRemote.url}`)} (${existingInfo.owner}/${existingInfo.repo})?`,
|
|
167
|
+
nonInteractive: false,
|
|
168
|
+
defaultValue: true,
|
|
169
|
+
});
|
|
170
|
+
path = useThis ? "use-existing" : "create-new";
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
path = "use-existing";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (existingRemote !== null && existingInfo !== null && !existingInfo.isGithub) {
|
|
177
|
+
// existing non-github remote — propose new repo by default.
|
|
178
|
+
if (interactive) {
|
|
179
|
+
const overwrite = await askYesNo({
|
|
180
|
+
message: `${ctx.remoteName} is not a GitHub URL. Create a new GitHub repo and overwrite ${ctx.remoteName}?`,
|
|
181
|
+
nonInteractive: false,
|
|
182
|
+
defaultValue: false,
|
|
183
|
+
});
|
|
184
|
+
path = overwrite ? "create-new" : "use-existing";
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
path = "use-existing";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// no existing remote
|
|
192
|
+
if (interactive) {
|
|
193
|
+
const wantCreate = await askYesNo({
|
|
194
|
+
message: "Create a new GitHub repo? (No → enter an existing GitHub URL to connect)",
|
|
195
|
+
nonInteractive: false,
|
|
196
|
+
defaultValue: true,
|
|
197
|
+
});
|
|
198
|
+
path = wantCreate ? "create-new" : "connect-existing";
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
path = ctx.ownerFlag.length > 0 && ctx.repoFlag.length > 0 ? "create-new" : "connect-existing";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
log.blank();
|
|
205
|
+
log.info(bold(`Path: ${path}`));
|
|
206
|
+
// ── package.json defaults (used by D and to render F preview) ────────────
|
|
207
|
+
const pkg = await readPackageJson(cwd);
|
|
208
|
+
if (pkg !== null) {
|
|
209
|
+
log.info(` ${dim("package.json")} ${cyan(pkg.path)}`);
|
|
210
|
+
}
|
|
211
|
+
else if (path === "create-new") {
|
|
212
|
+
log.info(` ${dim("package.json")} ${dim("(not found — repo name/description defaults will use folder name only)")}`);
|
|
213
|
+
}
|
|
214
|
+
// ── execute the chosen path ──────────────────────────────────────────────
|
|
215
|
+
if (path === "use-existing") {
|
|
216
|
+
if (existingInfo !== null && existingInfo.isGithub) {
|
|
217
|
+
plan.remoteUsed = true;
|
|
218
|
+
plan.owner = existingInfo.owner ?? "";
|
|
219
|
+
plan.repo = existingInfo.repo ?? "";
|
|
220
|
+
plan.remoteUrl = existingRemote.url;
|
|
221
|
+
}
|
|
222
|
+
else if (existingInfo !== null) {
|
|
223
|
+
// user opted to keep a non-github remote — record best-effort
|
|
224
|
+
plan.remoteUsed = true;
|
|
225
|
+
plan.remoteUrl = existingRemote.url;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (path === "create-new") {
|
|
229
|
+
const owner = ctx.ownerFlag.length > 0
|
|
230
|
+
? ctx.ownerFlag
|
|
231
|
+
: (await ghCurrentUser()) ??
|
|
232
|
+
(interactive
|
|
233
|
+
? await askInput({
|
|
234
|
+
message: "GitHub owner (user or org)",
|
|
235
|
+
nonInteractive: false,
|
|
236
|
+
required: true,
|
|
237
|
+
})
|
|
238
|
+
: "");
|
|
239
|
+
if (owner.length === 0) {
|
|
240
|
+
log.error(`Could not determine owner. Pass ${cyan("--owner <user>")} explicitly, or run ${cyan("gh auth login")} first.`);
|
|
241
|
+
process.exitCode = 1;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const defaultRepo = ctx.repoFlag.length > 0
|
|
245
|
+
? ctx.repoFlag
|
|
246
|
+
: typeof pkg?.data.name === "string" && pkg.data.name.length > 0
|
|
247
|
+
? pkg.data.name
|
|
248
|
+
: basename(cwd);
|
|
249
|
+
const repoName = interactive
|
|
250
|
+
? await askInput({
|
|
251
|
+
message: "Repository name",
|
|
252
|
+
nonInteractive: false,
|
|
253
|
+
default: defaultRepo,
|
|
254
|
+
required: true,
|
|
255
|
+
})
|
|
256
|
+
: defaultRepo;
|
|
257
|
+
if (repoName.length === 0) {
|
|
258
|
+
log.error("Repo name is empty.");
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const defaultDesc = typeof pkg?.data.description === "string" ? pkg.data.description : "";
|
|
263
|
+
const description = interactive
|
|
264
|
+
? await askInput({
|
|
265
|
+
message: "Description (Enter to skip)",
|
|
266
|
+
nonInteractive: false,
|
|
267
|
+
default: defaultDesc,
|
|
268
|
+
})
|
|
269
|
+
: defaultDesc;
|
|
270
|
+
let resolvedVisibility;
|
|
271
|
+
if (ctx.forcedVisibility === "public" || ctx.forcedVisibility === "private") {
|
|
272
|
+
resolvedVisibility = ctx.forcedVisibility;
|
|
273
|
+
}
|
|
274
|
+
else if (interactive) {
|
|
275
|
+
resolvedVisibility = await pickVisibility();
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
resolvedVisibility = "private";
|
|
279
|
+
}
|
|
280
|
+
plan.owner = owner;
|
|
281
|
+
plan.repo = repoName;
|
|
282
|
+
plan.visibility = resolvedVisibility;
|
|
283
|
+
plan.description = description;
|
|
284
|
+
const argv = buildGhCreateRepoArgs({
|
|
285
|
+
owner,
|
|
286
|
+
repo: repoName,
|
|
287
|
+
visibility: resolvedVisibility,
|
|
288
|
+
source: cwd,
|
|
289
|
+
remote: ctx.remoteName,
|
|
290
|
+
description: description.length > 0 ? description : undefined,
|
|
291
|
+
});
|
|
292
|
+
plan.repoCreationCommand = `gh ${argv.join(" ")}`;
|
|
293
|
+
plan.remoteUrl = `https://github.com/${owner}/${repoName}`;
|
|
294
|
+
plan.repoCreated = true;
|
|
295
|
+
plan.remoteAdded = existingRemote === null; // gh repo create adds remote when missing
|
|
296
|
+
plan.remoteSetUrl = existingRemote !== null;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// connect-existing
|
|
300
|
+
let target = ctx.connectTarget;
|
|
301
|
+
if (target === null) {
|
|
302
|
+
let ans = "";
|
|
303
|
+
if (interactive) {
|
|
304
|
+
ans = await askInput({
|
|
305
|
+
message: "Existing GitHub URL or owner/repo slug",
|
|
306
|
+
nonInteractive: false,
|
|
307
|
+
required: true,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
else if (ctx.ownerFlag.length > 0 && ctx.repoFlag.length > 0) {
|
|
311
|
+
ans = `${ctx.ownerFlag}/${ctx.repoFlag}`;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
log.error(`Non-interactive mode (dry-run / --yes / non-TTY) requires ${cyan("--connect <owner/repo or url>")} or ${cyan("--owner <user> --repo <name>")}.`);
|
|
315
|
+
log.info(dim(` example: vibeops github init --dry-run --connect goodtekxyz/vibeops`));
|
|
316
|
+
process.exitCode = 1;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
target = parseGitHubRemote(ans);
|
|
320
|
+
}
|
|
321
|
+
if (target === null || !target.isGithub || target.owner === null || target.repo === null) {
|
|
322
|
+
log.error(`Not a valid GitHub URL or owner/repo slug. Examples: ${cyan("https://github.com/<owner>/<repo>")} or ${cyan("<owner>/<repo>")}`);
|
|
323
|
+
process.exitCode = 1;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
plan.owner = target.owner;
|
|
327
|
+
plan.repo = target.repo;
|
|
328
|
+
plan.remoteUrl = target.httpsUrl ?? target.url;
|
|
329
|
+
if (existingRemote === null) {
|
|
330
|
+
plan.remoteAdded = true;
|
|
331
|
+
}
|
|
332
|
+
else if (existingRemote.url !== plan.remoteUrl) {
|
|
333
|
+
let allow = false;
|
|
334
|
+
if (interactive) {
|
|
335
|
+
allow = await askYesNo({
|
|
336
|
+
message: `${ctx.remoteName} already exists (${existingRemote.url}). Update its URL to this one?`,
|
|
337
|
+
nonInteractive: false,
|
|
338
|
+
defaultValue: false,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (!allow) {
|
|
342
|
+
log.info(`${dim("·")} Keeping existing ${ctx.remoteName} — not setting a new URL.`);
|
|
343
|
+
plan.remoteUrl = existingRemote.url;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
plan.remoteSetUrl = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
plan.remoteUsed = true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// ── F. package.json fields preview ───────────────────────────────────────
|
|
354
|
+
if (plan.owner.length > 0 && plan.repo.length > 0) {
|
|
355
|
+
if (ctx.packageUpdate && pkg !== null) {
|
|
356
|
+
const patch = buildRepositoryFieldsPatch({ owner: plan.owner, repo: plan.repo });
|
|
357
|
+
const before = {
|
|
358
|
+
repositoryUrl: readRepositoryUrl(pkg.data),
|
|
359
|
+
homepage: readHomepage(pkg.data),
|
|
360
|
+
bugsUrl: readBugsUrl(pkg.data),
|
|
361
|
+
};
|
|
362
|
+
const fields = [
|
|
363
|
+
{ field: "repository.url", before: before.repositoryUrl, after: patch.repositoryUrl },
|
|
364
|
+
{ field: "homepage", before: before.homepage, after: patch.homepage },
|
|
365
|
+
{ field: "bugs.url", before: before.bugsUrl, after: patch.bugsUrl },
|
|
366
|
+
].filter((f) => f.before !== f.after);
|
|
367
|
+
let allow = true;
|
|
368
|
+
if (interactive && fields.length > 0) {
|
|
369
|
+
const anyExisting = fields.some((f) => f.before.length > 0);
|
|
370
|
+
if (anyExisting) {
|
|
371
|
+
allow = await askYesNo({
|
|
372
|
+
message: `Overwrite package.json repository / homepage / bugs? (${fields.length} field${fields.length === 1 ? "" : "s"} will change)`,
|
|
373
|
+
nonInteractive: false,
|
|
374
|
+
defaultValue: false,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
allow = await askYesNo({
|
|
379
|
+
message: `Fill package.json repository / homepage / bugs? (${fields.length} new field${fields.length === 1 ? "" : "s"})`,
|
|
380
|
+
nonInteractive: false,
|
|
381
|
+
defaultValue: true,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else if (!interactive && fields.length > 0) {
|
|
386
|
+
const anyExisting = fields.some((f) => f.before.length > 0);
|
|
387
|
+
// default in non-interactive: write new fields, but never silently overwrite existing ones unless --yes.
|
|
388
|
+
allow = !anyExisting || explicitYes;
|
|
389
|
+
}
|
|
390
|
+
if (allow && fields.length > 0) {
|
|
391
|
+
plan.packageWrites = patch;
|
|
392
|
+
plan.packageDiffs = fields;
|
|
393
|
+
}
|
|
394
|
+
else if (!allow && fields.length > 0) {
|
|
395
|
+
if (interactive) {
|
|
396
|
+
log.info(`${dim("·")} Skipping package.json update (user choice).`);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
log.info(`${dim("·")} Skipping package.json update (preserving existing values — pass ${cyan("--yes")} or run interactively to overwrite).`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else if (!ctx.packageUpdate) {
|
|
404
|
+
log.info(`${dim("·")} --no-package-update — package.json will not be modified.`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// ── render plan ──────────────────────────────────────────────────────────
|
|
408
|
+
log.blank();
|
|
409
|
+
log.info(bold("Plan"));
|
|
410
|
+
renderPlan(ctx, plan);
|
|
411
|
+
if (dryRun) {
|
|
412
|
+
log.blank();
|
|
413
|
+
log.info(dim("dry-run — no commands executed, no files written."));
|
|
414
|
+
log.info(bold("Next steps"));
|
|
415
|
+
log.info(` 1) Drop ${cyan("--dry-run")} to apply this plan (gh / git / config / package.json mutation).`);
|
|
416
|
+
log.info(` 2) After commit, run ${cyan(`git push -u ${ctx.remoteName} <branch>`)} manually — VibeOps does not auto-push.`);
|
|
417
|
+
log.info(` 3) Verify with ${cyan("vibeops github status")}.`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
// ── execute ──────────────────────────────────────────────────────────────
|
|
421
|
+
if (plan.repoCreated) {
|
|
422
|
+
log.step(`gh repo create ${plan.owner}/${plan.repo}`);
|
|
423
|
+
const res = await ghCreateRepo({
|
|
424
|
+
owner: plan.owner,
|
|
425
|
+
repo: plan.repo,
|
|
426
|
+
visibility: plan.visibility === "" ? "private" : plan.visibility,
|
|
427
|
+
source: cwd,
|
|
428
|
+
remote: ctx.remoteName,
|
|
429
|
+
description: plan.description.length > 0 ? plan.description : undefined,
|
|
430
|
+
});
|
|
431
|
+
if (!res.ok) {
|
|
432
|
+
log.error(`gh repo create failed (exit ${res.exitCode ?? "?"}).`);
|
|
433
|
+
if (typeof res.stderr === "string" && res.stderr.length > 0) {
|
|
434
|
+
log.info(dim(res.stderr.trim()));
|
|
435
|
+
}
|
|
436
|
+
process.exitCode = res.exitCode ?? 1;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
log.ok(`created ${plan.owner}/${plan.repo} ${dim(plan.visibility)}`);
|
|
440
|
+
}
|
|
441
|
+
else if (plan.remoteAdded && plan.remoteUrl !== null) {
|
|
442
|
+
log.step(`git remote add ${ctx.remoteName} ${plan.remoteUrl}`);
|
|
443
|
+
try {
|
|
444
|
+
await gitRemoteAdd(cwd, ctx.remoteName, plan.remoteUrl);
|
|
445
|
+
log.ok(`added remote ${ctx.remoteName}`);
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
log.error(`git remote add failed: ${err.message}`);
|
|
449
|
+
process.exitCode = 1;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else if (plan.remoteSetUrl && plan.remoteUrl !== null) {
|
|
454
|
+
log.step(`git remote set-url ${ctx.remoteName} ${plan.remoteUrl}`);
|
|
455
|
+
try {
|
|
456
|
+
await gitRemoteSetUrl(cwd, ctx.remoteName, plan.remoteUrl);
|
|
457
|
+
log.ok(`updated remote ${ctx.remoteName}`);
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
log.error(`git remote set-url failed: ${err.message}`);
|
|
461
|
+
process.exitCode = 1;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// ── .vibeops.json ───────────────────────────────────────────────────────
|
|
466
|
+
const githubPatch = {
|
|
467
|
+
enabled: true,
|
|
468
|
+
mode: "gh-cli",
|
|
469
|
+
owner: plan.owner,
|
|
470
|
+
repo: plan.repo,
|
|
471
|
+
remote: ctx.remoteName,
|
|
472
|
+
visibility: plan.visibility === "" ? guessVisibilityFromExistingConfig(baseConfig.github) : plan.visibility,
|
|
473
|
+
url: plan.remoteUrl ?? "",
|
|
474
|
+
};
|
|
475
|
+
const { merged, changed } = mergeGithubConfig(baseConfig, githubPatch);
|
|
476
|
+
plan.configChanged = changed;
|
|
477
|
+
if (changed) {
|
|
478
|
+
await writeConfig(cwd, merged);
|
|
479
|
+
log.ok(".vibeops.json updated (github section)");
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
log.info(dim(".vibeops.json unchanged"));
|
|
483
|
+
}
|
|
484
|
+
// ── package.json ────────────────────────────────────────────────────────
|
|
485
|
+
if (plan.packageWrites !== null && plan.packageDiffs.length > 0) {
|
|
486
|
+
const res = await updatePackageRepositoryFields({
|
|
487
|
+
cwd,
|
|
488
|
+
patch: { owner: plan.owner, repo: plan.repo },
|
|
489
|
+
dryRun: false,
|
|
490
|
+
});
|
|
491
|
+
if (!res.ok) {
|
|
492
|
+
log.warn(`package.json update failed: ${res.reason ?? "unknown"}`);
|
|
493
|
+
}
|
|
494
|
+
else if (res.written) {
|
|
495
|
+
log.ok(`package.json updated ${dim(`(${res.diffs.length} field${res.diffs.length === 1 ? "" : "s"})`)}`);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
log.info(dim(`package.json unchanged`));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
log.blank();
|
|
502
|
+
log.info(bold("Done"));
|
|
503
|
+
log.info(` · git remote ${ctx.remoteName} → ${cyan(plan.remoteUrl ?? "(unset)")}`);
|
|
504
|
+
log.info(` · Verify with ${cyan("vibeops github status")}.`);
|
|
505
|
+
log.info(` · VibeOps never auto-pushes. When ready, run ${cyan(`git push -u ${ctx.remoteName} <branch>`)} yourself.`);
|
|
506
|
+
}
|
|
507
|
+
function guessVisibilityFromExistingConfig(prev) {
|
|
508
|
+
return prev?.visibility ?? "";
|
|
509
|
+
}
|
|
510
|
+
async function pickVisibility() {
|
|
511
|
+
return await select({
|
|
512
|
+
message: "Repository visibility?",
|
|
513
|
+
choices: [
|
|
514
|
+
{ name: "private", value: "private" },
|
|
515
|
+
{ name: "public", value: "public" },
|
|
516
|
+
],
|
|
517
|
+
default: "private",
|
|
518
|
+
loop: false,
|
|
519
|
+
pageSize: 2,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
function renderPlan(ctx, plan) {
|
|
523
|
+
if (plan.repoCreated) {
|
|
524
|
+
log.info(` ${green("+")} gh repo create ${cyan(`${plan.owner}/${plan.repo}`)} ${dim(`--${plan.visibility === "" ? "private" : plan.visibility} --source=${ctx.cwd} --remote=${ctx.remoteName}`)}`);
|
|
525
|
+
if (plan.description.length > 0) {
|
|
526
|
+
log.info(` ${dim("--description")} ${plan.description}`);
|
|
527
|
+
}
|
|
528
|
+
log.info(` ${dim("→")} This command also registers the ${ctx.remoteName} remote.`);
|
|
529
|
+
}
|
|
530
|
+
if (plan.remoteAdded && !plan.repoCreated && plan.remoteUrl !== null) {
|
|
531
|
+
log.info(` ${green("+")} git remote add ${cyan(ctx.remoteName)} ${plan.remoteUrl}`);
|
|
532
|
+
}
|
|
533
|
+
if (plan.remoteSetUrl && plan.remoteUrl !== null) {
|
|
534
|
+
log.info(` ${green("~")} git remote set-url ${cyan(ctx.remoteName)} ${plan.remoteUrl}`);
|
|
535
|
+
}
|
|
536
|
+
if (plan.remoteUsed && !plan.remoteAdded && !plan.remoteSetUrl && !plan.repoCreated) {
|
|
537
|
+
log.info(` ${dim("·")} keep existing remote ${cyan(ctx.remoteName)} ${plan.remoteUrl ?? ""}`);
|
|
538
|
+
}
|
|
539
|
+
if (plan.packageWrites !== null && plan.packageDiffs.length > 0) {
|
|
540
|
+
log.info(` ${green("~")} package.json fields:`);
|
|
541
|
+
for (const f of plan.packageDiffs) {
|
|
542
|
+
log.info(` ${dim(f.field.padEnd(16, " "))} ${dim(f.before.length === 0 ? "(empty)" : f.before)} → ${cyan(f.after)}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else if (!ctx.packageUpdate) {
|
|
546
|
+
log.info(` ${dim("·")} package.json untouched ${dim("(--no-package-update)")}`);
|
|
547
|
+
}
|
|
548
|
+
log.info(` ${green("~")} .vibeops.json github:`);
|
|
549
|
+
log.info(` ${dim("owner".padEnd(16, " "))} ${cyan(plan.owner.length > 0 ? plan.owner : "(none)")}`);
|
|
550
|
+
log.info(` ${dim("repo".padEnd(16, " "))} ${cyan(plan.repo.length > 0 ? plan.repo : "(none)")}`);
|
|
551
|
+
log.info(` ${dim("remote".padEnd(16, " "))} ${cyan(ctx.remoteName)}`);
|
|
552
|
+
log.info(` ${dim("visibility".padEnd(16, " "))} ${cyan(plan.visibility.length > 0 ? plan.visibility : "(unknown)")}`);
|
|
553
|
+
log.info(` ${dim("url".padEnd(16, " "))} ${cyan(plan.remoteUrl ?? "(unset)")}`);
|
|
554
|
+
}
|