@byrde/cursor 0.1.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/README.md +116 -0
- package/assets/agents/architect.md +128 -0
- package/assets/agents/developer.md +84 -0
- package/assets/agents/planner.md +59 -0
- package/assets/agents/tester.md +77 -0
- package/assets/rules/global.mdc +180 -0
- package/assets/rules/init.mdc +26 -0
- package/assets/templates/backlog.md +18 -0
- package/assets/templates/design.md +25 -0
- package/assets/templates/overview.md +38 -0
- package/assets/templates/testability/README.md +102 -0
- package/dist/cli.js +1241 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +465 -0
- package/dist/index.js.map +1 -0
- package/dist/sync-workflow.js +237 -0
- package/dist/sync-workflow.js.map +1 -0
- package/package.json +53 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { readFileSync as readFileSync5, realpathSync } from "fs";
|
|
5
|
+
import path8 from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { Command, CommanderError } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/api/init.ts
|
|
10
|
+
import path7 from "path";
|
|
11
|
+
|
|
12
|
+
// src/domain/config.ts
|
|
13
|
+
var PROJECT_CONFIG_FILENAME = "workflow.json";
|
|
14
|
+
function createDefaultWorkflowModels() {
|
|
15
|
+
return {
|
|
16
|
+
planner: "gpt-5.4-high",
|
|
17
|
+
architect: "gpt-5.4-high",
|
|
18
|
+
developer: "composer-2-fast",
|
|
19
|
+
tester: "composer-2-fast"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function createDefaultProjectConfig() {
|
|
23
|
+
return {
|
|
24
|
+
backlog: {
|
|
25
|
+
provider: "file",
|
|
26
|
+
file: {
|
|
27
|
+
path: "docs/backlog.md"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
workflow: {
|
|
31
|
+
defaults: {
|
|
32
|
+
architectReview: "required",
|
|
33
|
+
testing: "required"
|
|
34
|
+
},
|
|
35
|
+
models: createDefaultWorkflowModels()
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
var DEFAULT_PROJECT_CONFIG = createDefaultProjectConfig();
|
|
40
|
+
function isNonEmptyString(v) {
|
|
41
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
42
|
+
}
|
|
43
|
+
function normalizeBacklog(raw, base) {
|
|
44
|
+
if (!raw || typeof raw !== "object") {
|
|
45
|
+
return base;
|
|
46
|
+
}
|
|
47
|
+
const b = raw;
|
|
48
|
+
const provider = b.provider === "github-issues" || b.provider === "file" ? b.provider : base.provider;
|
|
49
|
+
if (provider === "file") {
|
|
50
|
+
const fileRaw = b.file;
|
|
51
|
+
const pathFrom = fileRaw && typeof fileRaw === "object" && isNonEmptyString(fileRaw.path) ? String(fileRaw.path).trim() : base.file.path;
|
|
52
|
+
return { provider: "file", file: { path: pathFrom } };
|
|
53
|
+
}
|
|
54
|
+
const ghRaw = b["github-issues"];
|
|
55
|
+
const githubOptionalDefaults = {
|
|
56
|
+
priorityField: "Priority",
|
|
57
|
+
statusField: "Status",
|
|
58
|
+
mcpServerName: "github"
|
|
59
|
+
};
|
|
60
|
+
function parsePositiveInt(v) {
|
|
61
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) {
|
|
62
|
+
return Math.trunc(v);
|
|
63
|
+
}
|
|
64
|
+
if (isNonEmptyString(v)) {
|
|
65
|
+
const n = Number.parseInt(String(v).trim(), 10);
|
|
66
|
+
if (Number.isFinite(n) && n > 0) {
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
if (ghRaw && typeof ghRaw === "object") {
|
|
73
|
+
const g = ghRaw;
|
|
74
|
+
const repository = isNonEmptyString(g.repository) ? g.repository.trim() : void 0;
|
|
75
|
+
const projectNumber = parsePositiveInt(g.projectNumber);
|
|
76
|
+
if (repository !== void 0 && projectNumber !== void 0) {
|
|
77
|
+
const priorityField = isNonEmptyString(g.priorityField) ? g.priorityField.trim() : githubOptionalDefaults.priorityField;
|
|
78
|
+
const statusField = isNonEmptyString(g.statusField) ? g.statusField.trim() : githubOptionalDefaults.statusField;
|
|
79
|
+
const labelRaw = g.label;
|
|
80
|
+
const label = isNonEmptyString(labelRaw) ? labelRaw.trim() : void 0;
|
|
81
|
+
return {
|
|
82
|
+
provider: "github-issues",
|
|
83
|
+
"github-issues": {
|
|
84
|
+
repository,
|
|
85
|
+
projectNumber,
|
|
86
|
+
projectOwner: isNonEmptyString(g.projectOwner) ? g.projectOwner.trim() : void 0,
|
|
87
|
+
priorityField,
|
|
88
|
+
statusField,
|
|
89
|
+
...label !== void 0 ? { label } : {},
|
|
90
|
+
mcpServerName: isNonEmptyString(g.mcpServerName) ? g.mcpServerName.trim() : githubOptionalDefaults.mcpServerName
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return base;
|
|
96
|
+
}
|
|
97
|
+
function normalizeWorkflow(raw, base) {
|
|
98
|
+
const models = { ...base.models };
|
|
99
|
+
let architectReview = base.defaults.architectReview;
|
|
100
|
+
let testing = base.defaults.testing;
|
|
101
|
+
if (raw && typeof raw === "object") {
|
|
102
|
+
const w = raw;
|
|
103
|
+
const d = w.defaults;
|
|
104
|
+
if (d && typeof d === "object") {
|
|
105
|
+
const def = d;
|
|
106
|
+
if (def.architectReview === "required" || def.architectReview === "optional") {
|
|
107
|
+
architectReview = def.architectReview;
|
|
108
|
+
}
|
|
109
|
+
if (def.testing === "required" || def.testing === "optional") {
|
|
110
|
+
testing = def.testing;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const m = w.models;
|
|
114
|
+
if (m && typeof m === "object") {
|
|
115
|
+
const mo = m;
|
|
116
|
+
const pick = (key) => {
|
|
117
|
+
const v = mo[key];
|
|
118
|
+
return isNonEmptyString(v) ? v.trim() : base.models[key];
|
|
119
|
+
};
|
|
120
|
+
models.planner = pick("planner");
|
|
121
|
+
models.architect = pick("architect");
|
|
122
|
+
models.developer = pick("developer");
|
|
123
|
+
models.tester = pick("tester");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
defaults: { architectReview, testing },
|
|
128
|
+
models
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function normalizeProjectConfig(raw) {
|
|
132
|
+
const base = createDefaultProjectConfig();
|
|
133
|
+
if (!raw || typeof raw !== "object") {
|
|
134
|
+
return base;
|
|
135
|
+
}
|
|
136
|
+
const o = raw;
|
|
137
|
+
return {
|
|
138
|
+
backlog: normalizeBacklog(o.backlog, base.backlog),
|
|
139
|
+
workflow: normalizeWorkflow(o.workflow, base.workflow)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/domain/asset-manifest.ts
|
|
144
|
+
var DEFAULT_MANIFEST = [
|
|
145
|
+
{
|
|
146
|
+
source: "assets/agents/architect.md",
|
|
147
|
+
target: "agents/architect.md",
|
|
148
|
+
renderAgent: "architect"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
source: "assets/agents/developer.md",
|
|
152
|
+
target: "agents/developer.md",
|
|
153
|
+
renderAgent: "developer"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
source: "assets/agents/planner.md",
|
|
157
|
+
target: "agents/planner.md",
|
|
158
|
+
renderAgent: "planner"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
source: "assets/agents/tester.md",
|
|
162
|
+
target: "agents/tester.md",
|
|
163
|
+
renderAgent: "tester"
|
|
164
|
+
},
|
|
165
|
+
{ source: "assets/rules/global.mdc", target: "rules/global.mdc" },
|
|
166
|
+
{ source: "assets/rules/init.mdc", target: "rules/init.mdc" },
|
|
167
|
+
{ source: "assets/templates/backlog.md", target: "templates/backlog.md" },
|
|
168
|
+
{ source: "assets/templates/design.md", target: "templates/design.md" },
|
|
169
|
+
{ source: "assets/templates/overview.md", target: "templates/overview.md" },
|
|
170
|
+
{
|
|
171
|
+
source: "assets/templates/testability/README.md",
|
|
172
|
+
target: "templates/testability/README.md"
|
|
173
|
+
}
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
// src/infrastructure/file-installer.ts
|
|
177
|
+
import {
|
|
178
|
+
existsSync as existsSync2,
|
|
179
|
+
mkdirSync as mkdirSync2,
|
|
180
|
+
readFileSync as readFileSync2,
|
|
181
|
+
statSync,
|
|
182
|
+
unlinkSync as unlinkSync2,
|
|
183
|
+
writeFileSync as writeFileSync2
|
|
184
|
+
} from "fs";
|
|
185
|
+
import path2 from "path";
|
|
186
|
+
|
|
187
|
+
// src/domain/managed-state.ts
|
|
188
|
+
import { createHash } from "crypto";
|
|
189
|
+
function hashFileContents(data) {
|
|
190
|
+
const hex = createHash("sha256").update(data).digest("hex");
|
|
191
|
+
return `sha256:${hex}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/infrastructure/agent-template.ts
|
|
195
|
+
var AGENT_MODEL_PLACEHOLDER = "{{MODEL}}";
|
|
196
|
+
function renderAgentTemplate(templateUtf8, model) {
|
|
197
|
+
if (!templateUtf8.includes(AGENT_MODEL_PLACEHOLDER)) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Agent template is missing ${AGENT_MODEL_PLACEHOLDER} placeholder`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return templateUtf8.replaceAll(AGENT_MODEL_PLACEHOLDER, model);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/infrastructure/managed-state-store.ts
|
|
206
|
+
import {
|
|
207
|
+
existsSync,
|
|
208
|
+
mkdirSync,
|
|
209
|
+
readFileSync,
|
|
210
|
+
renameSync,
|
|
211
|
+
unlinkSync,
|
|
212
|
+
writeFileSync
|
|
213
|
+
} from "fs";
|
|
214
|
+
import path from "path";
|
|
215
|
+
var MANAGED_STATE_FILENAME = ".managed.json";
|
|
216
|
+
function managedStatePath(cursorDir) {
|
|
217
|
+
return path.join(cursorDir, MANAGED_STATE_FILENAME);
|
|
218
|
+
}
|
|
219
|
+
function isPlainRecord(value) {
|
|
220
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
221
|
+
}
|
|
222
|
+
function parseManagedState(raw) {
|
|
223
|
+
let parsed;
|
|
224
|
+
try {
|
|
225
|
+
parsed = JSON.parse(raw);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Invalid managed state JSON in ${MANAGED_STATE_FILENAME}: ${msg}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (!isPlainRecord(parsed)) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Invalid managed state: root must be a JSON object (${MANAGED_STATE_FILENAME})`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const version = parsed.version;
|
|
238
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Invalid managed state: missing or invalid "version" string (${MANAGED_STATE_FILENAME})`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const filesRaw = parsed.files;
|
|
244
|
+
if (!isPlainRecord(filesRaw)) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Invalid managed state: "files" must be an object (${MANAGED_STATE_FILENAME})`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const files = {};
|
|
250
|
+
for (const [target, rec] of Object.entries(filesRaw)) {
|
|
251
|
+
if (!isPlainRecord(rec)) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Invalid managed state: files["${target}"] must be an object (${MANAGED_STATE_FILENAME})`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const hash = rec.hash;
|
|
257
|
+
const ver = rec.version;
|
|
258
|
+
if (typeof hash !== "string" || hash.length === 0) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Invalid managed state: files["${target}"].hash must be a non-empty string (${MANAGED_STATE_FILENAME})`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (typeof ver !== "string" || ver.length === 0) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Invalid managed state: files["${target}"].version must be a non-empty string (${MANAGED_STATE_FILENAME})`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const installedAt2 = rec.installedAt;
|
|
269
|
+
files[target] = {
|
|
270
|
+
hash,
|
|
271
|
+
version: ver,
|
|
272
|
+
...typeof installedAt2 === "string" ? { installedAt: installedAt2 } : {}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const installedAt = parsed.installedAt;
|
|
276
|
+
return {
|
|
277
|
+
version,
|
|
278
|
+
...typeof installedAt === "string" ? { installedAt } : {},
|
|
279
|
+
files
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function loadManagedState(cursorDir) {
|
|
283
|
+
const p = managedStatePath(cursorDir);
|
|
284
|
+
if (!existsSync(p)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const raw = readFileSync(p, "utf8");
|
|
288
|
+
return parseManagedState(raw);
|
|
289
|
+
}
|
|
290
|
+
function saveManagedStateAtomic(cursorDir, state) {
|
|
291
|
+
mkdirSync(cursorDir, { recursive: true });
|
|
292
|
+
const finalPath = managedStatePath(cursorDir);
|
|
293
|
+
const tmpPath = `${finalPath}.${process.pid}.tmp`;
|
|
294
|
+
const payload = `${JSON.stringify(state, null, 2)}
|
|
295
|
+
`;
|
|
296
|
+
try {
|
|
297
|
+
writeFileSync(tmpPath, payload, "utf8");
|
|
298
|
+
renameSync(tmpPath, finalPath);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
try {
|
|
301
|
+
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
throw e;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/infrastructure/file-installer.ts
|
|
309
|
+
function resolveManifestSourceBytes(entry, packageRoot2, projectConfig) {
|
|
310
|
+
const absSource = path2.join(packageRoot2, entry.source);
|
|
311
|
+
const raw = readFileSync2(absSource);
|
|
312
|
+
if (!entry.renderAgent) {
|
|
313
|
+
return raw;
|
|
314
|
+
}
|
|
315
|
+
const utf8 = raw.toString("utf8");
|
|
316
|
+
const model = projectConfig.workflow.models[entry.renderAgent];
|
|
317
|
+
const rendered = renderAgentTemplate(utf8, model);
|
|
318
|
+
return Buffer.from(rendered, "utf8");
|
|
319
|
+
}
|
|
320
|
+
function nowRecord(hash, packageVersion2) {
|
|
321
|
+
return {
|
|
322
|
+
hash,
|
|
323
|
+
version: packageVersion2,
|
|
324
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function installAssets(manifest, packageRoot2, targetDir, options) {
|
|
328
|
+
const resolvedRoot = path2.resolve(packageRoot2);
|
|
329
|
+
const resolvedTargetBase = path2.resolve(targetDir);
|
|
330
|
+
const projectConfig = normalizeProjectConfig(
|
|
331
|
+
options.projectConfig ?? createDefaultProjectConfig()
|
|
332
|
+
);
|
|
333
|
+
const initialState = loadManagedState(resolvedTargetBase);
|
|
334
|
+
const workingFiles = {
|
|
335
|
+
...initialState?.files ?? {}
|
|
336
|
+
};
|
|
337
|
+
const manifestOutcomes = [];
|
|
338
|
+
const staleOutcomes = [];
|
|
339
|
+
const warnings = [];
|
|
340
|
+
const manifestTargets = new Set(manifest.map((e) => e.target));
|
|
341
|
+
for (const entry of manifest) {
|
|
342
|
+
const absSource = path2.join(resolvedRoot, entry.source);
|
|
343
|
+
const absTarget = path2.join(resolvedTargetBase, entry.target);
|
|
344
|
+
statSync(absSource);
|
|
345
|
+
const sourceBytes = resolveManifestSourceBytes(
|
|
346
|
+
entry,
|
|
347
|
+
resolvedRoot,
|
|
348
|
+
projectConfig
|
|
349
|
+
);
|
|
350
|
+
const sourceHash = hashFileContents(sourceBytes);
|
|
351
|
+
const record = workingFiles[entry.target];
|
|
352
|
+
if (!existsSync2(absTarget)) {
|
|
353
|
+
mkdirSync2(path2.dirname(absTarget), { recursive: true });
|
|
354
|
+
writeFileSync2(absTarget, sourceBytes);
|
|
355
|
+
const writtenHash = hashFileContents(readFileSync2(absTarget));
|
|
356
|
+
workingFiles[entry.target] = nowRecord(
|
|
357
|
+
writtenHash,
|
|
358
|
+
options.packageVersion
|
|
359
|
+
);
|
|
360
|
+
if (record) {
|
|
361
|
+
manifestOutcomes.push({
|
|
362
|
+
kind: "recreated",
|
|
363
|
+
target: entry.target,
|
|
364
|
+
source: entry.source,
|
|
365
|
+
absolutePath: absTarget,
|
|
366
|
+
hash: writtenHash
|
|
367
|
+
});
|
|
368
|
+
} else {
|
|
369
|
+
manifestOutcomes.push({
|
|
370
|
+
kind: "installed",
|
|
371
|
+
target: entry.target,
|
|
372
|
+
source: entry.source,
|
|
373
|
+
absolutePath: absTarget,
|
|
374
|
+
hash: writtenHash
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const diskBytes = readFileSync2(absTarget);
|
|
380
|
+
const diskHash = hashFileContents(diskBytes);
|
|
381
|
+
if (record) {
|
|
382
|
+
if (diskHash === record.hash) {
|
|
383
|
+
writeFileSync2(absTarget, sourceBytes);
|
|
384
|
+
const newHash2 = hashFileContents(readFileSync2(absTarget));
|
|
385
|
+
workingFiles[entry.target] = nowRecord(newHash2, options.packageVersion);
|
|
386
|
+
manifestOutcomes.push({
|
|
387
|
+
kind: "updated",
|
|
388
|
+
target: entry.target,
|
|
389
|
+
source: entry.source,
|
|
390
|
+
absolutePath: absTarget,
|
|
391
|
+
hash: newHash2,
|
|
392
|
+
forced: false
|
|
393
|
+
});
|
|
394
|
+
} else if (!options.force) {
|
|
395
|
+
const msg = `Skipped managed file (modified on disk): ${entry.target}`;
|
|
396
|
+
warnings.push(msg);
|
|
397
|
+
if (entry.renderAgent) {
|
|
398
|
+
const desiredHash = sourceHash;
|
|
399
|
+
if (desiredHash !== record.hash) {
|
|
400
|
+
warnings.push(
|
|
401
|
+
`Model or template update not applied to ${entry.target} because the file was modified locally. Update the \`model:\` line yourself, or re-run init with --force to replace managed files.`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
manifestOutcomes.push({
|
|
406
|
+
kind: "skipped",
|
|
407
|
+
target: entry.target,
|
|
408
|
+
source: entry.source,
|
|
409
|
+
absolutePath: absTarget,
|
|
410
|
+
reason: "tracked_hash_mismatch",
|
|
411
|
+
forced: false
|
|
412
|
+
});
|
|
413
|
+
} else {
|
|
414
|
+
writeFileSync2(absTarget, sourceBytes);
|
|
415
|
+
const newHash2 = hashFileContents(readFileSync2(absTarget));
|
|
416
|
+
workingFiles[entry.target] = nowRecord(newHash2, options.packageVersion);
|
|
417
|
+
warnings.push(
|
|
418
|
+
`Overwrote modified managed file (--force): ${entry.target}`
|
|
419
|
+
);
|
|
420
|
+
manifestOutcomes.push({
|
|
421
|
+
kind: "updated",
|
|
422
|
+
target: entry.target,
|
|
423
|
+
source: entry.source,
|
|
424
|
+
absolutePath: absTarget,
|
|
425
|
+
hash: newHash2,
|
|
426
|
+
forced: true
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (diskHash === sourceHash) {
|
|
432
|
+
workingFiles[entry.target] = nowRecord(diskHash, options.packageVersion);
|
|
433
|
+
manifestOutcomes.push({
|
|
434
|
+
kind: "adopted",
|
|
435
|
+
target: entry.target,
|
|
436
|
+
source: entry.source,
|
|
437
|
+
absolutePath: absTarget,
|
|
438
|
+
hash: diskHash
|
|
439
|
+
});
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (!options.force) {
|
|
443
|
+
const msg = `Skipped untracked file (differs from package): ${entry.target}`;
|
|
444
|
+
warnings.push(msg);
|
|
445
|
+
manifestOutcomes.push({
|
|
446
|
+
kind: "skipped",
|
|
447
|
+
target: entry.target,
|
|
448
|
+
source: entry.source,
|
|
449
|
+
absolutePath: absTarget,
|
|
450
|
+
reason: "untracked_bytes_mismatch",
|
|
451
|
+
forced: false
|
|
452
|
+
});
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
writeFileSync2(absTarget, sourceBytes);
|
|
456
|
+
const newHash = hashFileContents(readFileSync2(absTarget));
|
|
457
|
+
workingFiles[entry.target] = nowRecord(newHash, options.packageVersion);
|
|
458
|
+
warnings.push(
|
|
459
|
+
`Overwrote untracked file that differed from package (--force): ${entry.target}`
|
|
460
|
+
);
|
|
461
|
+
manifestOutcomes.push({
|
|
462
|
+
kind: "updated",
|
|
463
|
+
target: entry.target,
|
|
464
|
+
source: entry.source,
|
|
465
|
+
absolutePath: absTarget,
|
|
466
|
+
hash: newHash,
|
|
467
|
+
forced: true
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
const initialKeys = Object.keys(initialState?.files ?? {});
|
|
471
|
+
for (const target of initialKeys) {
|
|
472
|
+
if (manifestTargets.has(target)) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
const record = workingFiles[target];
|
|
476
|
+
if (!record) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const absStale = path2.join(resolvedTargetBase, target);
|
|
480
|
+
if (!existsSync2(absStale)) {
|
|
481
|
+
delete workingFiles[target];
|
|
482
|
+
staleOutcomes.push({ kind: "stale_record_dropped", target });
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const diskHash = hashFileContents(readFileSync2(absStale));
|
|
486
|
+
if (diskHash === record.hash) {
|
|
487
|
+
unlinkSync2(absStale);
|
|
488
|
+
delete workingFiles[target];
|
|
489
|
+
staleOutcomes.push({
|
|
490
|
+
kind: "stale_removed",
|
|
491
|
+
target,
|
|
492
|
+
absolutePath: absStale
|
|
493
|
+
});
|
|
494
|
+
} else {
|
|
495
|
+
const msg = `Stale managed file not in manifest; keeping local changes: ${target}`;
|
|
496
|
+
warnings.push(msg);
|
|
497
|
+
staleOutcomes.push({
|
|
498
|
+
kind: "stale_kept_warn",
|
|
499
|
+
target,
|
|
500
|
+
absolutePath: absStale,
|
|
501
|
+
reason: "content_mismatch"
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const finalState = {
|
|
506
|
+
version: options.packageVersion,
|
|
507
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
508
|
+
files: workingFiles
|
|
509
|
+
};
|
|
510
|
+
saveManagedStateAtomic(resolvedTargetBase, finalState);
|
|
511
|
+
return {
|
|
512
|
+
manifestOutcomes,
|
|
513
|
+
staleOutcomes,
|
|
514
|
+
warnings
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/infrastructure/install-output-validator.ts
|
|
519
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
520
|
+
import path3 from "path";
|
|
521
|
+
var PACKAGE_OWNED_KINDS = /* @__PURE__ */ new Set(["installed", "updated", "adopted", "recreated"]);
|
|
522
|
+
function displayPath(cwd, absolutePath) {
|
|
523
|
+
const rel = path3.relative(cwd, absolutePath);
|
|
524
|
+
return rel.split(path3.sep).join("/");
|
|
525
|
+
}
|
|
526
|
+
function failureReason(absolutePath) {
|
|
527
|
+
if (!existsSync3(absolutePath)) {
|
|
528
|
+
return "missing";
|
|
529
|
+
}
|
|
530
|
+
const st = statSync2(absolutePath);
|
|
531
|
+
if (!st.isFile()) {
|
|
532
|
+
return "not a regular file";
|
|
533
|
+
}
|
|
534
|
+
if (st.size === 0) {
|
|
535
|
+
return "empty file";
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
function validateInstalledPackageOutputs(cwd, outcomes) {
|
|
540
|
+
const resolvedCwd = path3.resolve(cwd);
|
|
541
|
+
const failures = [];
|
|
542
|
+
for (const o of outcomes) {
|
|
543
|
+
if (!PACKAGE_OWNED_KINDS.has(o.kind)) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const reason = failureReason(o.absolutePath);
|
|
547
|
+
if (reason !== null) {
|
|
548
|
+
failures.push(`${displayPath(resolvedCwd, o.absolutePath)} (${reason})`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (failures.length > 0) {
|
|
552
|
+
const msg = `Post-install validation failed:
|
|
553
|
+
${failures.map((f) => ` - ${f}`).join("\n")}`;
|
|
554
|
+
throw new Error(msg);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/infrastructure/doc-template-installer.ts
|
|
559
|
+
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync3 } from "fs";
|
|
560
|
+
import path4 from "path";
|
|
561
|
+
var BASE_TEMPLATE_DOC_ENTRIES = [
|
|
562
|
+
{ source: "overview.md", target: "docs/overview.md" },
|
|
563
|
+
{ source: "design.md", target: "docs/design.md" },
|
|
564
|
+
{ source: "testability/README.md", target: "docs/testability/README.md" }
|
|
565
|
+
];
|
|
566
|
+
function scaffoldTemplateDocs(packageRoot2, cwd, config) {
|
|
567
|
+
const created = [];
|
|
568
|
+
const skipped = [];
|
|
569
|
+
const entries = resolveTemplateEntries(config);
|
|
570
|
+
mkdirSync3(path4.join(cwd, "docs"), { recursive: true });
|
|
571
|
+
for (const entry of entries) {
|
|
572
|
+
const source = path4.join(packageRoot2, "assets", "templates", entry.source);
|
|
573
|
+
const target = path4.join(cwd, entry.target);
|
|
574
|
+
statSync3(source);
|
|
575
|
+
mkdirSync3(path4.dirname(target), { recursive: true });
|
|
576
|
+
if (existsSync4(target)) {
|
|
577
|
+
skipped.push(target);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
copyFileSync(source, target);
|
|
581
|
+
created.push(target);
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
created,
|
|
585
|
+
skipped
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function resolveTemplateEntries(config) {
|
|
589
|
+
const entries = [...BASE_TEMPLATE_DOC_ENTRIES];
|
|
590
|
+
if (!config || config.backlog.provider === "file") {
|
|
591
|
+
entries.splice(2, 0, {
|
|
592
|
+
source: "backlog.md",
|
|
593
|
+
target: config?.backlog.file?.path ?? "docs/backlog.md"
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return entries;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/infrastructure/project-config-store.ts
|
|
600
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
601
|
+
import path5 from "path";
|
|
602
|
+
var LEGACY_PROJECT_CONFIG_FILENAME = "byrde.json";
|
|
603
|
+
function projectConfigPath(cwd) {
|
|
604
|
+
return path5.join(cwd, ".cursor", PROJECT_CONFIG_FILENAME);
|
|
605
|
+
}
|
|
606
|
+
function loadProjectConfig(cwd) {
|
|
607
|
+
const configPath = projectConfigPath(cwd);
|
|
608
|
+
const legacyPath = path5.join(cwd, ".cursor", LEGACY_PROJECT_CONFIG_FILENAME);
|
|
609
|
+
if (existsSync5(configPath)) {
|
|
610
|
+
return normalizeProjectConfig(
|
|
611
|
+
JSON.parse(readFileSync3(configPath, "utf8"))
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if (existsSync5(legacyPath)) {
|
|
615
|
+
return normalizeProjectConfig(
|
|
616
|
+
JSON.parse(readFileSync3(legacyPath, "utf8"))
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
function ensureProjectConfig(cwd, config = DEFAULT_PROJECT_CONFIG) {
|
|
622
|
+
const configPath = projectConfigPath(cwd);
|
|
623
|
+
if (existsSync5(configPath)) {
|
|
624
|
+
return {
|
|
625
|
+
path: configPath,
|
|
626
|
+
created: false
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
mkdirSync4(path5.dirname(configPath), { recursive: true });
|
|
630
|
+
const normalized = normalizeProjectConfig(config);
|
|
631
|
+
writeFileSync3(configPath, `${JSON.stringify(normalized, null, 2)}
|
|
632
|
+
`, "utf8");
|
|
633
|
+
return {
|
|
634
|
+
path: configPath,
|
|
635
|
+
created: true
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function saveProjectConfig(cwd, config = DEFAULT_PROJECT_CONFIG) {
|
|
639
|
+
const configPath = projectConfigPath(cwd);
|
|
640
|
+
const created = !existsSync5(configPath);
|
|
641
|
+
mkdirSync4(path5.dirname(configPath), { recursive: true });
|
|
642
|
+
const normalized = normalizeProjectConfig(config);
|
|
643
|
+
writeFileSync3(configPath, `${JSON.stringify(normalized, null, 2)}
|
|
644
|
+
`, "utf8");
|
|
645
|
+
return {
|
|
646
|
+
path: configPath,
|
|
647
|
+
created
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/infrastructure/github-mcp-store.ts
|
|
652
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
653
|
+
import path6 from "path";
|
|
654
|
+
var MANAGED_GITHUB_SERVER_PREFIX = "cursor-workflow:";
|
|
655
|
+
function mcpConfigPath(cwd) {
|
|
656
|
+
return path6.join(cwd, ".cursor", "mcp.json");
|
|
657
|
+
}
|
|
658
|
+
function readExistingConfig(cwd) {
|
|
659
|
+
const filePath = mcpConfigPath(cwd);
|
|
660
|
+
if (!existsSync6(filePath)) {
|
|
661
|
+
return { mcpServers: {} };
|
|
662
|
+
}
|
|
663
|
+
const raw = JSON.parse(readFileSync4(filePath, "utf8"));
|
|
664
|
+
return {
|
|
665
|
+
mcpServers: raw.mcpServers ?? {}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function managedServerKey(serverName) {
|
|
669
|
+
return `${MANAGED_GITHUB_SERVER_PREFIX}${serverName}`;
|
|
670
|
+
}
|
|
671
|
+
function writeGitHubMcpServer(params) {
|
|
672
|
+
const filePath = mcpConfigPath(params.cwd);
|
|
673
|
+
const created = !existsSync6(filePath);
|
|
674
|
+
const existing = readExistingConfig(params.cwd);
|
|
675
|
+
const serverKey = managedServerKey(params.serverName);
|
|
676
|
+
const nextServers = {};
|
|
677
|
+
for (const [key, value] of Object.entries(existing.mcpServers)) {
|
|
678
|
+
if (!key.startsWith(MANAGED_GITHUB_SERVER_PREFIX)) {
|
|
679
|
+
nextServers[key] = value;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
nextServers[serverKey] = {
|
|
683
|
+
command: "npx",
|
|
684
|
+
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
685
|
+
env: {
|
|
686
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: params.token
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
mkdirSync5(path6.dirname(filePath), { recursive: true });
|
|
690
|
+
writeFileSync4(
|
|
691
|
+
filePath,
|
|
692
|
+
`${JSON.stringify({ mcpServers: nextServers }, null, 2)}
|
|
693
|
+
`,
|
|
694
|
+
"utf8"
|
|
695
|
+
);
|
|
696
|
+
return {
|
|
697
|
+
path: filePath,
|
|
698
|
+
created,
|
|
699
|
+
serverKey
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/api/init.ts
|
|
704
|
+
async function runInit(params, packageRoot2) {
|
|
705
|
+
const targetDir = path7.join(params.cwd, ".cursor");
|
|
706
|
+
const projectConfig = normalizeProjectConfig(
|
|
707
|
+
params.projectConfig ?? DEFAULT_PROJECT_CONFIG
|
|
708
|
+
);
|
|
709
|
+
const result = installAssets(DEFAULT_MANIFEST, packageRoot2, targetDir, {
|
|
710
|
+
force: params.force,
|
|
711
|
+
packageVersion: params.packageVersion,
|
|
712
|
+
projectConfig
|
|
713
|
+
});
|
|
714
|
+
const warnings = [...result.warnings];
|
|
715
|
+
validateInstalledPackageOutputs(params.cwd, result.manifestOutcomes);
|
|
716
|
+
const configResult = params.overwriteProjectConfig ? saveProjectConfig(params.cwd, projectConfig) : ensureProjectConfig(params.cwd, projectConfig);
|
|
717
|
+
const docsResult = scaffoldTemplateDocs(packageRoot2, params.cwd, projectConfig);
|
|
718
|
+
const shouldConfigureGitHubMcp = projectConfig.backlog.provider === "github-issues";
|
|
719
|
+
let mcpLabel;
|
|
720
|
+
if (shouldConfigureGitHubMcp && !params.skipMcp) {
|
|
721
|
+
const token = params.githubMcpToken;
|
|
722
|
+
if (!token) {
|
|
723
|
+
warnings.push(
|
|
724
|
+
"GitHub backlog selected, but no GitHub token was provided for Cursor MCP setup. Re-run `init` or edit `.cursor/mcp.json` manually."
|
|
725
|
+
);
|
|
726
|
+
} else {
|
|
727
|
+
const mcpResult = writeGitHubMcpServer({
|
|
728
|
+
cwd: params.cwd,
|
|
729
|
+
serverName: projectConfig.backlog["github-issues"]?.mcpServerName ?? "github",
|
|
730
|
+
token
|
|
731
|
+
});
|
|
732
|
+
mcpLabel = `${mcpResult.created ? "GitHub MCP config created" : "GitHub MCP config updated"}: ${mcpResult.path}`;
|
|
733
|
+
}
|
|
734
|
+
} else if (shouldConfigureGitHubMcp) {
|
|
735
|
+
mcpLabel = "Skipped GitHub MCP setup (--skip-mcp).";
|
|
736
|
+
} else {
|
|
737
|
+
mcpLabel = "No GitHub MCP setup needed for the selected backlog.";
|
|
738
|
+
}
|
|
739
|
+
console.log("Project scaffolded.");
|
|
740
|
+
for (const w of warnings) {
|
|
741
|
+
console.warn(w);
|
|
742
|
+
}
|
|
743
|
+
if (params.verbose) {
|
|
744
|
+
const configLabel = params.overwriteProjectConfig ? configResult.created ? "Project config created" : "Project config updated" : configResult.created ? "Project config created" : "Project config preserved";
|
|
745
|
+
console.log(`${configLabel}: ${configResult.path}`);
|
|
746
|
+
console.log(
|
|
747
|
+
`Docs scaffolded: ${docsResult.created.length} created, ${docsResult.skipped.length} preserved.`
|
|
748
|
+
);
|
|
749
|
+
console.log(mcpLabel);
|
|
750
|
+
if (configResult.created) {
|
|
751
|
+
console.log(`config created: ${configResult.path}`);
|
|
752
|
+
} else {
|
|
753
|
+
console.log(`config preserved: ${configResult.path}`);
|
|
754
|
+
}
|
|
755
|
+
for (const created of docsResult.created) {
|
|
756
|
+
console.log(`doc created: ${created}`);
|
|
757
|
+
}
|
|
758
|
+
for (const skipped of docsResult.skipped) {
|
|
759
|
+
console.log(`doc preserved: ${skipped}`);
|
|
760
|
+
}
|
|
761
|
+
for (const o of result.manifestOutcomes) {
|
|
762
|
+
const dest = `${o.target} -> ${o.absolutePath}`;
|
|
763
|
+
if (o.kind === "skipped") {
|
|
764
|
+
console.log(`skipped (${o.reason}): ${dest}`);
|
|
765
|
+
} else {
|
|
766
|
+
console.log(`${o.kind}: ${dest}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
for (const s of result.staleOutcomes) {
|
|
770
|
+
if (s.kind === "stale_removed") {
|
|
771
|
+
console.log(`stale removed: ${s.target} (${s.absolutePath})`);
|
|
772
|
+
} else if (s.kind === "stale_record_dropped") {
|
|
773
|
+
console.log(`stale record dropped (file missing): ${s.target}`);
|
|
774
|
+
} else {
|
|
775
|
+
console.log(
|
|
776
|
+
`stale kept (${s.reason}): ${s.target} (${s.absolutePath})`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
...result,
|
|
783
|
+
warnings
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/infrastructure/init-questionnaire.ts
|
|
788
|
+
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
789
|
+
import { styleText } from "util";
|
|
790
|
+
|
|
791
|
+
// src/infrastructure/github-auth.ts
|
|
792
|
+
import { execFile, execFileSync } from "child_process";
|
|
793
|
+
function execFileResult(command, args) {
|
|
794
|
+
return new Promise((resolve) => {
|
|
795
|
+
execFile(command, args, (error, stdout, stderr) => {
|
|
796
|
+
const exitCode = error && typeof error === "object" && "code" in error ? error.code : error ? 1 : 0;
|
|
797
|
+
resolve({
|
|
798
|
+
stdout: String(stdout ?? ""),
|
|
799
|
+
stderr: String(stderr ?? ""),
|
|
800
|
+
exitCode
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
function parseGitHubAccounts(output) {
|
|
806
|
+
const accounts = [];
|
|
807
|
+
const lines = output.split("\n");
|
|
808
|
+
for (let i = 0; i < lines.length; i++) {
|
|
809
|
+
const line = lines[i];
|
|
810
|
+
const accountMatch = line.match(/Logged in to (\S+) account (\S+)/);
|
|
811
|
+
if (!accountMatch) {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const host = accountMatch[1];
|
|
815
|
+
const account = accountMatch[2];
|
|
816
|
+
let active = line.includes("\u2713");
|
|
817
|
+
for (let j = i + 1; j < Math.min(i + 8, lines.length); j++) {
|
|
818
|
+
const next = lines[j];
|
|
819
|
+
if (/Logged in to \S+ account \S+/.test(next)) {
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
if (/Active account:\s*true\b/.test(next)) {
|
|
823
|
+
active = true;
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
if (/Active account:\s*false\b/.test(next)) {
|
|
827
|
+
active = false;
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
accounts.push({ host, account, active });
|
|
832
|
+
}
|
|
833
|
+
return accounts;
|
|
834
|
+
}
|
|
835
|
+
async function listGitHubAccounts() {
|
|
836
|
+
let result;
|
|
837
|
+
try {
|
|
838
|
+
result = await execFileResult("gh", ["auth", "status"]);
|
|
839
|
+
} catch {
|
|
840
|
+
return [];
|
|
841
|
+
}
|
|
842
|
+
if (result.exitCode !== 0) {
|
|
843
|
+
return [];
|
|
844
|
+
}
|
|
845
|
+
const combined = `${result.stdout}
|
|
846
|
+
${result.stderr}`;
|
|
847
|
+
return parseGitHubAccounts(combined);
|
|
848
|
+
}
|
|
849
|
+
async function resolveGitHubTokenForAccount(account) {
|
|
850
|
+
const args = ["auth", "token"];
|
|
851
|
+
if (account) {
|
|
852
|
+
args.push("--user", account);
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
const result = await execFileResult("gh", args);
|
|
856
|
+
const token = result.stdout.trim();
|
|
857
|
+
if (result.exitCode !== 0 || !token) {
|
|
858
|
+
return void 0;
|
|
859
|
+
}
|
|
860
|
+
return token;
|
|
861
|
+
} catch {
|
|
862
|
+
return void 0;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/infrastructure/init-questionnaire.ts
|
|
867
|
+
var BOLD = "\x1B[1m";
|
|
868
|
+
var DIM = "\x1B[2m";
|
|
869
|
+
var RESET = "\x1B[0m";
|
|
870
|
+
var DEFAULT_PROMPTS = {
|
|
871
|
+
select,
|
|
872
|
+
input,
|
|
873
|
+
confirm,
|
|
874
|
+
password
|
|
875
|
+
};
|
|
876
|
+
async function runInitQuestionnaire(options, prompts = DEFAULT_PROMPTS) {
|
|
877
|
+
printBanner();
|
|
878
|
+
const existingConfig = options.existingConfig;
|
|
879
|
+
if (existingConfig) {
|
|
880
|
+
const reconfigure = await prompts.confirm({
|
|
881
|
+
message: "Reconfigure workflow setup (.cursor/workflow.json)?",
|
|
882
|
+
default: false
|
|
883
|
+
});
|
|
884
|
+
if (!reconfigure) {
|
|
885
|
+
return {
|
|
886
|
+
projectConfig: normalizeProjectConfig(existingConfig),
|
|
887
|
+
shouldWriteProjectConfig: false
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const projectConfig = await promptProjectConfig(
|
|
892
|
+
existingConfig ?? createDefaultProjectConfig(),
|
|
893
|
+
prompts
|
|
894
|
+
);
|
|
895
|
+
const githubMcpToken = projectConfig.backlog.provider === "github-issues" ? await promptGitHubMcpToken(prompts) : void 0;
|
|
896
|
+
return {
|
|
897
|
+
projectConfig,
|
|
898
|
+
shouldWriteProjectConfig: true,
|
|
899
|
+
githubMcpToken
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
async function promptProjectConfig(defaults, prompts) {
|
|
903
|
+
const provider = await prompts.select({
|
|
904
|
+
message: "Which backlog style should this project use?",
|
|
905
|
+
choices: [
|
|
906
|
+
{
|
|
907
|
+
name: "Markdown file in the repository",
|
|
908
|
+
description: "Track work in a markdown backlog file that the workflow updates directly.",
|
|
909
|
+
value: "file"
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
name: "GitHub Project (v2) + issues",
|
|
913
|
+
description: "Track work in a GitHub Project (v2): milestones = epics, Project Priority orders work, Project Status maps to workflow state. Uses GitHub MCP instead of a local backlog file.",
|
|
914
|
+
value: "github-issues"
|
|
915
|
+
}
|
|
916
|
+
],
|
|
917
|
+
default: defaults.backlog.provider
|
|
918
|
+
});
|
|
919
|
+
const backlog = await promptBacklogConfig(provider, defaults, prompts);
|
|
920
|
+
const architectReview = await promptDefaultMode(
|
|
921
|
+
"Default architect review behavior?",
|
|
922
|
+
defaults.workflow.defaults.architectReview,
|
|
923
|
+
prompts,
|
|
924
|
+
{
|
|
925
|
+
required: "Require a second architect review by default",
|
|
926
|
+
optional: "Let the orchestrator skip architect review by default when the task is clearly small and low-risk"
|
|
927
|
+
}
|
|
928
|
+
);
|
|
929
|
+
const testing = await promptDefaultMode(
|
|
930
|
+
"Default adversarial testing behavior?",
|
|
931
|
+
defaults.workflow.defaults.testing,
|
|
932
|
+
prompts,
|
|
933
|
+
{
|
|
934
|
+
required: "Require `/tester` by default",
|
|
935
|
+
optional: "Let the orchestrator skip `/tester` by default when developer verification is sufficient"
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
const models = await promptWorkflowModels(defaults.workflow.models, prompts);
|
|
939
|
+
return {
|
|
940
|
+
backlog,
|
|
941
|
+
workflow: {
|
|
942
|
+
defaults: {
|
|
943
|
+
architectReview,
|
|
944
|
+
testing
|
|
945
|
+
},
|
|
946
|
+
models
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
async function promptWorkflowModels(defaults, prompts) {
|
|
951
|
+
const useRecommended = await prompts.confirm({
|
|
952
|
+
message: "Use recommended Cursor models for /planner, /architect, /developer, and /tester?",
|
|
953
|
+
default: true
|
|
954
|
+
});
|
|
955
|
+
if (useRecommended) {
|
|
956
|
+
return defaults;
|
|
957
|
+
}
|
|
958
|
+
const planner = (await prompts.input({
|
|
959
|
+
message: "Model id for /planner:",
|
|
960
|
+
default: defaults.planner,
|
|
961
|
+
validate: (value) => value.trim() ? true : "Enter a model id (see Cursor docs)."
|
|
962
|
+
})).trim();
|
|
963
|
+
const architect = (await prompts.input({
|
|
964
|
+
message: "Model id for /architect:",
|
|
965
|
+
default: defaults.architect,
|
|
966
|
+
validate: (value) => value.trim() ? true : "Enter a model id (see Cursor docs)."
|
|
967
|
+
})).trim();
|
|
968
|
+
const developer = (await prompts.input({
|
|
969
|
+
message: "Model id for /developer:",
|
|
970
|
+
default: defaults.developer,
|
|
971
|
+
validate: (value) => value.trim() ? true : "Enter a model id (see Cursor docs)."
|
|
972
|
+
})).trim();
|
|
973
|
+
const tester = (await prompts.input({
|
|
974
|
+
message: "Model id for /tester:",
|
|
975
|
+
default: defaults.tester,
|
|
976
|
+
validate: (value) => value.trim() ? true : "Enter a model id (see Cursor docs)."
|
|
977
|
+
})).trim();
|
|
978
|
+
return { planner, architect, developer, tester };
|
|
979
|
+
}
|
|
980
|
+
async function promptBacklogConfig(provider, defaults, prompts) {
|
|
981
|
+
if (provider === "github-issues") {
|
|
982
|
+
const githubDefaults = defaults.backlog["github-issues"];
|
|
983
|
+
const repository = (await prompts.input({
|
|
984
|
+
message: "GitHub repository for issues (owner/name):",
|
|
985
|
+
default: githubDefaults?.repository ?? "",
|
|
986
|
+
validate: (value) => value.trim().match(/^[^/\s]+\/[^/\s]+$/) ? true : "Use the form owner/name."
|
|
987
|
+
})).trim();
|
|
988
|
+
const projectNumberStr = await prompts.input({
|
|
989
|
+
message: "GitHub Project (v2) number:",
|
|
990
|
+
default: githubDefaults?.projectNumber != null ? String(githubDefaults.projectNumber) : "",
|
|
991
|
+
validate: (value) => {
|
|
992
|
+
const n = Number.parseInt(value.trim(), 10);
|
|
993
|
+
return Number.isFinite(n) && n > 0 ? true : "Enter a positive Project number (from the Projects tab).";
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
const projectNumber = Number.parseInt(projectNumberStr.trim(), 10);
|
|
997
|
+
const projectOwner = (await prompts.input({
|
|
998
|
+
message: "Project owner (org or user login; leave blank to use the repository owner):",
|
|
999
|
+
default: githubDefaults?.projectOwner ?? ""
|
|
1000
|
+
})).trim();
|
|
1001
|
+
const priorityField = (await prompts.input({
|
|
1002
|
+
message: "Project field name for backlog ordering:",
|
|
1003
|
+
default: githubDefaults?.priorityField ?? "Priority",
|
|
1004
|
+
validate: (value) => value.trim() ? true : "Field name is required."
|
|
1005
|
+
})).trim();
|
|
1006
|
+
const statusField = (await prompts.input({
|
|
1007
|
+
message: "Project field name for workflow status:",
|
|
1008
|
+
default: githubDefaults?.statusField ?? "Status",
|
|
1009
|
+
validate: (value) => value.trim() ? true : "Field name is required."
|
|
1010
|
+
})).trim();
|
|
1011
|
+
const labelRaw = (await prompts.input({
|
|
1012
|
+
message: "Optional issue label filter (leave blank for none; secondary to Project items):",
|
|
1013
|
+
default: githubDefaults?.label ?? ""
|
|
1014
|
+
})).trim();
|
|
1015
|
+
return {
|
|
1016
|
+
provider,
|
|
1017
|
+
"github-issues": {
|
|
1018
|
+
repository,
|
|
1019
|
+
projectNumber,
|
|
1020
|
+
...projectOwner.trim().length > 0 ? { projectOwner: projectOwner.trim() } : {},
|
|
1021
|
+
priorityField,
|
|
1022
|
+
statusField,
|
|
1023
|
+
...labelRaw.trim().length > 0 ? { label: labelRaw.trim() } : {},
|
|
1024
|
+
mcpServerName: githubDefaults?.mcpServerName ?? "github"
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
const fileDefaults = defaults.backlog.file;
|
|
1029
|
+
const backlogPath = (await prompts.input({
|
|
1030
|
+
message: "Backlog file path:",
|
|
1031
|
+
default: fileDefaults?.path ?? "docs/backlog.md",
|
|
1032
|
+
validate: (value) => value.trim() ? true : "Path is required."
|
|
1033
|
+
})).trim();
|
|
1034
|
+
return {
|
|
1035
|
+
provider,
|
|
1036
|
+
file: {
|
|
1037
|
+
path: backlogPath
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
var GITHUB_MCP_AUTH_QUESTION = "How should Cursor authenticate to GitHub for the MCP server?";
|
|
1042
|
+
var GITHUB_MCP_TOKEN_PROMPT = "GitHub personal access token for the Cursor MCP server:";
|
|
1043
|
+
function formatGitHubMcpContextBlock() {
|
|
1044
|
+
return `${BOLD}GitHub MCP${RESET}
|
|
1045
|
+
${DIM}The GitHub MCP server needs a token so Cursor can reach your repository, Project (v2), and issues.${RESET}`;
|
|
1046
|
+
}
|
|
1047
|
+
function githubMcpAuthSelectMessage() {
|
|
1048
|
+
return `${formatGitHubMcpContextBlock()}
|
|
1049
|
+
|
|
1050
|
+
${GITHUB_MCP_AUTH_QUESTION}`;
|
|
1051
|
+
}
|
|
1052
|
+
function githubMcpAuthSelectTheme() {
|
|
1053
|
+
return {
|
|
1054
|
+
style: {
|
|
1055
|
+
message: (text, status) => {
|
|
1056
|
+
if (status === "done") {
|
|
1057
|
+
return styleText("bold", GITHUB_MCP_AUTH_QUESTION);
|
|
1058
|
+
}
|
|
1059
|
+
return text;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function githubMcpPasswordMessageNoGhAccounts() {
|
|
1065
|
+
return `${formatGitHubMcpContextBlock()}
|
|
1066
|
+
|
|
1067
|
+
${DIM}No GitHub CLI accounts were found (install \`gh\` and run \`gh auth login\` to sign in).${RESET}
|
|
1068
|
+
${DIM}You can paste a fine-grained or classic PAT with repo scope for the MCP GitHub server.${RESET}
|
|
1069
|
+
|
|
1070
|
+
${GITHUB_MCP_TOKEN_PROMPT}`;
|
|
1071
|
+
}
|
|
1072
|
+
function githubMcpPasswordTheme() {
|
|
1073
|
+
return {
|
|
1074
|
+
style: {
|
|
1075
|
+
message: (text, status) => {
|
|
1076
|
+
if (status === "done") {
|
|
1077
|
+
return styleText("bold", GITHUB_MCP_TOKEN_PROMPT);
|
|
1078
|
+
}
|
|
1079
|
+
return text;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function maskTokenHint(token) {
|
|
1085
|
+
const t = token.trim();
|
|
1086
|
+
if (t.length <= 8) {
|
|
1087
|
+
return "\u2026";
|
|
1088
|
+
}
|
|
1089
|
+
return `${t.slice(0, 4)}\u2026${t.slice(-4)}`;
|
|
1090
|
+
}
|
|
1091
|
+
function formatAccountChoice(a) {
|
|
1092
|
+
const active = a.active ? " \u2014 active" : "";
|
|
1093
|
+
return `${a.account} @ ${a.host}${active}`;
|
|
1094
|
+
}
|
|
1095
|
+
async function promptGitHubMcpToken(prompts) {
|
|
1096
|
+
const envToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN?.trim();
|
|
1097
|
+
const accounts = await listGitHubAccounts();
|
|
1098
|
+
const choices = [];
|
|
1099
|
+
if (envToken) {
|
|
1100
|
+
choices.push({
|
|
1101
|
+
name: `Use GITHUB_PERSONAL_ACCESS_TOKEN from the environment (${maskTokenHint(envToken)})`,
|
|
1102
|
+
value: { kind: "env" }
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
for (const account of accounts) {
|
|
1106
|
+
choices.push({
|
|
1107
|
+
name: formatAccountChoice(account),
|
|
1108
|
+
value: { kind: "gh-account", account: account.account }
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
choices.push({
|
|
1112
|
+
name: "Enter a personal access token manually",
|
|
1113
|
+
value: { kind: "manual" }
|
|
1114
|
+
});
|
|
1115
|
+
if (accounts.length === 0 && !envToken) {
|
|
1116
|
+
return (await prompts.password({
|
|
1117
|
+
message: githubMcpPasswordMessageNoGhAccounts(),
|
|
1118
|
+
theme: githubMcpPasswordTheme(),
|
|
1119
|
+
validate: (value) => value.trim() ? true : "GitHub token is required."
|
|
1120
|
+
})).trim();
|
|
1121
|
+
}
|
|
1122
|
+
const selected = await prompts.select({
|
|
1123
|
+
message: githubMcpAuthSelectMessage(),
|
|
1124
|
+
choices,
|
|
1125
|
+
theme: githubMcpAuthSelectTheme()
|
|
1126
|
+
});
|
|
1127
|
+
if (selected.kind === "env") {
|
|
1128
|
+
return envToken;
|
|
1129
|
+
}
|
|
1130
|
+
if (selected.kind === "manual") {
|
|
1131
|
+
return (await prompts.password({
|
|
1132
|
+
message: GITHUB_MCP_TOKEN_PROMPT,
|
|
1133
|
+
validate: (value) => value.trim() ? true : "GitHub token is required."
|
|
1134
|
+
})).trim();
|
|
1135
|
+
}
|
|
1136
|
+
const resolved = await resolveGitHubTokenForAccount(selected.account);
|
|
1137
|
+
if (resolved) {
|
|
1138
|
+
return resolved;
|
|
1139
|
+
}
|
|
1140
|
+
console.warn(
|
|
1141
|
+
`${BOLD}Could not read a token for that account via \`gh auth token\`.${RESET} ${DIM}Sign in with \`gh auth login\` or enter a token below.${RESET}`
|
|
1142
|
+
);
|
|
1143
|
+
return (await prompts.password({
|
|
1144
|
+
message: GITHUB_MCP_TOKEN_PROMPT,
|
|
1145
|
+
validate: (value) => value.trim() ? true : "GitHub token is required."
|
|
1146
|
+
})).trim();
|
|
1147
|
+
}
|
|
1148
|
+
async function promptDefaultMode(message, defaultValue, prompts, labels) {
|
|
1149
|
+
return prompts.select({
|
|
1150
|
+
message,
|
|
1151
|
+
choices: [
|
|
1152
|
+
{ name: labels.required, value: "required" },
|
|
1153
|
+
{ name: labels.optional, value: "optional" }
|
|
1154
|
+
],
|
|
1155
|
+
default: defaultValue
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
function printBanner() {
|
|
1159
|
+
console.log(`
|
|
1160
|
+
${BOLD}@byrde/cursor${RESET}
|
|
1161
|
+
${DIM}Install and configure the workflow so the project starts with the
|
|
1162
|
+
right backlog, review, testing, and GitHub tooling from day one.${RESET}
|
|
1163
|
+
`);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/cli.ts
|
|
1167
|
+
var packageRoot = path8.resolve(
|
|
1168
|
+
path8.dirname(fileURLToPath(import.meta.url)),
|
|
1169
|
+
".."
|
|
1170
|
+
);
|
|
1171
|
+
function readPackageVersion() {
|
|
1172
|
+
const pkgPath = path8.join(packageRoot, "package.json");
|
|
1173
|
+
const raw = readFileSync5(pkgPath, "utf8");
|
|
1174
|
+
const pkg = JSON.parse(raw);
|
|
1175
|
+
return pkg.version;
|
|
1176
|
+
}
|
|
1177
|
+
var packageVersion = readPackageVersion();
|
|
1178
|
+
function createProgram(deps = {}) {
|
|
1179
|
+
const runInitCommand = deps.runInit ?? runInit;
|
|
1180
|
+
const runQuestionnaire = deps.runInitQuestionnaire ?? runInitQuestionnaire;
|
|
1181
|
+
const program = new Command();
|
|
1182
|
+
program.name("byrde-cursor").description("Byrde Cursor workflow CLI and assets").version(packageVersion);
|
|
1183
|
+
program.command("init").description("Interactively configure and install the workflow into the target project").option(
|
|
1184
|
+
"--cwd <path>",
|
|
1185
|
+
"target working directory",
|
|
1186
|
+
process.cwd()
|
|
1187
|
+
).option("--skip-mcp", "skip GitHub MCP setup", false).option("--force", "overwrite managed files when needed", false).option("--verbose", "print extra diagnostics", false).action(async (options) => {
|
|
1188
|
+
const questionnaire = await runQuestionnaire({
|
|
1189
|
+
cwd: options.cwd,
|
|
1190
|
+
existingConfig: loadProjectConfig(options.cwd)
|
|
1191
|
+
});
|
|
1192
|
+
await runInitCommand(
|
|
1193
|
+
{
|
|
1194
|
+
cwd: options.cwd,
|
|
1195
|
+
skipMcp: options.skipMcp,
|
|
1196
|
+
verbose: options.verbose,
|
|
1197
|
+
force: options.force,
|
|
1198
|
+
packageVersion,
|
|
1199
|
+
projectConfig: questionnaire.projectConfig,
|
|
1200
|
+
overwriteProjectConfig: questionnaire.shouldWriteProjectConfig,
|
|
1201
|
+
githubMcpToken: questionnaire.githubMcpToken
|
|
1202
|
+
},
|
|
1203
|
+
packageRoot
|
|
1204
|
+
);
|
|
1205
|
+
});
|
|
1206
|
+
return program;
|
|
1207
|
+
}
|
|
1208
|
+
function isExecutedAsCli() {
|
|
1209
|
+
const entry = process.argv[1];
|
|
1210
|
+
if (!entry) return false;
|
|
1211
|
+
try {
|
|
1212
|
+
const modulePath = realpathSync(fileURLToPath(import.meta.url));
|
|
1213
|
+
const argvPath = realpathSync(path8.resolve(entry));
|
|
1214
|
+
return modulePath === argvPath;
|
|
1215
|
+
} catch {
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
function reportCliFailure(err) {
|
|
1220
|
+
if (err instanceof CommanderError) {
|
|
1221
|
+
const code = err.exitCode ?? 1;
|
|
1222
|
+
if (code !== 0) {
|
|
1223
|
+
console.error(err.message);
|
|
1224
|
+
}
|
|
1225
|
+
process.exit(code);
|
|
1226
|
+
} else if (err instanceof Error) {
|
|
1227
|
+
console.error(err.message);
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
} else {
|
|
1230
|
+
console.error(String(err));
|
|
1231
|
+
process.exit(1);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (isExecutedAsCli()) {
|
|
1235
|
+
void createProgram().parseAsync(process.argv).catch(reportCliFailure);
|
|
1236
|
+
}
|
|
1237
|
+
export {
|
|
1238
|
+
createProgram,
|
|
1239
|
+
reportCliFailure
|
|
1240
|
+
};
|
|
1241
|
+
//# sourceMappingURL=cli.js.map
|