@dadado/agent-kit-cli 4.2.2

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/dist/index.js ADDED
@@ -0,0 +1,3119 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { defineCommand as defineCommand11, runMain } from "citty";
5
+
6
+ // src/commands/add.ts
7
+ import { defineCommand } from "citty";
8
+
9
+ // src/lifecycle/apply.ts
10
+ import { copyFile, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
11
+ import path3 from "path";
12
+
13
+ // src/manifest/types.ts
14
+ var MANIFEST_FILENAME = "agent-kit.json";
15
+ var MANIFEST_RELATIVE_PATH = `.cursor/${MANIFEST_FILENAME}`;
16
+ var MANIFEST_SCHEMA_VERSION = 1;
17
+ var DEFAULT_PROTECTED_PATHS = [
18
+ ".cursor/HANDOFF.md",
19
+ ".cursor/plans/**",
20
+ ".cursor/memory/**",
21
+ ".cursor/context/**"
22
+ ];
23
+ var DOMAIN_PACK_IDS = [
24
+ "cybersec",
25
+ "devops",
26
+ "engineering-architecture",
27
+ "clean-code",
28
+ "project-management",
29
+ "context-management",
30
+ "quality"
31
+ ];
32
+
33
+ // src/utils/fs.ts
34
+ import { constants } from "fs";
35
+ import { access, mkdir, readFile, readdir, writeFile } from "fs/promises";
36
+ import path from "path";
37
+ async function fileExists(target) {
38
+ try {
39
+ await access(target, constants.F_OK);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ async function readJson(target) {
46
+ if (!await fileExists(target)) return null;
47
+ const raw = await readFile(target, "utf8");
48
+ return JSON.parse(raw);
49
+ }
50
+ async function writeJson(target, data) {
51
+ await ensureDir(path.dirname(target));
52
+ await writeFile(target, `${JSON.stringify(data, null, 2)}
53
+ `, "utf8");
54
+ }
55
+ async function ensureDir(target) {
56
+ await mkdir(target, { recursive: true });
57
+ }
58
+ async function listDirectory(rootDir) {
59
+ return readdir(rootDir);
60
+ }
61
+
62
+ // src/lifecycle/paths.ts
63
+ import path2 from "path";
64
+ function resolveContained(root, rel) {
65
+ const rootAbs = path2.resolve(root);
66
+ const candidate = path2.resolve(rootAbs, rel);
67
+ const relToRoot = path2.relative(rootAbs, candidate);
68
+ if (relToRoot.startsWith("..") || path2.isAbsolute(relToRoot)) {
69
+ throw new Error(`Path escapes registry/project root: ${rel}`);
70
+ }
71
+ return candidate;
72
+ }
73
+ function toPosixRel(root, absPath) {
74
+ return path2.relative(root, absPath).split(path2.sep).join("/");
75
+ }
76
+
77
+ // src/lifecycle/glob.ts
78
+ function normalizeRelPath(input) {
79
+ return input.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/");
80
+ }
81
+ function matchSegments(patternParts, pathParts) {
82
+ let pi = 0;
83
+ let si = 0;
84
+ while (pi < patternParts.length && si < pathParts.length) {
85
+ const p = patternParts[pi] ?? "";
86
+ if (p === "**") {
87
+ if (pi === patternParts.length - 1) return true;
88
+ for (let skip = si; skip <= pathParts.length; skip++) {
89
+ if (matchSegments(patternParts.slice(pi + 1), pathParts.slice(skip))) return true;
90
+ }
91
+ return false;
92
+ }
93
+ const segment = pathParts[si] ?? "";
94
+ if (p.includes("*") || p.includes("?")) {
95
+ const re = new RegExp(
96
+ `^${p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`
97
+ );
98
+ if (!re.test(segment)) return false;
99
+ } else if (p !== segment) {
100
+ return false;
101
+ }
102
+ pi += 1;
103
+ si += 1;
104
+ }
105
+ while (pi < patternParts.length && patternParts[pi] === "**") pi += 1;
106
+ return pi === patternParts.length && si === pathParts.length;
107
+ }
108
+ function matchesAnyGlob(relPath, globs) {
109
+ const normalized = normalizeRelPath(relPath);
110
+ const pathParts = normalized.split("/").filter(Boolean);
111
+ for (const glob of globs) {
112
+ const pattern = normalizeRelPath(glob);
113
+ if (matchSegments(pattern.split("/").filter(Boolean), pathParts)) return true;
114
+ }
115
+ return false;
116
+ }
117
+
118
+ // src/lifecycle/protected.ts
119
+ function resolveProtectedGlobs(manifest) {
120
+ const fromManifest = manifest?.protected ?? [];
121
+ const overrides = (manifest?.overrides ?? []).map((o) => o.path);
122
+ const merged = [...DEFAULT_PROTECTED_PATHS, ...fromManifest, ...overrides];
123
+ return [...new Set(merged.map(normalizeRelPath))];
124
+ }
125
+ function isProtectedPath(relPath, globs) {
126
+ return matchesAnyGlob(relPath, globs);
127
+ }
128
+
129
+ // src/lifecycle/apply.ts
130
+ function emptyStats() {
131
+ return { written: [], skippedProtected: [], missing: [], unchanged: [] };
132
+ }
133
+ function mergeStats(into, from) {
134
+ into.written.push(...from.written);
135
+ into.skippedProtected.push(...from.skippedProtected);
136
+ into.missing.push(...from.missing);
137
+ into.unchanged.push(...from.unchanged);
138
+ return into;
139
+ }
140
+ async function copyRegistryFile(registryRoot, projectRoot, sourceRel, targetRel, protectedGlobs) {
141
+ const targetNorm = targetRel.split(path3.sep).join("/");
142
+ if (isProtectedPath(targetNorm, protectedGlobs)) {
143
+ return "skipped-protected";
144
+ }
145
+ const sourceAbs = resolveContained(registryRoot, sourceRel);
146
+ const targetAbs = resolveContained(projectRoot, targetRel);
147
+ try {
148
+ await readFile2(sourceAbs);
149
+ } catch {
150
+ return "missing-source";
151
+ }
152
+ let existing = null;
153
+ try {
154
+ existing = await readFile2(targetAbs, "utf8");
155
+ } catch {
156
+ existing = null;
157
+ }
158
+ const next = await readFile2(sourceAbs, "utf8");
159
+ if (existing === next) return "unchanged";
160
+ await mkdir2(path3.dirname(targetAbs), { recursive: true });
161
+ await copyFile(sourceAbs, targetAbs);
162
+ return "written";
163
+ }
164
+ function recordOutcome(stats, targetRel, outcome) {
165
+ const rel = targetRel.split(path3.sep).join("/");
166
+ switch (outcome) {
167
+ case "written":
168
+ stats.written.push(rel);
169
+ break;
170
+ case "skipped-protected":
171
+ stats.skippedProtected.push(rel);
172
+ break;
173
+ case "missing-source":
174
+ stats.missing.push(rel);
175
+ break;
176
+ case "unchanged":
177
+ stats.unchanged.push(rel);
178
+ break;
179
+ }
180
+ }
181
+ async function saveManifest(projectRoot, manifest) {
182
+ const target = path3.join(projectRoot, MANIFEST_RELATIVE_PATH);
183
+ const payload = {
184
+ ...manifest,
185
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
186
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
187
+ };
188
+ await writeJson(target, payload);
189
+ return toPosixRel(projectRoot, target);
190
+ }
191
+ function buildManifest(input) {
192
+ const manifest = {
193
+ schemaVersion: 1,
194
+ version: input.version,
195
+ protected: input.protected ?? [...DEFAULT_PROTECTED_PATHS]
196
+ };
197
+ if (input.profile) manifest.profile = input.profile;
198
+ if (input.packs?.length) manifest.packs = [...new Set(input.packs)].sort();
199
+ if (input.skills?.length) manifest.skills = [...new Set(input.skills)].sort();
200
+ if (input.registryUrl || input.registryRef) {
201
+ manifest.registry = {};
202
+ if (input.registryUrl) manifest.registry.url = input.registryUrl;
203
+ if (input.registryRef) manifest.registry.ref = input.registryRef;
204
+ }
205
+ return manifest;
206
+ }
207
+ function upsertIdList(list, id) {
208
+ return [.../* @__PURE__ */ new Set([...list ?? [], id])].sort();
209
+ }
210
+
211
+ // src/utils/logger.ts
212
+ import { cyan, green, red, yellow } from "kolorist";
213
+ var logger = {
214
+ info(message) {
215
+ console.log(cyan("info"), message);
216
+ },
217
+ success(message) {
218
+ console.log(green("done"), message);
219
+ },
220
+ warn(message) {
221
+ console.log(yellow("warn"), message);
222
+ },
223
+ error(message) {
224
+ console.error(red("error"), message);
225
+ }
226
+ };
227
+
228
+ // src/lifecycle/report.ts
229
+ function logApplyStats(stats) {
230
+ if (stats.written.length > 0) {
231
+ logger.success(`Wrote ${stats.written.length} file(s)`);
232
+ for (const p of stats.written) logger.info(` + ${p}`);
233
+ }
234
+ if (stats.unchanged.length > 0) {
235
+ logger.info(`Unchanged: ${stats.unchanged.length}`);
236
+ }
237
+ if (stats.skippedProtected.length > 0) {
238
+ logger.warn(`Skipped protected (L3): ${stats.skippedProtected.length}`);
239
+ for (const p of stats.skippedProtected) logger.info(` ~ ${p}`);
240
+ }
241
+ if (stats.missing.length > 0) {
242
+ logger.warn(`Missing in registry: ${stats.missing.length}`);
243
+ for (const p of stats.missing) logger.info(` ? ${p}`);
244
+ }
245
+ }
246
+
247
+ // src/registry/resolve.ts
248
+ import { execFile } from "child_process";
249
+ import { createHash } from "crypto";
250
+ import { homedir } from "os";
251
+ import path4 from "path";
252
+ import { promisify } from "util";
253
+ var execFileAsync = promisify(execFile);
254
+ var DEFAULT_REGISTRY_URL = "https://github.com/agent-kit-startup/agent-kit";
255
+ var DEFAULT_REGISTRY_REF = "main";
256
+ var SAFE_URL = /^https:\/\/[^\s]+$/;
257
+ var SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
258
+ function assertSafeRegistrySource(url, ref) {
259
+ if (!SAFE_URL.test(url)) {
260
+ throw new Error(
261
+ `Refusing registry url "${url}": only https:// URLs are allowed. Use --registry <local-path> for local checkouts.`
262
+ );
263
+ }
264
+ if (!SAFE_REF.test(ref)) {
265
+ throw new Error(`Refusing registry ref "${ref}": invalid characters or leading "-".`);
266
+ }
267
+ }
268
+ function gitEnv() {
269
+ return { ...process.env, GIT_ALLOW_PROTOCOL: "https" };
270
+ }
271
+ function cacheKey(url, ref) {
272
+ return createHash("sha256").update(`${url}@${ref}`).digest("hex").slice(0, 16);
273
+ }
274
+ async function hasRegistryIndex(root) {
275
+ return fileExists(path4.join(root, "registry", "registry.json"));
276
+ }
277
+ async function cloneRegistry(url, ref, dest) {
278
+ const { mkdir: mkdir5 } = await import("fs/promises");
279
+ await mkdir5(path4.dirname(dest), { recursive: true });
280
+ try {
281
+ await execFileAsync("git", ["clone", "--depth", "1", "--branch", ref, "--", url, dest], {
282
+ env: gitEnv()
283
+ });
284
+ } catch {
285
+ await execFileAsync("git", ["clone", "--depth", "1", "--", url, dest], { env: gitEnv() });
286
+ await execFileAsync("git", ["checkout", ref, "--"], { cwd: dest, env: gitEnv() });
287
+ }
288
+ }
289
+ async function refreshCache(cacheDir) {
290
+ try {
291
+ await execFileAsync("git", ["fetch", "--depth", "1", "origin"], {
292
+ cwd: cacheDir,
293
+ env: gitEnv()
294
+ });
295
+ await execFileAsync("git", ["reset", "--hard", "FETCH_HEAD"], {
296
+ cwd: cacheDir,
297
+ env: gitEnv()
298
+ });
299
+ } catch {
300
+ }
301
+ }
302
+ async function resolveRegistryRoot(options) {
303
+ if (options.registryPath) {
304
+ const root = path4.resolve(options.registryPath);
305
+ if (!await hasRegistryIndex(root)) {
306
+ throw new Error(`No registry/registry.json under --registry ${root}`);
307
+ }
308
+ return { root, source: "flag" };
309
+ }
310
+ const envPath = process.env.AGENT_KIT_REGISTRY;
311
+ if (envPath) {
312
+ const root = path4.resolve(envPath);
313
+ if (!await hasRegistryIndex(root)) {
314
+ throw new Error(`No registry/registry.json under AGENT_KIT_REGISTRY=${root}`);
315
+ }
316
+ return { root, source: "env", url: options.registryUrl, ref: options.registryRef };
317
+ }
318
+ if (await hasRegistryIndex(options.cwd)) {
319
+ return {
320
+ root: path4.resolve(options.cwd),
321
+ source: "cwd",
322
+ url: options.registryUrl,
323
+ ref: options.registryRef
324
+ };
325
+ }
326
+ const url = options.registryUrl ?? DEFAULT_REGISTRY_URL;
327
+ const ref = options.registryRef ?? DEFAULT_REGISTRY_REF;
328
+ assertSafeRegistrySource(url, ref);
329
+ const dest = path4.join(homedir(), ".cache", "agent-kit", "registry", cacheKey(url, ref));
330
+ if (await hasRegistryIndex(dest)) {
331
+ if (options.refresh) await refreshCache(dest);
332
+ return { root: dest, source: "remote-cache", url, ref };
333
+ }
334
+ await cloneRegistry(url, ref, dest);
335
+ if (!await hasRegistryIndex(dest)) {
336
+ throw new Error(`Cloned ${url}@${ref} but registry/registry.json is missing`);
337
+ }
338
+ return { root: dest, source: "remote-cache", url, ref };
339
+ }
340
+
341
+ // src/lifecycle/resolve-cli.ts
342
+ async function resolveRegistryFromCli(options) {
343
+ const fromManifest = options.manifest?.registry;
344
+ return resolveRegistryRoot({
345
+ cwd: options.cwd,
346
+ registryPath: options.registry,
347
+ registryUrl: options.url ?? fromManifest?.url,
348
+ registryRef: options.ref ?? fromManifest?.ref,
349
+ refresh: options.refresh
350
+ });
351
+ }
352
+ var REGISTRY_CLI_ARGS = {
353
+ registry: {
354
+ type: "string",
355
+ description: "Local path to a kit checkout that contains registry/"
356
+ },
357
+ url: {
358
+ type: "string",
359
+ description: "Remote registry git URL (default: public agent-kit)"
360
+ },
361
+ ref: {
362
+ type: "string",
363
+ description: "Git ref for remote registry (branch or tag)"
364
+ },
365
+ refresh: {
366
+ type: "boolean",
367
+ description: "Refresh cached remote registry",
368
+ default: false
369
+ }
370
+ };
371
+
372
+ // src/lifecycle/version.ts
373
+ var KIT_VERSION = "3.0.0";
374
+
375
+ // src/manifest/index.ts
376
+ import path5 from "path";
377
+
378
+ // src/manifest/parse.ts
379
+ var SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
380
+ var KEBAB_ID = /^[a-z][a-z0-9-]*$/;
381
+ var ManifestValidationError = class extends Error {
382
+ constructor(message, issues) {
383
+ super(message);
384
+ this.issues = issues;
385
+ this.name = "ManifestValidationError";
386
+ }
387
+ issues;
388
+ };
389
+ function isPlainObject(value) {
390
+ return typeof value === "object" && value !== null && !Array.isArray(value);
391
+ }
392
+ function assertStringArray(label, value, issues) {
393
+ if (value === void 0) return void 0;
394
+ if (!Array.isArray(value)) {
395
+ issues.push(`${label} must be an array`);
396
+ return void 0;
397
+ }
398
+ const out = [];
399
+ for (let i = 0; i < value.length; i++) {
400
+ const item = value[i];
401
+ if (typeof item !== "string" || item.length === 0) {
402
+ issues.push(`${label}[${i}] must be a non-empty string`);
403
+ continue;
404
+ }
405
+ out.push(item);
406
+ }
407
+ return out;
408
+ }
409
+ function assertKebabIdArray(label, value, issues) {
410
+ const arr = assertStringArray(label, value, issues);
411
+ if (!arr) return arr;
412
+ for (let i = 0; i < arr.length; i++) {
413
+ if (!KEBAB_ID.test(arr[i] ?? "")) {
414
+ issues.push(`${label}[${i}] must match ${KEBAB_ID}`);
415
+ }
416
+ }
417
+ return arr;
418
+ }
419
+ function parseOverrides(value, issues) {
420
+ if (value === void 0) return void 0;
421
+ if (!Array.isArray(value)) {
422
+ issues.push("overrides must be an array");
423
+ return void 0;
424
+ }
425
+ const out = [];
426
+ for (let i = 0; i < value.length; i++) {
427
+ const item = value[i];
428
+ if (!isPlainObject(item)) {
429
+ issues.push(`overrides[${i}] must be an object`);
430
+ continue;
431
+ }
432
+ if (typeof item.path !== "string" || item.path.length === 0) {
433
+ issues.push(`overrides[${i}].path must be a non-empty string`);
434
+ continue;
435
+ }
436
+ const entry = { path: item.path };
437
+ if (item.replaces !== void 0) {
438
+ if (typeof item.replaces !== "string" || item.replaces.length === 0) {
439
+ issues.push(`overrides[${i}].replaces must be a non-empty string`);
440
+ } else {
441
+ entry.replaces = item.replaces;
442
+ }
443
+ }
444
+ if (item.note !== void 0) {
445
+ if (typeof item.note !== "string") {
446
+ issues.push(`overrides[${i}].note must be a string`);
447
+ } else {
448
+ entry.note = item.note;
449
+ }
450
+ }
451
+ out.push(entry);
452
+ }
453
+ return out;
454
+ }
455
+ function parseAgentKitManifest(raw) {
456
+ const issues = [];
457
+ if (!isPlainObject(raw)) {
458
+ throw new ManifestValidationError("Manifest must be a JSON object", ["root must be object"]);
459
+ }
460
+ const { $schema: _schema, ...rest } = raw;
461
+ void _schema;
462
+ if (rest.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
463
+ issues.push(`schemaVersion must be ${MANIFEST_SCHEMA_VERSION}`);
464
+ }
465
+ if (typeof rest.version !== "string" || !SEMVER.test(rest.version)) {
466
+ issues.push("version must be a semver string");
467
+ }
468
+ if (rest.profile !== void 0 && (typeof rest.profile !== "string" || rest.profile.length === 0)) {
469
+ issues.push("profile must be a non-empty string");
470
+ }
471
+ const packs = assertKebabIdArray("packs", rest.packs, issues);
472
+ const skills = assertKebabIdArray("skills", rest.skills, issues);
473
+ const protectedPaths = assertStringArray("protected", rest.protected, issues);
474
+ const overrides = parseOverrides(rest.overrides, issues);
475
+ let registry;
476
+ if (rest.registry !== void 0) {
477
+ if (!isPlainObject(rest.registry)) {
478
+ issues.push("registry must be an object");
479
+ } else {
480
+ registry = {};
481
+ if (rest.registry.url !== void 0) {
482
+ if (typeof rest.registry.url !== "string") {
483
+ issues.push("registry.url must be a string");
484
+ } else {
485
+ registry.url = rest.registry.url;
486
+ }
487
+ }
488
+ if (rest.registry.ref !== void 0) {
489
+ if (typeof rest.registry.ref !== "string" || rest.registry.ref.length === 0) {
490
+ issues.push("registry.ref must be a non-empty string");
491
+ } else {
492
+ registry.ref = rest.registry.ref;
493
+ }
494
+ }
495
+ }
496
+ }
497
+ if (rest.installedAt !== void 0 && typeof rest.installedAt !== "string") {
498
+ issues.push("installedAt must be a string");
499
+ }
500
+ const allowed = /* @__PURE__ */ new Set([
501
+ "schemaVersion",
502
+ "version",
503
+ "profile",
504
+ "packs",
505
+ "skills",
506
+ "protected",
507
+ "overrides",
508
+ "registry",
509
+ "installedAt"
510
+ ]);
511
+ for (const key of Object.keys(rest)) {
512
+ if (!allowed.has(key)) {
513
+ issues.push(`unknown field: ${key}`);
514
+ }
515
+ }
516
+ if (issues.length > 0) {
517
+ throw new ManifestValidationError(`Invalid agent-kit.json (${issues.length} issue(s))`, issues);
518
+ }
519
+ const manifest = {
520
+ schemaVersion: 1,
521
+ version: rest.version
522
+ };
523
+ if (typeof rest.profile === "string") manifest.profile = rest.profile;
524
+ if (packs) manifest.packs = packs;
525
+ if (skills) manifest.skills = skills;
526
+ if (protectedPaths) manifest.protected = protectedPaths;
527
+ if (overrides) manifest.overrides = overrides;
528
+ if (registry) manifest.registry = registry;
529
+ if (typeof rest.installedAt === "string") manifest.installedAt = rest.installedAt;
530
+ return manifest;
531
+ }
532
+
533
+ // src/manifest/index.ts
534
+ async function loadAgentKitManifest(cwd) {
535
+ const target = path5.join(cwd, MANIFEST_RELATIVE_PATH);
536
+ const raw = await readJson(target);
537
+ if (raw === null) return null;
538
+ return parseAgentKitManifest(raw);
539
+ }
540
+
541
+ // src/registry/client.ts
542
+ import path6 from "path";
543
+ async function loadRegistry(rootDir) {
544
+ const indexPath = path6.join(rootDir, "registry", "registry.json");
545
+ const index = await readJson(indexPath);
546
+ if (!index) throw new Error(`Registry not found at ${indexPath}`);
547
+ return index;
548
+ }
549
+ function allSkills(index) {
550
+ return [...index.skills.core, ...index.skills.community];
551
+ }
552
+ function allPacks(index) {
553
+ return index.packs ?? [];
554
+ }
555
+ async function findSkill(rootDir, skillId) {
556
+ const index = await loadRegistry(rootDir);
557
+ const skill = allSkills(index).find((item) => item.id === skillId);
558
+ if (!skill) throw new Error(`Skill '${skillId}' not found in registry.`);
559
+ return skill;
560
+ }
561
+ async function findPack(rootDir, packId) {
562
+ const index = await loadRegistry(rootDir);
563
+ const pack = allPacks(index).find((item) => item.id === packId);
564
+ if (!pack) throw new Error(`Pack '${packId}' not found in registry.`);
565
+ return pack;
566
+ }
567
+
568
+ // src/registry/install.ts
569
+ import { readFile as readFile3 } from "fs/promises";
570
+ import path7 from "path";
571
+ function targetForMember(member) {
572
+ switch (member.kind) {
573
+ case "skill": {
574
+ const category = member.source.includes("/core/") ? "core" : "community";
575
+ return {
576
+ sourceRel: path7.posix.join(member.source, "SKILL.md"),
577
+ targetRel: path7.posix.join(".cursor", "skills", category, member.id, "SKILL.md")
578
+ };
579
+ }
580
+ case "rule":
581
+ return {
582
+ sourceRel: member.source,
583
+ targetRel: path7.posix.join(".cursor", "rules", path7.posix.basename(member.source))
584
+ };
585
+ case "agent":
586
+ return {
587
+ sourceRel: member.source,
588
+ targetRel: path7.posix.join(".cursor", "agents", path7.posix.basename(member.source))
589
+ };
590
+ case "command":
591
+ return {
592
+ sourceRel: member.source,
593
+ targetRel: path7.posix.join(".cursor", "commands", path7.posix.basename(member.source))
594
+ };
595
+ case "hook": {
596
+ const targetRel = member.source.startsWith(".cursor/") ? member.source : path7.posix.join(".cursor", "hooks", path7.posix.basename(member.source));
597
+ return { sourceRel: member.source, targetRel };
598
+ }
599
+ case "template": {
600
+ const base = path7.posix.basename(member.source);
601
+ if (member.source.includes("/context-management/") || member.source.includes("/engineering-architecture/")) {
602
+ return {
603
+ sourceRel: member.source,
604
+ targetRel: path7.posix.join(".cursor", "context", "templates", base)
605
+ };
606
+ }
607
+ return {
608
+ sourceRel: member.source,
609
+ targetRel: path7.posix.join("templates", base)
610
+ };
611
+ }
612
+ default: {
613
+ const _exhaustive = member.kind;
614
+ throw new Error(`Unsupported pack member kind: ${_exhaustive}`);
615
+ }
616
+ }
617
+ }
618
+ async function installSkill(registryRoot, projectRoot, skill, options = {}) {
619
+ const stats = emptyStats();
620
+ const category = skill.path.includes("/core/") ? "core" : "community";
621
+ const sourceRel = path7.posix.join(skill.path, "SKILL.md");
622
+ const targetRel = path7.posix.join(".cursor", "skills", category, skill.id, "SKILL.md");
623
+ const outcome = await copyRegistryFile(
624
+ registryRoot,
625
+ projectRoot,
626
+ sourceRel,
627
+ targetRel,
628
+ options.protectedGlobs ?? []
629
+ );
630
+ recordOutcome(stats, targetRel, outcome);
631
+ return stats;
632
+ }
633
+ async function installSkillsByIds(registryRoot, projectRoot, skillIds, options = {}) {
634
+ const index = await readJson(path7.join(registryRoot, "registry", "registry.json"));
635
+ const stats = emptyStats();
636
+ if (!index) return stats;
637
+ const pool = allSkills(index);
638
+ for (const id of skillIds) {
639
+ const skill = pool.find((s) => s.id === id);
640
+ if (!skill) {
641
+ stats.missing.push(id);
642
+ continue;
643
+ }
644
+ mergeStats(stats, await installSkill(registryRoot, projectRoot, skill, options));
645
+ }
646
+ return stats;
647
+ }
648
+ async function loadPackManifest(registryRoot, packId) {
649
+ const summary = await findPack(registryRoot, packId);
650
+ const manifestPath = resolveContained(registryRoot, path7.join(summary.path, "pack.json"));
651
+ const manifest = await readJson(manifestPath);
652
+ if (!manifest) throw new Error(`Pack manifest not found at ${manifestPath}`);
653
+ return manifest;
654
+ }
655
+ async function installPack(registryRoot, projectRoot, packId, options = {}) {
656
+ await loadRegistry(registryRoot);
657
+ const packManifest = await loadPackManifest(registryRoot, packId);
658
+ const stats = emptyStats();
659
+ const protectedGlobs = options.protectedGlobs ?? [];
660
+ for (const member of packManifest.members) {
661
+ const { sourceRel, targetRel } = targetForMember(member);
662
+ const outcome = await copyRegistryFile(
663
+ registryRoot,
664
+ projectRoot,
665
+ sourceRel,
666
+ targetRel,
667
+ protectedGlobs
668
+ );
669
+ recordOutcome(stats, targetRel, outcome);
670
+ }
671
+ return stats;
672
+ }
673
+ function packMemberTargets(member) {
674
+ return targetForMember(member);
675
+ }
676
+
677
+ // src/commands/add.ts
678
+ var addCommand = defineCommand({
679
+ meta: {
680
+ name: "add",
681
+ description: "Install a skill or L1 pack from the registry into the project."
682
+ },
683
+ args: {
684
+ id: {
685
+ type: "positional",
686
+ description: "Skill id or pack id from registry"
687
+ },
688
+ skill: {
689
+ type: "boolean",
690
+ description: "Force treat id as a skill (when pack shares the same id)",
691
+ default: false
692
+ },
693
+ pack: {
694
+ type: "boolean",
695
+ description: "Force treat id as a pack (when skill shares the same id)",
696
+ default: false
697
+ },
698
+ cwd: {
699
+ type: "string",
700
+ default: process.cwd()
701
+ },
702
+ ...REGISTRY_CLI_ARGS
703
+ },
704
+ async run({ args }) {
705
+ if (!args.id) throw new Error("Provide an id: agent-kit add <skill|pack>");
706
+ if (args.skill && args.pack) {
707
+ throw new Error("Use only one of --skill or --pack");
708
+ }
709
+ const existing = await loadAgentKitManifest(args.cwd);
710
+ const registry = await resolveRegistryFromCli({
711
+ cwd: args.cwd,
712
+ registry: args.registry,
713
+ url: args.url,
714
+ ref: args.ref,
715
+ refresh: args.refresh,
716
+ manifest: existing
717
+ });
718
+ const protectedGlobs = resolveProtectedGlobs(existing);
719
+ const index = await loadRegistry(registry.root);
720
+ const asSkill = allSkills(index).find((s) => s.id === args.id);
721
+ const asPack = allPacks(index).find((p) => p.id === args.id);
722
+ const base = existing ? { ...existing } : buildManifest({
723
+ version: KIT_VERSION,
724
+ profile: "default",
725
+ registryUrl: registry.url,
726
+ registryRef: registry.ref
727
+ });
728
+ const finish = async (kind, id) => {
729
+ if (kind === "skill") base.skills = upsertIdList(base.skills, id);
730
+ else base.packs = upsertIdList(base.packs, id);
731
+ if (registry.url || registry.ref) {
732
+ base.registry = {
733
+ url: registry.url ?? base.registry?.url,
734
+ ref: registry.ref ?? base.registry?.ref
735
+ };
736
+ }
737
+ base.version = KIT_VERSION;
738
+ await saveManifest(args.cwd, base);
739
+ };
740
+ if (args.skill) {
741
+ const skill = await findSkill(registry.root, args.id);
742
+ const stats = await installSkill(registry.root, args.cwd, skill, { protectedGlobs });
743
+ logApplyStats(stats);
744
+ await finish("skill", skill.id);
745
+ logger.success(`Skill '${skill.id}' added.`);
746
+ return;
747
+ }
748
+ if (args.pack) {
749
+ const pack = await findPack(registry.root, args.id);
750
+ const stats = await installPack(registry.root, args.cwd, pack.id, { protectedGlobs });
751
+ logApplyStats(stats);
752
+ await finish("pack", pack.id);
753
+ logger.success(`Pack '${pack.id}' added.`);
754
+ return;
755
+ }
756
+ if (asSkill && asPack) {
757
+ throw new Error(
758
+ `'${args.id}' is both a skill and a pack. Disambiguate: agent-kit add --skill ${args.id} | agent-kit add --pack ${args.id}`
759
+ );
760
+ }
761
+ if (asSkill) {
762
+ const stats = await installSkill(registry.root, args.cwd, asSkill, { protectedGlobs });
763
+ logApplyStats(stats);
764
+ await finish("skill", asSkill.id);
765
+ logger.success(`Skill '${asSkill.id}' added.`);
766
+ return;
767
+ }
768
+ if (asPack) {
769
+ const stats = await installPack(registry.root, args.cwd, asPack.id, { protectedGlobs });
770
+ logApplyStats(stats);
771
+ await finish("pack", asPack.id);
772
+ logger.success(`Pack '${asPack.id}' added.`);
773
+ return;
774
+ }
775
+ throw new Error(`'${args.id}' not found as skill or pack in registry.`);
776
+ }
777
+ });
778
+
779
+ // src/commands/contribute.ts
780
+ import { defineCommand as defineCommand2 } from "citty";
781
+
782
+ // src/lifecycle/contribute.ts
783
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
784
+ import path8 from "path";
785
+
786
+ // src/lifecycle/contribute-gate.ts
787
+ var SECRET_PATTERNS = [
788
+ {
789
+ code: "secret-aws",
790
+ re: /AKIA[0-9A-Z]{16}/,
791
+ message: "Looks like an AWS access key id"
792
+ },
793
+ {
794
+ code: "secret-private-key",
795
+ re: /-----BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY-----/,
796
+ message: "Contains a private key block"
797
+ },
798
+ {
799
+ code: "secret-bearer",
800
+ re: /(?:api[_-]?key|token|secret|password)\s*[:=]\s*['"][^'"]{12,}['"]/i,
801
+ message: "Looks like an inline API key / token / password"
802
+ },
803
+ {
804
+ code: "secret-github-pat",
805
+ re: /\bghp_[A-Za-z0-9]{20,}\b/,
806
+ message: "Looks like a GitHub personal access token"
807
+ }
808
+ ];
809
+ var HYGIENE_PATTERNS = [
810
+ {
811
+ code: "meta-agent",
812
+ re: /\b(como IA|as an AI|no meu racioc[ií]nio|vou usar a tool|contexto do chat)\b/i,
813
+ message: "Agent metalinguage \u2014 not allowed in versioned kit artifacts"
814
+ },
815
+ {
816
+ code: "session-transient",
817
+ re: /\b(conforme falamos|no chat anterior|WIP do agente|hoje eu configurei)\b/i,
818
+ message: "Session / chat-transient wording \u2014 rewrite in project voice"
819
+ }
820
+ ];
821
+ var BLOCKED_PATH_GLOBS = [
822
+ /^\.cursor\/HANDOFF\.md$/i,
823
+ /^\.cursor\/plans\//i,
824
+ /^\.cursor\/memory\//i,
825
+ /^\.cursor\/context\//i,
826
+ /^\.env/i,
827
+ /(^|\/)credentials?\./i,
828
+ /(^|\/)secrets?\./i
829
+ ];
830
+ var MAX_BYTES = 2e5;
831
+ function gateContributePath(projectRel) {
832
+ const posix = projectRel.split("\\").join("/");
833
+ const issues = [];
834
+ for (const re of BLOCKED_PATH_GLOBS) {
835
+ if (re.test(posix)) {
836
+ issues.push({
837
+ code: "path-blocked",
838
+ message: `Path is session/L3 or secret-like \u2014 cannot contribute: ${posix}`
839
+ });
840
+ }
841
+ }
842
+ return issues;
843
+ }
844
+ function gateContributeContent(content, projectRel) {
845
+ const issues = [...gateContributePath(projectRel)];
846
+ const bytes = Buffer.byteLength(content, "utf8");
847
+ if (bytes > MAX_BYTES) {
848
+ issues.push({
849
+ code: "too-large",
850
+ message: `File too large (${bytes} bytes; max ${MAX_BYTES})`
851
+ });
852
+ }
853
+ for (const p of SECRET_PATTERNS) {
854
+ if (p.re.test(content)) issues.push({ code: p.code, message: p.message });
855
+ }
856
+ for (const p of HYGIENE_PATTERNS) {
857
+ if (p.re.test(content)) issues.push({ code: p.code, message: p.message });
858
+ }
859
+ return { ok: issues.length === 0, issues };
860
+ }
861
+
862
+ // src/lifecycle/l0.ts
863
+ var L0_ARTIFACTS = [
864
+ // Rules
865
+ {
866
+ source: ".cursor/rules/cursor-plan-handoff.mdc",
867
+ target: ".cursor/rules/cursor-plan-handoff.mdc"
868
+ },
869
+ {
870
+ source: ".cursor/rules/context-guardian.mdc",
871
+ target: ".cursor/rules/context-guardian.mdc"
872
+ },
873
+ {
874
+ source: ".cursor/rules/cursor-skills-git-workflow.mdc",
875
+ target: ".cursor/rules/cursor-skills-git-workflow.mdc"
876
+ },
877
+ {
878
+ source: ".cursor/rules/cursor-skills-general.mdc",
879
+ target: ".cursor/rules/cursor-skills-general.mdc"
880
+ },
881
+ { source: ".cursor/rules/ux-tone.mdc", target: ".cursor/rules/ux-tone.mdc" },
882
+ {
883
+ source: ".cursor/rules/agent-output-hygiene.mdc",
884
+ target: ".cursor/rules/agent-output-hygiene.mdc"
885
+ },
886
+ {
887
+ source: ".cursor/rules/docs-professional-standard.mdc",
888
+ target: ".cursor/rules/docs-professional-standard.mdc"
889
+ },
890
+ { source: ".cursor/rules/memory-loop.mdc", target: ".cursor/rules/memory-loop.mdc" },
891
+ {
892
+ source: "registry/rules/git-secrets-safety.mdc",
893
+ target: ".cursor/rules/git-secrets-safety.mdc"
894
+ },
895
+ // Commands
896
+ {
897
+ source: ".cursor/commands/start-project.md",
898
+ target: ".cursor/commands/start-project.md"
899
+ },
900
+ {
901
+ source: ".cursor/commands/continue-plan.md",
902
+ target: ".cursor/commands/continue-plan.md"
903
+ },
904
+ {
905
+ source: ".cursor/commands/run-plan.md",
906
+ target: ".cursor/commands/run-plan.md"
907
+ },
908
+ // Deprecated aliases of /run-plan (kept so existing references keep working)
909
+ {
910
+ source: ".cursor/commands/run-plan-loop.md",
911
+ target: ".cursor/commands/run-plan-loop.md"
912
+ },
913
+ {
914
+ source: ".cursor/commands/run-plan-orchestrated.md",
915
+ target: ".cursor/commands/run-plan-orchestrated.md"
916
+ },
917
+ { source: ".cursor/commands/handoff.md", target: ".cursor/commands/handoff.md" },
918
+ { source: ".cursor/commands/summary.md", target: ".cursor/commands/summary.md" },
919
+ {
920
+ source: ".cursor/commands/git-staging.md",
921
+ target: ".cursor/commands/git-staging.md"
922
+ },
923
+ { source: ".cursor/commands/git-prod.md", target: ".cursor/commands/git-prod.md" },
924
+ // Secrets gate (structural)
925
+ {
926
+ source: ".cursor/hooks/pre-commit/check-secrets.sh",
927
+ target: ".cursor/hooks/pre-commit/check-secrets.sh"
928
+ },
929
+ // Native Cursor agent hooks (session context + handoff on compact)
930
+ {
931
+ source: ".cursor/hooks.json",
932
+ target: ".cursor/hooks.json"
933
+ },
934
+ {
935
+ source: ".cursor/hooks/agent/session-plan-guard.py",
936
+ target: ".cursor/hooks/agent/session-plan-guard.py"
937
+ },
938
+ {
939
+ source: ".cursor/hooks/agent/precompact-handoff.py",
940
+ target: ".cursor/hooks/agent/precompact-handoff.py"
941
+ },
942
+ // Git spine docs at project root (not a nested agent-kit/ copy)
943
+ {
944
+ source: "autogit/gitupdate.md",
945
+ target: "autogit/gitupdate.md"
946
+ },
947
+ {
948
+ source: "autogit/plan-routine.md",
949
+ target: "autogit/plan-routine.md"
950
+ }
951
+ ];
952
+
953
+ // src/lifecycle/contribute.ts
954
+ async function readLocal(projectRoot, rel) {
955
+ try {
956
+ return await readFile4(resolveContained(projectRoot, rel), "utf8");
957
+ } catch {
958
+ return null;
959
+ }
960
+ }
961
+ async function buildRegistryPathMap(registryRoot, manifest) {
962
+ const map = /* @__PURE__ */ new Map();
963
+ for (const a of L0_ARTIFACTS) {
964
+ map.set(a.target.split(path8.sep).join("/"), a.source.split(path8.sep).join("/"));
965
+ }
966
+ for (const packId of manifest.packs ?? []) {
967
+ const pack = await loadPackManifest(registryRoot, packId);
968
+ for (const member of pack.members) {
969
+ const { sourceRel, targetRel } = packMemberTargets(member);
970
+ map.set(targetRel.split(path8.sep).join("/"), sourceRel.split(path8.sep).join("/"));
971
+ }
972
+ }
973
+ if ((manifest.skills ?? []).length > 0) {
974
+ const index = await loadRegistry(registryRoot);
975
+ const pool = allSkills(index);
976
+ for (const id of manifest.skills ?? []) {
977
+ const skill = pool.find((s) => s.id === id);
978
+ if (!skill) continue;
979
+ const category = skill.path.includes("/core/") ? "core" : "community";
980
+ const sourceRel = path8.posix.join(skill.path, "SKILL.md");
981
+ const targetRel = path8.posix.join(".cursor", "skills", category, skill.id, "SKILL.md");
982
+ map.set(targetRel, sourceRel);
983
+ }
984
+ }
985
+ return map;
986
+ }
987
+ function guessRegistryPath(projectRel) {
988
+ const p = projectRel.split(path8.sep).join("/");
989
+ if (p.startsWith(".cursor/skills/") && p.endsWith("/SKILL.md")) {
990
+ const rest = p.slice(".cursor/skills/".length);
991
+ const parts = rest.split("/");
992
+ if (parts.length === 3 && (parts[0] === "core" || parts[0] === "community")) {
993
+ return path8.posix.join("registry/skills", rest);
994
+ }
995
+ const skillId = parts[0];
996
+ if (parts.length === 2 && skillId) {
997
+ return path8.posix.join("registry/skills", "community", skillId, "SKILL.md");
998
+ }
999
+ }
1000
+ if (p.startsWith(".cursor/rules/")) {
1001
+ return p;
1002
+ }
1003
+ if (p.startsWith(".cursor/commands/")) {
1004
+ return p;
1005
+ }
1006
+ if (p.startsWith(".cursor/agents/")) {
1007
+ return p;
1008
+ }
1009
+ if (p.startsWith(".cursor/hooks/")) {
1010
+ return p;
1011
+ }
1012
+ if (p.startsWith("autogit/")) {
1013
+ return p;
1014
+ }
1015
+ return null;
1016
+ }
1017
+ async function planContribute(options) {
1018
+ const includeDrift = options.includeDrift !== false;
1019
+ const pathMap = await buildRegistryPathMap(options.registryRoot, options.manifest);
1020
+ const candidates = [];
1021
+ const seen = /* @__PURE__ */ new Set();
1022
+ const push = async (kind, projectPath, registryPath) => {
1023
+ const key = projectPath.split(path8.sep).join("/");
1024
+ if (seen.has(key)) return;
1025
+ seen.add(key);
1026
+ const content = await readLocal(options.projectRoot, projectPath);
1027
+ if (content === null) {
1028
+ return;
1029
+ }
1030
+ if (kind === "drift") {
1031
+ try {
1032
+ const upstream = await readFile4(
1033
+ resolveContained(options.registryRoot, registryPath),
1034
+ "utf8"
1035
+ );
1036
+ if (upstream === content) return;
1037
+ } catch {
1038
+ }
1039
+ }
1040
+ const gate = gateContributeContent(content, key);
1041
+ candidates.push({
1042
+ kind,
1043
+ projectPath: key,
1044
+ registryPath,
1045
+ gateOk: gate.ok,
1046
+ issues: gate.issues
1047
+ });
1048
+ };
1049
+ if (includeDrift) {
1050
+ for (const [projectPath, registryPath] of pathMap) {
1051
+ await push("drift", projectPath, registryPath);
1052
+ }
1053
+ }
1054
+ for (const raw of options.extraPaths ?? []) {
1055
+ const projectPath = raw.split(path8.sep).join("/");
1056
+ const blocked = gateContributePath(projectPath);
1057
+ if (blocked.length > 0) {
1058
+ if (!seen.has(projectPath)) {
1059
+ seen.add(projectPath);
1060
+ candidates.push({
1061
+ kind: "path",
1062
+ projectPath,
1063
+ registryPath: "",
1064
+ gateOk: false,
1065
+ issues: blocked
1066
+ });
1067
+ }
1068
+ continue;
1069
+ }
1070
+ const mapped = pathMap.get(projectPath) ?? guessRegistryPath(projectPath);
1071
+ if (!mapped) {
1072
+ candidates.push({
1073
+ kind: "path",
1074
+ projectPath,
1075
+ registryPath: "",
1076
+ gateOk: false,
1077
+ issues: [
1078
+ {
1079
+ code: "unmapped",
1080
+ message: "Cannot map to a registry path \u2014 pass an installed kit artifact or .cursor/{rules,commands,skills,agents,hooks}/*"
1081
+ }
1082
+ ]
1083
+ });
1084
+ continue;
1085
+ }
1086
+ await push("path", projectPath, mapped);
1087
+ }
1088
+ return {
1089
+ candidates,
1090
+ accepted: candidates.filter((c) => c.gateOk),
1091
+ rejected: candidates.filter((c) => !c.gateOk)
1092
+ };
1093
+ }
1094
+ async function writeContributeToRegistry(projectRoot, registryRoot, accepted) {
1095
+ const written = [];
1096
+ for (const c of accepted) {
1097
+ if (!c.registryPath) continue;
1098
+ const content = await readFile4(resolveContained(projectRoot, c.projectPath), "utf8");
1099
+ const dest = resolveContained(registryRoot, c.registryPath);
1100
+ await mkdir3(path8.dirname(dest), { recursive: true });
1101
+ await writeFile2(dest, content, "utf8");
1102
+ written.push(c.registryPath);
1103
+ }
1104
+ return written;
1105
+ }
1106
+ function formatPrBody(accepted) {
1107
+ const bullets = accepted.map((c) => `- \`${c.projectPath}\` \u2192 \`${c.registryPath}\``).join("\n");
1108
+ return [
1109
+ "## Summary",
1110
+ "Upstream contribution from a consumer project via `agent-kit contribute`.",
1111
+ "",
1112
+ "### Files",
1113
+ bullets || "- (none)",
1114
+ "",
1115
+ "## Checklist",
1116
+ "- [ ] Anti-slop / hygiene gate passed locally",
1117
+ "- [ ] No secrets or session state (HANDOFF/plans/memory)",
1118
+ "- [ ] Docs / registry index updated if this adds a skill or pack member",
1119
+ ""
1120
+ ].join("\n");
1121
+ }
1122
+
1123
+ // src/commands/contribute.ts
1124
+ function parsePaths(raw) {
1125
+ if (!raw?.trim()) return [];
1126
+ return [
1127
+ ...new Set(
1128
+ raw.split(",").map((s) => s.trim()).filter(Boolean)
1129
+ )
1130
+ ];
1131
+ }
1132
+ var contributeCommand = defineCommand2({
1133
+ meta: {
1134
+ name: "contribute",
1135
+ description: "Propose upstream changes: detect local drift/new kit artifacts, run anti-slop gate, optionally write into a kit checkout for a PR."
1136
+ },
1137
+ args: {
1138
+ cwd: {
1139
+ type: "string",
1140
+ default: process.cwd()
1141
+ },
1142
+ path: {
1143
+ type: "string",
1144
+ description: "Comma-separated project-relative paths to contribute (new or edited)"
1145
+ },
1146
+ drift: {
1147
+ type: "boolean",
1148
+ description: "Include drifted installed L0/pack/skill files vs registry (default: true)",
1149
+ default: true
1150
+ },
1151
+ write: {
1152
+ type: "boolean",
1153
+ description: "Copy accepted files into the local kit/registry checkout (--registry required / resolved)",
1154
+ default: false
1155
+ },
1156
+ ...REGISTRY_CLI_ARGS
1157
+ },
1158
+ async run({ args }) {
1159
+ const manifest = await loadAgentKitManifest(args.cwd);
1160
+ if (!manifest) {
1161
+ logger.warn("No .cursor/agent-kit.json \u2014 run agent-kit install first.");
1162
+ return;
1163
+ }
1164
+ const registry = await resolveRegistryFromCli({
1165
+ cwd: args.cwd,
1166
+ registry: args.registry,
1167
+ url: args.url,
1168
+ ref: args.ref,
1169
+ refresh: args.refresh,
1170
+ manifest
1171
+ });
1172
+ if (registry.source !== "flag" && args.write) {
1173
+ logger.warn(
1174
+ "contribute --write needs a local kit checkout. Pass --registry /path/to/agent-kit."
1175
+ );
1176
+ return;
1177
+ }
1178
+ logger.info(`Registry: ${registry.root} (${registry.source})`);
1179
+ const plan = await planContribute({
1180
+ registryRoot: registry.root,
1181
+ projectRoot: args.cwd,
1182
+ manifest,
1183
+ extraPaths: parsePaths(args.path),
1184
+ includeDrift: args.drift
1185
+ });
1186
+ console.log(
1187
+ `Contribute plan: accepted=${plan.accepted.length} rejected=${plan.rejected.length} (drift scan=${args.drift})`
1188
+ );
1189
+ if (plan.candidates.length === 0) {
1190
+ logger.success("Nothing to contribute (no drift / no --path).");
1191
+ return;
1192
+ }
1193
+ for (const c of plan.candidates) {
1194
+ const mark = c.gateOk ? "ok" : "FAIL";
1195
+ console.log(
1196
+ `${mark.padEnd(4)} ${c.kind.padEnd(5)} ${c.projectPath} \u2192 ${c.registryPath || "(unmapped)"}`
1197
+ );
1198
+ for (const issue of c.issues) {
1199
+ console.log(` [${issue.code}] ${issue.message}`);
1200
+ }
1201
+ }
1202
+ if (plan.accepted.length === 0) {
1203
+ logger.warn("No files passed the contribute gate.");
1204
+ return;
1205
+ }
1206
+ console.log("\n--- Suggested PR body ---\n");
1207
+ console.log(formatPrBody(plan.accepted));
1208
+ if (!args.write) {
1209
+ logger.info(
1210
+ "Dry run. Re-run with --write --registry /path/to/agent-kit to copy accepted files into the kit checkout, then open a PR (gh pr create --base main)."
1211
+ );
1212
+ return;
1213
+ }
1214
+ const written = await writeContributeToRegistry(args.cwd, registry.root, plan.accepted);
1215
+ logger.success(`Wrote ${written.length} file(s) into ${registry.root}`);
1216
+ for (const p of written) console.log(` + ${p}`);
1217
+ console.log("\nNext (HITL \u2014 review before push):");
1218
+ console.log(` cd ${registry.root}`);
1219
+ console.log(" git checkout -b contribute/<short-topic>");
1220
+ console.log(' git add <files> && git commit -m "feat: contribute <topic> from consumer"');
1221
+ console.log(" git push -u origin HEAD");
1222
+ console.log(' gh pr create --base main --title "feat: contribute <topic>" --body-file -');
1223
+ }
1224
+ });
1225
+
1226
+ // src/commands/diff.ts
1227
+ import { defineCommand as defineCommand3 } from "citty";
1228
+
1229
+ // src/lifecycle/diff.ts
1230
+ import { readFile as readFile5 } from "fs/promises";
1231
+ import path9 from "path";
1232
+ async function readIfExists(abs) {
1233
+ try {
1234
+ return await readFile5(abs, "utf8");
1235
+ } catch {
1236
+ return null;
1237
+ }
1238
+ }
1239
+ async function comparePair(registryRoot, projectRoot, sourceRel, targetRel, protectedGlobs) {
1240
+ const posixTarget = targetRel.split(path9.sep).join("/");
1241
+ if (isProtectedPath(posixTarget, protectedGlobs)) {
1242
+ return { path: posixTarget, status: "protected" };
1243
+ }
1244
+ let sourceAbs;
1245
+ try {
1246
+ sourceAbs = resolveContained(registryRoot, sourceRel);
1247
+ } catch {
1248
+ return { path: posixTarget, status: "missing-registry" };
1249
+ }
1250
+ const registryContent = await readIfExists(sourceAbs);
1251
+ if (registryContent === null) {
1252
+ return { path: posixTarget, status: "missing-registry" };
1253
+ }
1254
+ let targetAbs;
1255
+ try {
1256
+ targetAbs = resolveContained(projectRoot, targetRel);
1257
+ } catch {
1258
+ return { path: posixTarget, status: "missing-local" };
1259
+ }
1260
+ const localContent = await readIfExists(targetAbs);
1261
+ if (localContent === null) return { path: posixTarget, status: "missing-local" };
1262
+ if (localContent === registryContent) return { path: posixTarget, status: "match" };
1263
+ return { path: posixTarget, status: "drift" };
1264
+ }
1265
+ async function diffAgainstRegistry(registryRoot, projectRoot, manifest) {
1266
+ const protectedGlobs = resolveProtectedGlobs(manifest);
1267
+ const entries = [];
1268
+ const seen = /* @__PURE__ */ new Set();
1269
+ const pushUnique = async (sourceRel, targetRel) => {
1270
+ const key = targetRel.split(path9.sep).join("/");
1271
+ if (seen.has(key)) return;
1272
+ seen.add(key);
1273
+ entries.push(
1274
+ await comparePair(registryRoot, projectRoot, sourceRel, targetRel, protectedGlobs)
1275
+ );
1276
+ };
1277
+ for (const a of L0_ARTIFACTS) {
1278
+ await pushUnique(a.source, a.target);
1279
+ }
1280
+ for (const packId of manifest.packs ?? []) {
1281
+ const pack = await loadPackManifest(registryRoot, packId);
1282
+ for (const member of pack.members) {
1283
+ const { sourceRel, targetRel } = packMemberTargets(member);
1284
+ await pushUnique(sourceRel, targetRel);
1285
+ }
1286
+ }
1287
+ if ((manifest.skills ?? []).length > 0) {
1288
+ const index = await loadRegistry(registryRoot);
1289
+ const pool = allSkills(index);
1290
+ for (const id of manifest.skills ?? []) {
1291
+ const skill = pool.find((s) => s.id === id);
1292
+ if (!skill) {
1293
+ entries.push({ path: `skill:${id}`, status: "missing-registry" });
1294
+ continue;
1295
+ }
1296
+ const category = skill.path.includes("/core/") ? "core" : "community";
1297
+ const sourceRel = path9.posix.join(skill.path, "SKILL.md");
1298
+ const targetRel = path9.posix.join(".cursor", "skills", category, skill.id, "SKILL.md");
1299
+ await pushUnique(sourceRel, targetRel);
1300
+ }
1301
+ }
1302
+ return entries;
1303
+ }
1304
+ function summarizeDiff(entries) {
1305
+ const summary = {
1306
+ match: 0,
1307
+ drift: 0,
1308
+ "missing-local": 0,
1309
+ "missing-registry": 0,
1310
+ protected: 0
1311
+ };
1312
+ for (const e of entries) summary[e.status] += 1;
1313
+ return summary;
1314
+ }
1315
+
1316
+ // src/commands/diff.ts
1317
+ var diffCommand = defineCommand3({
1318
+ meta: {
1319
+ name: "diff",
1320
+ description: "Compare installed kit artifacts to the registry (respects L3 protected)."
1321
+ },
1322
+ args: {
1323
+ cwd: {
1324
+ type: "string",
1325
+ default: process.cwd()
1326
+ },
1327
+ all: {
1328
+ type: "boolean",
1329
+ description: "Include matching files in the report",
1330
+ default: false
1331
+ },
1332
+ ...REGISTRY_CLI_ARGS
1333
+ },
1334
+ async run({ args }) {
1335
+ const manifest = await loadAgentKitManifest(args.cwd);
1336
+ if (!manifest) {
1337
+ logger.warn("No .cursor/agent-kit.json \u2014 run agent-kit install first.");
1338
+ return;
1339
+ }
1340
+ const registry = await resolveRegistryFromCli({
1341
+ cwd: args.cwd,
1342
+ registry: args.registry,
1343
+ url: args.url,
1344
+ ref: args.ref,
1345
+ refresh: args.refresh,
1346
+ manifest
1347
+ });
1348
+ logger.info(`Registry: ${registry.root} (${registry.source})`);
1349
+ const entries = await diffAgainstRegistry(registry.root, args.cwd, manifest);
1350
+ const summary = summarizeDiff(entries);
1351
+ console.log(
1352
+ `Summary: match=${summary.match} drift=${summary.drift} missing-local=${summary["missing-local"]} missing-registry=${summary["missing-registry"]} protected=${summary.protected}`
1353
+ );
1354
+ const rows = args.all ? entries : entries.filter((e) => e.status !== "match");
1355
+ if (rows.length === 0) {
1356
+ logger.success("No drift (matches only).");
1357
+ return;
1358
+ }
1359
+ for (const e of rows) {
1360
+ console.log(`${e.status.padEnd(18)} ${e.path}`);
1361
+ }
1362
+ }
1363
+ });
1364
+
1365
+ // src/commands/handoff.ts
1366
+ import { spawn } from "child_process";
1367
+ import { readFile as readFile6, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
1368
+ import path10 from "path";
1369
+ import { defineCommand as defineCommand4 } from "citty";
1370
+
1371
+ // src/types.ts
1372
+ var GIT_PLATFORM_META = {
1373
+ github: {
1374
+ cli: "gh",
1375
+ prTerm: "Pull Request",
1376
+ prCommand: "gh pr create",
1377
+ ciDefault: "github-actions"
1378
+ },
1379
+ gitlab: {
1380
+ cli: "glab",
1381
+ prTerm: "Merge Request",
1382
+ prCommand: "glab mr create",
1383
+ ciDefault: "gitlab-ci"
1384
+ },
1385
+ bitbucket: {
1386
+ cli: "bb",
1387
+ prTerm: "Pull Request",
1388
+ prCommand: "bb pr create",
1389
+ ciDefault: "bitbucket-pipelines"
1390
+ },
1391
+ "azure-devops": {
1392
+ cli: "az repos",
1393
+ prTerm: "Pull Request",
1394
+ prCommand: "az repos pr create",
1395
+ ciDefault: "azure-pipelines"
1396
+ },
1397
+ gitea: { cli: "tea", prTerm: "Pull Request", prCommand: "tea pr create", ciDefault: "none" }
1398
+ };
1399
+ var CI_PLATFORM_FILES = {
1400
+ "github-actions": ".github/workflows",
1401
+ "gitlab-ci": ".gitlab-ci.yml",
1402
+ "azure-pipelines": "azure-pipelines.yml",
1403
+ "bitbucket-pipelines": "bitbucket-pipelines.yml",
1404
+ jenkins: "Jenkinsfile",
1405
+ circleci: ".circleci/config.yml",
1406
+ travis: ".travis.yml"
1407
+ };
1408
+ var PM_TOOL_LABELS = {
1409
+ clickup: "ClickUp",
1410
+ jira: "Jira",
1411
+ linear: "Linear",
1412
+ asana: "Asana",
1413
+ "github-issues": "GitHub Issues",
1414
+ "github-projects": "GitHub Projects",
1415
+ "azure-boards": "Azure Boards",
1416
+ trello: "Trello",
1417
+ shortcut: "Shortcut",
1418
+ notion: "Notion",
1419
+ youtrack: "YouTrack",
1420
+ none: "None"
1421
+ };
1422
+
1423
+ // src/commands/handoff.ts
1424
+ function parsePlanFrontmatter(raw) {
1425
+ const match = raw.match(/^---\n([\s\S]*?)\n---/);
1426
+ if (!match?.[1]) return null;
1427
+ const block = match[1];
1428
+ const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim();
1429
+ const todos = [];
1430
+ const todoBlocks = block.matchAll(/- id:\s*(\S+)\s*\n\s*content:\s*(.+)\n\s*status:\s*(\S+)/g);
1431
+ for (const m of todoBlocks) {
1432
+ if (m[1] && m[2] && m[3]) {
1433
+ todos.push({ id: m[1], content: m[2].replace(/^["']|["']$/g, ""), status: m[3] });
1434
+ }
1435
+ }
1436
+ return { name, todos };
1437
+ }
1438
+ async function findActivePlan(plansDir) {
1439
+ if (!await fileExists(plansDir)) return null;
1440
+ const files = (await readdir2(plansDir)).filter((f) => f.endsWith(".plan.md")).sort().reverse();
1441
+ for (const file of files) {
1442
+ const raw = await readFile6(path10.join(plansDir, file), "utf8");
1443
+ const fm = parsePlanFrontmatter(raw);
1444
+ if (fm?.todos?.some((t) => t.status !== "completed" && t.status !== "cancelled")) {
1445
+ return { file, raw };
1446
+ }
1447
+ }
1448
+ return files[0] ? { file: files[0], raw: await readFile6(path10.join(plansDir, files[0]), "utf8") } : null;
1449
+ }
1450
+ function now() {
1451
+ return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16);
1452
+ }
1453
+ async function loadProfile(rootDir) {
1454
+ const configPath = path10.join(rootDir, ".cursor", "agent-kit.config.json");
1455
+ try {
1456
+ return await readJson(configPath);
1457
+ } catch {
1458
+ return null;
1459
+ }
1460
+ }
1461
+ function buildRoutines(profile) {
1462
+ const lines = [];
1463
+ const workflow = profile?.git.workflow;
1464
+ if (workflow === "homolog-prod") {
1465
+ lines.push("- [ ] `git staging` - move changes to staging");
1466
+ lines.push("- [ ] `git prod` - promote to production (after approval)");
1467
+ } else {
1468
+ lines.push("- [ ] Commit, push and open PR/MR");
1469
+ }
1470
+ const pmTools = profile?.services.projectManagement;
1471
+ if (pmTools && pmTools.length > 0) {
1472
+ const labels = pmTools.map((t) => PM_TOOL_LABELS[t]).join(", ");
1473
+ lines.push(`- [ ] Update tasks in ${labels} (if applicable)`);
1474
+ }
1475
+ lines.push("- [ ] Review CHANGELOG");
1476
+ return lines;
1477
+ }
1478
+ function closingInstruction(profile) {
1479
+ const pmTools = profile?.services.projectManagement;
1480
+ if (pmTools && pmTools.length > 0) {
1481
+ const labels = pmTools.map((t) => PM_TOOL_LABELS[t]).join(", ");
1482
+ return `All todos completed. Check git staging/prod and update ${labels} if applicable.`;
1483
+ }
1484
+ return "All todos completed. Check commit/push and PR/MR.";
1485
+ }
1486
+ function buildHandoff(planFile, fm, profile) {
1487
+ const completed = fm.todos?.filter((t) => t.status === "completed") ?? [];
1488
+ const pending = fm.todos?.filter((t) => t.status === "pending") ?? [];
1489
+ const inProgress = fm.todos?.filter((t) => t.status === "in_progress") ?? [];
1490
+ const nextTodo = inProgress[0] ?? pending[0];
1491
+ const completedPhase = completed.length;
1492
+ const totalPhases = fm.todos?.length ?? 0;
1493
+ const routines = buildRoutines(profile);
1494
+ return [
1495
+ `# Handoff - ${fm.name ?? planFile}`,
1496
+ "",
1497
+ `- **Plan:** ${planFile}`,
1498
+ `- **Last updated:** ${now()}`,
1499
+ `- **Progress:** ${completedPhase}/${totalPhases} todos completed`,
1500
+ "",
1501
+ "## Completed",
1502
+ ...completed.length > 0 ? completed.map((t) => `- [x] \`${t.id}\`: ${t.content}`) : ["- (none)"],
1503
+ "",
1504
+ "## In progress",
1505
+ ...inProgress.length > 0 ? inProgress.map((t) => `- [ ] \`${t.id}\`: ${t.content}`) : ["- (none)"],
1506
+ "",
1507
+ "## Pending",
1508
+ ...pending.length > 0 ? pending.map((t) => `- [ ] \`${t.id}\`: ${t.content}`) : ["- (none)"],
1509
+ "",
1510
+ "## Instruction for Next Agent",
1511
+ "",
1512
+ nextTodo ? `Continue from todo \`${nextTodo.id}\`: ${nextTodo.content}.` : closingInstruction(profile),
1513
+ "",
1514
+ "## Suggested routines",
1515
+ "",
1516
+ ...routines,
1517
+ "",
1518
+ "---",
1519
+ "*Generated by `agent-kit handoff`. To resume: open new conversation and say `/continue-plan`.*",
1520
+ ""
1521
+ ].join("\n");
1522
+ }
1523
+ function printV3Guidance() {
1524
+ logger.info("No plan in .cursor/plans/ and no ./cursor-handoff in this directory.");
1525
+ logger.info(
1526
+ "Create a plan with todos or update .cursor/HANDOFF.md manually (/handoff command in Cursor)."
1527
+ );
1528
+ logger.info("Resume in IDE: /resume, summaries and transcripts.");
1529
+ }
1530
+ function runCursorHandoff(scriptPath, cwd) {
1531
+ return new Promise((resolve, reject) => {
1532
+ const child = spawn("sh", [scriptPath, "handoff"], {
1533
+ cwd,
1534
+ stdio: "inherit"
1535
+ });
1536
+ child.on("error", (err) => reject(err));
1537
+ child.on("close", (code) => resolve(code ?? 1));
1538
+ });
1539
+ }
1540
+ var handoffCommand = defineCommand4({
1541
+ meta: {
1542
+ name: "handoff",
1543
+ description: "Write .cursor/HANDOFF.md from the active Cursor plan, or run ./cursor-handoff handoff when no plan exists."
1544
+ },
1545
+ args: {
1546
+ cwd: {
1547
+ type: "string",
1548
+ description: "Project root directory",
1549
+ default: process.cwd()
1550
+ }
1551
+ },
1552
+ async run({ args }) {
1553
+ const profile = await loadProfile(args.cwd);
1554
+ const plansDir = path10.join(args.cwd, ".cursor", "plans");
1555
+ const handoffPath = path10.join(args.cwd, ".cursor", "HANDOFF.md");
1556
+ const plan = await findActivePlan(plansDir);
1557
+ if (plan) {
1558
+ const fm = parsePlanFrontmatter(plan.raw);
1559
+ if (fm) {
1560
+ await ensureDir(path10.join(args.cwd, ".cursor"));
1561
+ const content = buildHandoff(plan.file, fm, profile);
1562
+ await writeFile3(handoffPath, content, "utf8");
1563
+ logger.success("HANDOFF.md updated: .cursor/HANDOFF.md");
1564
+ logger.info(
1565
+ `Plan: ${plan.file} (${fm.todos?.filter((t) => t.status === "completed").length}/${fm.todos?.length} completed)`
1566
+ );
1567
+ logger.info("");
1568
+ logger.info("Suggested next steps:");
1569
+ let n = 1;
1570
+ for (const line of buildRoutines(profile)) {
1571
+ logger.info(` ${n}. ${line.replace(/^- \[ \] /, "")}`);
1572
+ n += 1;
1573
+ }
1574
+ logger.info("");
1575
+ logger.info("To resume: open new conversation and say /continue-plan");
1576
+ return;
1577
+ }
1578
+ logger.warn(`Plan ${plan.file} without valid frontmatter; trying legacy flow.`);
1579
+ }
1580
+ const scriptPath = path10.join(args.cwd, "cursor-handoff");
1581
+ if (!await fileExists(scriptPath)) {
1582
+ printV3Guidance();
1583
+ return;
1584
+ }
1585
+ if (process.platform === "win32") {
1586
+ logger.warn(
1587
+ "cursor-handoff is a shell script. On Windows, run in Git Bash or WSL: sh cursor-handoff handoff"
1588
+ );
1589
+ printV3Guidance();
1590
+ return;
1591
+ }
1592
+ try {
1593
+ const code = await runCursorHandoff(scriptPath, args.cwd);
1594
+ if (code !== 0) {
1595
+ process.exitCode = code;
1596
+ }
1597
+ } catch (err) {
1598
+ logger.warn(`Failed to execute cursor-handoff: ${String(err)}`);
1599
+ printV3Guidance();
1600
+ }
1601
+ }
1602
+ });
1603
+
1604
+ // src/commands/init.ts
1605
+ import path23 from "path";
1606
+ import { intro, outro } from "@clack/prompts";
1607
+ import { defineCommand as defineCommand5 } from "citty";
1608
+
1609
+ // src/generator/index.ts
1610
+ import { writeFile as writeFile9 } from "fs/promises";
1611
+ import path16 from "path";
1612
+
1613
+ // src/generator/agents-md.ts
1614
+ import { writeFile as writeFile4 } from "fs/promises";
1615
+ import path11 from "path";
1616
+
1617
+ // src/generator/platform.ts
1618
+ function gitProviderLabel(profile) {
1619
+ const p = profile.git.provider;
1620
+ if (!p || p === "other") return "Git";
1621
+ const labels = {
1622
+ github: "GitHub",
1623
+ gitlab: "GitLab",
1624
+ bitbucket: "Bitbucket",
1625
+ "azure-devops": "Azure DevOps",
1626
+ gitea: "Gitea"
1627
+ };
1628
+ return labels[p] ?? "Git";
1629
+ }
1630
+ function gitCliTool(profile) {
1631
+ const p = profile.git.provider;
1632
+ if (!p || p === "other") return "git";
1633
+ return GIT_PLATFORM_META[p]?.cli ?? "git";
1634
+ }
1635
+ function prTerminology(profile) {
1636
+ const p = profile.git.provider;
1637
+ if (!p || p === "other") return "PR/MR";
1638
+ return GIT_PLATFORM_META[p]?.prTerm ?? "PR/MR";
1639
+ }
1640
+ function pmToolsList(profile) {
1641
+ const tools = profile.services.projectManagement;
1642
+ if (!tools || tools.length === 0) return "";
1643
+ return tools.map((t) => PM_TOOL_LABELS[t]).join(", ");
1644
+ }
1645
+ function pmRoutineLine(profile) {
1646
+ const label = pmToolsList(profile);
1647
+ if (!label) return "";
1648
+ return `- Update tasks in ${label} (if applicable)`;
1649
+ }
1650
+ function ciLabel(profile) {
1651
+ const labels = {
1652
+ "github-actions": "GitHub Actions",
1653
+ "gitlab-ci": "GitLab CI",
1654
+ "azure-pipelines": "Azure Pipelines",
1655
+ "bitbucket-pipelines": "Bitbucket Pipelines",
1656
+ jenkins: "Jenkins",
1657
+ circleci: "CircleCI",
1658
+ travis: "Travis CI"
1659
+ };
1660
+ return labels[profile.infra.ci] ?? "";
1661
+ }
1662
+ function devopsFlowSummary(profile) {
1663
+ const parts = [];
1664
+ const provider = gitProviderLabel(profile);
1665
+ parts.push(`Git: ${provider}`);
1666
+ if (profile.git.workflow !== "unknown") {
1667
+ const workflowLabels = {
1668
+ "trunk-based": "trunk-based",
1669
+ "feature-pr": `feature branch \u2192 ${prTerminology(profile)} \u2192 main`,
1670
+ gitflow: "gitflow (develop/release/main)",
1671
+ "homolog-prod": "staging \u2192 production"
1672
+ };
1673
+ parts.push(`Workflow: ${workflowLabels[profile.git.workflow] ?? profile.git.workflow}`);
1674
+ }
1675
+ const ci = ciLabel(profile);
1676
+ if (ci) parts.push(`CI/CD: ${ci}`);
1677
+ const pm = pmToolsList(profile);
1678
+ if (pm) parts.push(`Project mgmt: ${pm}`);
1679
+ return parts.join(" | ");
1680
+ }
1681
+
1682
+ // src/generator/agents-md.ts
1683
+ async function generateAgentsMd(profile) {
1684
+ const target = path11.join(profile.rootDir, "AGENTS.md");
1685
+ const flow = devopsFlowSummary(profile);
1686
+ const prTerm = prTerminology(profile);
1687
+ const content = `# AGENTS.md
1688
+
1689
+ Project configured with Agent Kit v3.
1690
+
1691
+ ## Detected context
1692
+ - Stack: ${profile.stack.language}${profile.stack.framework ? ` (${profile.stack.framework})` : ""}
1693
+ - IDE: ${profile.ide.ide} (${profile.ide.plan})
1694
+ - ${flow}
1695
+
1696
+ ## Guidelines
1697
+ 1. Prefer small, verifiable changes.
1698
+ 2. Adapt response depth to user's IDE plan/model.
1699
+ 3. Use /worktree for experiments, /best-of-n for critical decisions (Cursor 3.0).
1700
+ 4. Security review before merge.
1701
+ 5. Always create a ${prTerm} \u2014 never push directly to main.
1702
+ `;
1703
+ await writeFile4(target, content, "utf8");
1704
+ }
1705
+
1706
+ // src/generator/cursor.ts
1707
+ import { writeFile as writeFile5 } from "fs/promises";
1708
+ import path12 from "path";
1709
+ function buildGitWorkflowRule(profile) {
1710
+ const prTerm = prTerminology(profile);
1711
+ const cli = gitCliTool(profile);
1712
+ const provider = gitProviderLabel(profile);
1713
+ const pm = pmToolsList(profile);
1714
+ const lines = [
1715
+ "# Git Workflow",
1716
+ `- Platform: ${provider} | CLI: \`${cli}\``,
1717
+ "- Use Conventional Commits (feat:, fix:, docs:, refactor:, chore:, perf:, test:).",
1718
+ "- For risky changes, prefer /worktree (isolated git worktree).",
1719
+ "- For comparing approaches, use /best-of-n."
1720
+ ];
1721
+ if (profile.git.workflow === "homolog-prod") {
1722
+ lines.push(
1723
+ "- Flow: development \u2192 staging \u2192 production.",
1724
+ `- Create ${prTerm} targeting the staging branch; merge to main only via promotion.`,
1725
+ "- NEVER commit directly to main."
1726
+ );
1727
+ } else if (profile.git.workflow === "feature-pr") {
1728
+ lines.push(
1729
+ `- Flow: feature branch \u2192 ${prTerm} \u2192 main.`,
1730
+ `- Always create a ${prTerm} for review before merging.`
1731
+ );
1732
+ } else if (profile.git.workflow === "gitflow") {
1733
+ lines.push(
1734
+ "- Flow: feature \u2192 develop \u2192 release \u2192 main.",
1735
+ `- Create ${prTerm} targeting develop for features, main for releases.`
1736
+ );
1737
+ }
1738
+ if (pm) {
1739
+ lines.push(`- After merge: update task status in ${pm} if integration is available.`);
1740
+ }
1741
+ return `${lines.join("\n")}
1742
+ `;
1743
+ }
1744
+ function buildHandoffRule(profile) {
1745
+ const pm = pmRoutineLine(profile);
1746
+ const routines = ["- Suggest routines: git commit/push, review CHANGELOG."];
1747
+ if (pm) routines.push(`${pm}.`);
1748
+ return `# Handoff \u2014 State in File
1749
+ - After completing each task: save .cursor/HANDOFF.md with progress and next steps.
1750
+ - Update to-dos in the plan (.cursor/plans/) when completing or starting a task.
1751
+ - One HANDOFF per project \u2014 source of truth for continuity.
1752
+ ${routines.join("\n")}
1753
+ - To resume: /continue-plan in a new conversation.
1754
+ - Native features (summaries, /resume, transcripts, Agents Window) complement \u2014 not replace.
1755
+ - Context at ~60%: save handoff and suggest new conversation.
1756
+ `;
1757
+ }
1758
+ function cursorRulesByPlan(profile) {
1759
+ if (profile.ide.plan === "cursor-free") {
1760
+ return [
1761
+ {
1762
+ filename: "01-core.mdc",
1763
+ content: `# Core Rule
1764
+ - Respond directly and concisely.
1765
+ - Focus on small changes and local validation.
1766
+ - Size each task for ~50% of context window.
1767
+ `
1768
+ },
1769
+ {
1770
+ filename: "02-handoff.mdc",
1771
+ content: buildHandoffRule(profile)
1772
+ }
1773
+ ];
1774
+ }
1775
+ return [
1776
+ {
1777
+ filename: "01-core.mdc",
1778
+ content: `# Core Rule
1779
+ - Execute tasks end-to-end whenever possible.
1780
+ - Prioritize security, tests, and architectural consistency.
1781
+ - Size each task for ~50% of context window.
1782
+ `
1783
+ },
1784
+ {
1785
+ filename: "02-git-workflow.mdc",
1786
+ content: buildGitWorkflowRule(profile)
1787
+ },
1788
+ {
1789
+ filename: "03-handoff.mdc",
1790
+ content: buildHandoffRule(profile)
1791
+ },
1792
+ {
1793
+ filename: "04-ide-guide.mdc",
1794
+ content: `# Cursor Guide
1795
+ - Use Agents Window for parallelism \u2014 each agent reads HANDOFF before acting.
1796
+ - Use Await for long-running processes.
1797
+ - Transcripts and @mentions for cross-reference.
1798
+ - /worktree for risky changes.
1799
+ - /best-of-n to compare approaches.
1800
+ `
1801
+ }
1802
+ ];
1803
+ }
1804
+ async function generateCursorArtifacts(profile) {
1805
+ const rulesDir = path12.join(profile.rootDir, ".cursor", "rules");
1806
+ const skillsDir = path12.join(profile.rootDir, ".cursor", "skills", "core");
1807
+ const agentsDir = path12.join(profile.rootDir, ".cursor", "agents");
1808
+ const commandsDir = path12.join(profile.rootDir, ".cursor", "commands");
1809
+ await Promise.all([
1810
+ ensureDir(rulesDir),
1811
+ ensureDir(skillsDir),
1812
+ ensureDir(agentsDir),
1813
+ ensureDir(commandsDir)
1814
+ ]);
1815
+ const rules = cursorRulesByPlan(profile);
1816
+ await Promise.all(
1817
+ rules.map((rule) => writeFile5(path12.join(rulesDir, rule.filename), rule.content, "utf8"))
1818
+ );
1819
+ const includeAgents = profile.ide.plan !== "cursor-free";
1820
+ if (includeAgents) {
1821
+ await writeFile5(
1822
+ path12.join(agentsDir, "security-reviewer.md"),
1823
+ "# Security Reviewer\n\nFocus on auth, PII, secrets, injection, and logging.\n",
1824
+ "utf8"
1825
+ );
1826
+ }
1827
+ const ci = ciLabel(profile);
1828
+ const provider = gitProviderLabel(profile);
1829
+ const statusLines = [
1830
+ "# /agent-kit-status",
1831
+ "",
1832
+ "Show current profile and active components.",
1833
+ "",
1834
+ "## DevOps Flow",
1835
+ `- Git: ${provider} (${profile.git.workflow})`
1836
+ ];
1837
+ if (ci) statusLines.push(`- CI/CD: ${ci}`);
1838
+ const pm = pmToolsList(profile);
1839
+ if (pm) statusLines.push(`- Project management: ${pm}`);
1840
+ statusLines.push("");
1841
+ await writeFile5(
1842
+ path12.join(commandsDir, "agent-kit-status.md"),
1843
+ `${statusLines.join("\n")}
1844
+ `,
1845
+ "utf8"
1846
+ );
1847
+ }
1848
+
1849
+ // src/generator/git-hooks.ts
1850
+ import { chmod, writeFile as writeFile6 } from "fs/promises";
1851
+ import path13 from "path";
1852
+ async function generateGitHooks(profile) {
1853
+ if (!profile.installHooks) return;
1854
+ const hooksDir = path13.join(profile.rootDir, ".git", "hooks");
1855
+ await ensureDir(hooksDir);
1856
+ const preCommitPath = path13.join(hooksDir, "pre-commit");
1857
+ const preCommit = `#!/usr/bin/env bash
1858
+ set -euo pipefail
1859
+
1860
+ if command -v rg >/dev/null 2>&1; then
1861
+ rg -n --hidden --glob '!node_modules/**' '(AKIA|BEGIN PRIVATE KEY|xoxb-)' . && {
1862
+ echo "Potential secret detected. Commit blocked."
1863
+ exit 1
1864
+ } || true
1865
+ fi
1866
+ `;
1867
+ await writeFile6(preCommitPath, preCommit, "utf8");
1868
+ await chmod(preCommitPath, 493);
1869
+ }
1870
+
1871
+ // src/generator/vscode.ts
1872
+ import { writeFile as writeFile7 } from "fs/promises";
1873
+ import path14 from "path";
1874
+ async function generateVSCodeArtifacts(profile) {
1875
+ const vscodeDir = path14.join(profile.rootDir, ".vscode");
1876
+ const githubDir = path14.join(profile.rootDir, ".github");
1877
+ await Promise.all([ensureDir(vscodeDir), ensureDir(githubDir)]);
1878
+ await writeFile7(
1879
+ path14.join(vscodeDir, "settings.json"),
1880
+ `${JSON.stringify(
1881
+ {
1882
+ "editor.formatOnSave": true,
1883
+ "editor.codeActionsOnSave": {
1884
+ "source.fixAll": "explicit"
1885
+ },
1886
+ "files.autoSave": "afterDelay"
1887
+ },
1888
+ null,
1889
+ 2
1890
+ )}
1891
+ `,
1892
+ "utf8"
1893
+ );
1894
+ const provider = gitProviderLabel(profile);
1895
+ const prTerm = prTerminology(profile);
1896
+ await writeFile7(
1897
+ path14.join(githubDir, "copilot-instructions.md"),
1898
+ `# Copilot Instructions
1899
+
1900
+ - Keep code changes small and testable.
1901
+ - Use Conventional Commits (feat:, fix:, docs:, etc.).
1902
+ - Prefer security-safe defaults.
1903
+ - Git platform: ${provider}. Always create a ${prTerm} for review.
1904
+ `,
1905
+ "utf8"
1906
+ );
1907
+ if (profile.ide.plan === "vscode-pro") {
1908
+ await writeFile7(
1909
+ path14.join(vscodeDir, "security-review.agent.md"),
1910
+ "# Security Review Agent\n\nSpecialized mode for security review.\n",
1911
+ "utf8"
1912
+ );
1913
+ }
1914
+ }
1915
+
1916
+ // src/generator/windsurf.ts
1917
+ import { writeFile as writeFile8 } from "fs/promises";
1918
+ import path15 from "path";
1919
+ async function generateWindsurfArtifacts(profile) {
1920
+ const provider = gitProviderLabel(profile);
1921
+ const prTerm = prTerminology(profile);
1922
+ const content = `# Windsurf Rules
1923
+ - Keep instructions concise and objective.
1924
+ - Prefer small diffs and explicit validation.
1925
+ - Apply security review before merge.
1926
+ - Use Conventional Commits (feat:, fix:, docs:, etc.).
1927
+ - Git: ${provider}. Create a ${prTerm} for every change.
1928
+ `;
1929
+ await writeFile8(path15.join(profile.rootDir, ".windsurfrules"), content, "utf8");
1930
+ }
1931
+
1932
+ // src/generator/index.ts
1933
+ async function generateFromProfile(profile) {
1934
+ await generateAgentsMd(profile);
1935
+ await generateGitHooks(profile);
1936
+ if (profile.ide.ide === "cursor" || profile.ide.plan.startsWith("cursor")) {
1937
+ await generateCursorArtifacts(profile);
1938
+ }
1939
+ if (profile.ide.ide === "vscode" || profile.ide.plan.startsWith("vscode")) {
1940
+ await generateVSCodeArtifacts(profile);
1941
+ }
1942
+ if (profile.ide.ide === "windsurf" || profile.ide.plan === "windsurf") {
1943
+ await generateWindsurfArtifacts(profile);
1944
+ }
1945
+ if (profile.ide.plan === "default") {
1946
+ await Promise.all([
1947
+ generateCursorArtifacts(profile),
1948
+ generateVSCodeArtifacts(profile),
1949
+ generateWindsurfArtifacts(profile)
1950
+ ]);
1951
+ }
1952
+ const pluginDir = path16.join(profile.rootDir, ".cursor-plugin");
1953
+ await ensureDir(pluginDir);
1954
+ await writeFile9(
1955
+ path16.join(pluginDir, "plugin.json"),
1956
+ `${JSON.stringify(
1957
+ {
1958
+ name: "agent-kit",
1959
+ displayName: "Agent Kit",
1960
+ author: "agent-kit-startup",
1961
+ description: "Bootstrap de ambiente dev com IA (Cursor, VS Code, Windsurf)",
1962
+ keywords: ["agents", "context", "automation", "multi-ide"],
1963
+ license: "MIT",
1964
+ version: "3.0.0"
1965
+ },
1966
+ null,
1967
+ 2
1968
+ )}
1969
+ `,
1970
+ "utf8"
1971
+ );
1972
+ }
1973
+
1974
+ // src/scanner/scan.ts
1975
+ import path22 from "path";
1976
+
1977
+ // src/scanner/detect-git.ts
1978
+ import { execFile as execFile2 } from "child_process";
1979
+ import path17 from "path";
1980
+ import { promisify as promisify2 } from "util";
1981
+ var exec = promisify2(execFile2);
1982
+ function detectProvider(remoteUrl) {
1983
+ if (!remoteUrl) return void 0;
1984
+ if (remoteUrl.includes("github")) return "github";
1985
+ if (remoteUrl.includes("gitlab")) return "gitlab";
1986
+ if (remoteUrl.includes("bitbucket")) return "bitbucket";
1987
+ if (remoteUrl.includes("dev.azure.com") || remoteUrl.includes("visualstudio.com"))
1988
+ return "azure-devops";
1989
+ if (remoteUrl.includes("gitea") || remoteUrl.includes("codeberg")) return "gitea";
1990
+ return "other";
1991
+ }
1992
+ function inferWorkflow(currentBranch) {
1993
+ if (!currentBranch) return "unknown";
1994
+ if (currentBranch === "main" || currentBranch === "master") return "feature-pr";
1995
+ if (currentBranch.includes("develop") || currentBranch.includes("release")) return "gitflow";
1996
+ if (currentBranch.includes("staging") || currentBranch.includes("homolog")) return "homolog-prod";
1997
+ return "feature-pr";
1998
+ }
1999
+ async function runGit(args, rootDir) {
2000
+ try {
2001
+ const { stdout } = await exec("git", args, { cwd: rootDir });
2002
+ return stdout.trim();
2003
+ } catch {
2004
+ return void 0;
2005
+ }
2006
+ }
2007
+ async function detectGit(rootDir) {
2008
+ const hasGit = await fileExists(path17.join(rootDir, ".git"));
2009
+ if (!hasGit) return { workflow: "unknown" };
2010
+ const remoteUrl = await runGit(["remote", "get-url", "origin"], rootDir);
2011
+ const currentBranch = await runGit(["branch", "--show-current"], rootDir);
2012
+ return {
2013
+ provider: detectProvider(remoteUrl),
2014
+ remoteUrl,
2015
+ currentBranch,
2016
+ workflow: inferWorkflow(currentBranch)
2017
+ };
2018
+ }
2019
+
2020
+ // src/scanner/detect-ide.ts
2021
+ import path18 from "path";
2022
+ async function detectIde(rootDir) {
2023
+ const hasCursor = await fileExists(path18.join(rootDir, ".cursor"));
2024
+ const hasVSCode = await fileExists(path18.join(rootDir, ".vscode"));
2025
+ const hasWindsurf = await fileExists(path18.join(rootDir, ".windsurfrules"));
2026
+ if (hasCursor) return { ide: "cursor", plan: "cursor-pro" };
2027
+ if (hasVSCode) return { ide: "vscode", plan: "vscode-pro" };
2028
+ if (hasWindsurf) return { ide: "windsurf", plan: "windsurf" };
2029
+ return { ide: "unknown", plan: "default" };
2030
+ }
2031
+
2032
+ // src/scanner/detect-infra.ts
2033
+ import path19 from "path";
2034
+ async function detectInfra(rootDir) {
2035
+ const docker = await fileExists(path19.join(rootDir, "Dockerfile")) || await fileExists(path19.join(rootDir, "docker-compose.yml")) || await fileExists(path19.join(rootDir, "docker-compose.yaml"));
2036
+ const kubernetes = await fileExists(path19.join(rootDir, "k8s")) || await fileExists(path19.join(rootDir, "kubernetes"));
2037
+ let ci = "none";
2038
+ for (const [platform, filePath] of Object.entries(CI_PLATFORM_FILES)) {
2039
+ if (await fileExists(path19.join(rootDir, filePath))) {
2040
+ ci = platform;
2041
+ break;
2042
+ }
2043
+ }
2044
+ return { docker, kubernetes, ci };
2045
+ }
2046
+
2047
+ // src/scanner/detect-services.ts
2048
+ import { readFile as readFile7 } from "fs/promises";
2049
+ import path20 from "path";
2050
+ async function detectProjectManagement(rootDir) {
2051
+ const tools = [];
2052
+ const mcpConfigPaths = [
2053
+ path20.join(rootDir, ".cursor", "mcp.json"),
2054
+ path20.join(rootDir, "mcp.json")
2055
+ ];
2056
+ for (const configPath of mcpConfigPaths) {
2057
+ if (!await fileExists(configPath)) continue;
2058
+ try {
2059
+ const raw = await readFile7(configPath, "utf8");
2060
+ const lower = raw.toLowerCase();
2061
+ if (lower.includes("clickup")) tools.push("clickup");
2062
+ if (lower.includes("jira") || lower.includes("atlassian")) tools.push("jira");
2063
+ if (lower.includes("linear")) tools.push("linear");
2064
+ if (lower.includes("asana")) tools.push("asana");
2065
+ if (lower.includes("youtrack")) tools.push("youtrack");
2066
+ if (lower.includes("shortcut")) tools.push("shortcut");
2067
+ } catch {
2068
+ }
2069
+ }
2070
+ if (await fileExists(path20.join(rootDir, ".github", "ISSUE_TEMPLATE"))) {
2071
+ tools.push("github-issues");
2072
+ }
2073
+ if (await fileExists(path20.join(rootDir, ".github", "projects"))) {
2074
+ tools.push("github-projects");
2075
+ }
2076
+ return [...new Set(tools)];
2077
+ }
2078
+ async function detectServices(rootDir) {
2079
+ const hasPrisma = await fileExists(path20.join(rootDir, "prisma/schema.prisma"));
2080
+ const hasSequelize = await fileExists(path20.join(rootDir, "sequelize"));
2081
+ const hasDrizzle = await fileExists(path20.join(rootDir, "drizzle.config.ts"));
2082
+ const hasKnex = await fileExists(path20.join(rootDir, "knexfile.ts"));
2083
+ const hasTypeorm = await fileExists(path20.join(rootDir, "ormconfig.json"));
2084
+ const database = hasPrisma || hasSequelize || hasDrizzle || hasKnex || hasTypeorm ? "postgresql" : void 0;
2085
+ const orm = hasPrisma ? "prisma" : hasDrizzle ? "drizzle" : hasSequelize ? "sequelize" : hasKnex ? "knex" : hasTypeorm ? "typeorm" : void 0;
2086
+ const projectManagement = await detectProjectManagement(rootDir);
2087
+ return {
2088
+ database,
2089
+ orm,
2090
+ projectManagement: projectManagement.length > 0 ? projectManagement : void 0
2091
+ };
2092
+ }
2093
+
2094
+ // src/scanner/detect-stack.ts
2095
+ import path21 from "path";
2096
+ var PROJECT_MARKERS = [
2097
+ "package.json",
2098
+ "requirements.txt",
2099
+ "pyproject.toml",
2100
+ "go.mod",
2101
+ "Gemfile",
2102
+ "pom.xml",
2103
+ "build.gradle",
2104
+ "composer.json",
2105
+ "Cargo.toml"
2106
+ ];
2107
+ async function detectStack(rootDir) {
2108
+ const hasAnyProjectMarker = (await Promise.all(PROJECT_MARKERS.map((item) => fileExists(path21.join(rootDir, item))))).some(Boolean);
2109
+ const hasPackageJson = await fileExists(path21.join(rootDir, "package.json"));
2110
+ if (hasPackageJson) {
2111
+ const hasNextConfig = await fileExists(path21.join(rootDir, "next.config.js")) || await fileExists(path21.join(rootDir, "next.config.mjs")) || await fileExists(path21.join(rootDir, "next.config.ts"));
2112
+ const hasNestConfig = await fileExists(path21.join(rootDir, "nest-cli.json"));
2113
+ return {
2114
+ language: "node",
2115
+ framework: hasNextConfig ? "nextjs" : hasNestConfig ? "nestjs" : "node",
2116
+ packageManager: "pnpm",
2117
+ hasProjectFiles: hasAnyProjectMarker
2118
+ };
2119
+ }
2120
+ if (await fileExists(path21.join(rootDir, "pyproject.toml"))) {
2121
+ return { language: "python", framework: "python", hasProjectFiles: hasAnyProjectMarker };
2122
+ }
2123
+ if (await fileExists(path21.join(rootDir, "go.mod"))) {
2124
+ return { language: "go", framework: "go", hasProjectFiles: hasAnyProjectMarker };
2125
+ }
2126
+ if (await fileExists(path21.join(rootDir, "Cargo.toml"))) {
2127
+ return { language: "rust", framework: "rust", hasProjectFiles: hasAnyProjectMarker };
2128
+ }
2129
+ if (await fileExists(path21.join(rootDir, "composer.json"))) {
2130
+ return { language: "php", framework: "php", hasProjectFiles: hasAnyProjectMarker };
2131
+ }
2132
+ return { language: "unknown", hasProjectFiles: hasAnyProjectMarker };
2133
+ }
2134
+
2135
+ // src/scanner/scan.ts
2136
+ var GREENFIELD_SAFE_FILES = /* @__PURE__ */ new Set([
2137
+ ".git",
2138
+ ".gitignore",
2139
+ "LICENSE",
2140
+ "README.md",
2141
+ ".cursor",
2142
+ ".vscode"
2143
+ ]);
2144
+ function isGreenfieldByEntries(entries) {
2145
+ const meaningful = entries.filter((entry) => !GREENFIELD_SAFE_FILES.has(entry));
2146
+ return meaningful.length === 0;
2147
+ }
2148
+ async function runScanner(rootDir) {
2149
+ const normalizedRoot = path22.resolve(rootDir);
2150
+ const entries = await listDirectory(normalizedRoot);
2151
+ const stack = await detectStack(normalizedRoot);
2152
+ const isGreenfield = isGreenfieldByEntries(entries) || !stack.hasProjectFiles;
2153
+ return {
2154
+ rootDir: normalizedRoot,
2155
+ isGreenfield,
2156
+ stack,
2157
+ git: await detectGit(normalizedRoot),
2158
+ ide: await detectIde(normalizedRoot),
2159
+ infra: await detectInfra(normalizedRoot),
2160
+ services: await detectServices(normalizedRoot)
2161
+ };
2162
+ }
2163
+
2164
+ // src/utils/prompts.ts
2165
+ import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
2166
+ function ensureNotCancelled(value) {
2167
+ if (isCancel(value)) {
2168
+ cancel("Operation cancelled.");
2169
+ process.exit(0);
2170
+ }
2171
+ return value;
2172
+ }
2173
+ async function askIdeAndPlan(current) {
2174
+ const ide = ensureNotCancelled(
2175
+ await select({
2176
+ message: "Which is your main IDE?",
2177
+ initialValue: current.ide === "unknown" ? void 0 : current.ide,
2178
+ options: [
2179
+ { label: "Cursor", value: "cursor" },
2180
+ { label: "VS Code", value: "vscode" },
2181
+ { label: "Windsurf", value: "windsurf" },
2182
+ { label: "Other", value: "other" }
2183
+ ]
2184
+ })
2185
+ );
2186
+ const plan = ensureNotCancelled(
2187
+ await select({
2188
+ message: "What's your IDE plan?",
2189
+ initialValue: current.plan === "default" ? void 0 : current.plan,
2190
+ options: [
2191
+ { label: "Cursor Free", value: "cursor-free" },
2192
+ { label: "Cursor Pro / Business", value: "cursor-pro" },
2193
+ { label: "VS Code + Copilot Free", value: "vscode-free" },
2194
+ { label: "VS Code + Copilot Pro / Business", value: "vscode-pro" },
2195
+ { label: "Windsurf", value: "windsurf" },
2196
+ { label: "Don't know / default", value: "default" }
2197
+ ]
2198
+ })
2199
+ );
2200
+ return { ide, plan };
2201
+ }
2202
+ async function askGitWorkflow(current) {
2203
+ return ensureNotCancelled(
2204
+ await select({
2205
+ message: "What's your Git workflow?",
2206
+ initialValue: current === "unknown" ? void 0 : current,
2207
+ options: [
2208
+ { label: "trunk-based (direct to main)", value: "trunk-based" },
2209
+ { label: "feature branch -> PR/MR -> main", value: "feature-pr" },
2210
+ { label: "gitflow (develop/release/main)", value: "gitflow" },
2211
+ { label: "staging -> production (staging -> main)", value: "homolog-prod" }
2212
+ ]
2213
+ })
2214
+ );
2215
+ }
2216
+ async function askProjectManagement(existing = []) {
2217
+ return ensureNotCancelled(
2218
+ await multiselect({
2219
+ message: "Do you use any project management system?",
2220
+ initialValues: existing,
2221
+ options: [
2222
+ { label: "GitHub Issues / Projects", value: "github-issues" },
2223
+ { label: "Jira", value: "jira" },
2224
+ { label: "Linear", value: "linear" },
2225
+ { label: "ClickUp", value: "clickup" },
2226
+ { label: "Azure Boards", value: "azure-boards" },
2227
+ { label: "Asana", value: "asana" },
2228
+ { label: "Trello", value: "trello" },
2229
+ { label: "Shortcut", value: "shortcut" },
2230
+ { label: "Notion", value: "notion" },
2231
+ { label: "YouTrack", value: "youtrack" }
2232
+ ],
2233
+ required: false
2234
+ })
2235
+ );
2236
+ }
2237
+ async function runExistingProjectWizard(scan) {
2238
+ const ide = await askIdeAndPlan(scan.ide);
2239
+ const workflow = await askGitWorkflow(scan.git.workflow);
2240
+ const projectManagement = await askProjectManagement(scan.services.projectManagement ?? []);
2241
+ const installHooks = ensureNotCancelled(
2242
+ await confirm({
2243
+ message: "Install git hooks? (pre-commit: secrets + lint)",
2244
+ initialValue: true
2245
+ })
2246
+ );
2247
+ return {
2248
+ rootDir: scan.rootDir,
2249
+ stack: scan.stack,
2250
+ git: { ...scan.git, workflow },
2251
+ ide,
2252
+ infra: scan.infra,
2253
+ services: {
2254
+ ...scan.services,
2255
+ projectManagement: projectManagement.length > 0 ? projectManagement : void 0
2256
+ },
2257
+ installHooks,
2258
+ selectedCoreComponents: [
2259
+ "git-workflow",
2260
+ "security-review",
2261
+ "clean-code",
2262
+ "docs-repo",
2263
+ "ide-guide"
2264
+ ]
2265
+ };
2266
+ }
2267
+ async function runGreenfieldWizard(scan) {
2268
+ const language = ensureNotCancelled(
2269
+ await select({
2270
+ message: "What will be the main stack?",
2271
+ options: [
2272
+ { label: "Node.js (JavaScript/TypeScript)", value: "node" },
2273
+ { label: "Python", value: "python" },
2274
+ { label: "Go", value: "go" },
2275
+ { label: "Ruby", value: "ruby" },
2276
+ { label: "Java / Kotlin", value: "java" },
2277
+ { label: "PHP", value: "php" },
2278
+ { label: "Rust", value: "rust" },
2279
+ { label: "C# / .NET", value: "dotnet" },
2280
+ { label: "Other", value: "other" }
2281
+ ]
2282
+ })
2283
+ );
2284
+ const framework = ensureNotCancelled(
2285
+ await text({
2286
+ message: "Main framework? (ex: nextjs, nestjs, django, rails, spring, none)",
2287
+ placeholder: "none",
2288
+ initialValue: "none",
2289
+ validate(value) {
2290
+ return value.trim().length === 0 ? "Enter a framework or 'none'" : void 0;
2291
+ }
2292
+ })
2293
+ );
2294
+ const database = ensureNotCancelled(
2295
+ await select({
2296
+ message: "Will you use a database?",
2297
+ options: [
2298
+ { label: "PostgreSQL", value: "postgresql" },
2299
+ { label: "MySQL", value: "mysql" },
2300
+ { label: "MongoDB", value: "mongodb" },
2301
+ { label: "SQLite", value: "sqlite" },
2302
+ { label: "SQL Server", value: "sqlserver" },
2303
+ { label: "None for now", value: "none" }
2304
+ ]
2305
+ })
2306
+ );
2307
+ const orm = ensureNotCancelled(
2308
+ await text({
2309
+ message: "ORM / query builder?",
2310
+ placeholder: "prisma, drizzle, sequelize, typeorm, none",
2311
+ initialValue: "none"
2312
+ })
2313
+ );
2314
+ const ide = await askIdeAndPlan(scan.ide);
2315
+ const workflow = await askGitWorkflow(scan.git.workflow);
2316
+ const projectManagement = await askProjectManagement();
2317
+ const installHooks = ensureNotCancelled(
2318
+ await confirm({
2319
+ message: "Install git hooks? (pre-commit: secrets + lint)",
2320
+ initialValue: true
2321
+ })
2322
+ );
2323
+ return {
2324
+ rootDir: scan.rootDir,
2325
+ stack: {
2326
+ language,
2327
+ framework: framework === "none" ? void 0 : framework,
2328
+ hasProjectFiles: false
2329
+ },
2330
+ git: {
2331
+ ...scan.git,
2332
+ workflow
2333
+ },
2334
+ ide,
2335
+ infra: scan.infra,
2336
+ services: {
2337
+ database: database === "none" ? void 0 : database,
2338
+ orm: orm === "none" ? void 0 : orm,
2339
+ projectManagement: projectManagement.length > 0 ? projectManagement : void 0
2340
+ },
2341
+ installHooks,
2342
+ selectedCoreComponents: [
2343
+ "git-workflow",
2344
+ "security-review",
2345
+ "clean-code",
2346
+ "docs-repo",
2347
+ "ide-guide"
2348
+ ]
2349
+ };
2350
+ }
2351
+
2352
+ // src/commands/init.ts
2353
+ var initCommand = defineCommand5({
2354
+ meta: {
2355
+ name: "init",
2356
+ description: "Run scanner + wizard + generation setup."
2357
+ },
2358
+ args: {
2359
+ cwd: {
2360
+ type: "string",
2361
+ description: "Project root directory",
2362
+ default: process.cwd()
2363
+ }
2364
+ },
2365
+ async run({ args }) {
2366
+ intro("agent-kit v3.0.0");
2367
+ const scan = await runScanner(args.cwd);
2368
+ logger.info(`Root: ${scan.rootDir}`);
2369
+ logger.info(
2370
+ scan.isGreenfield ? "Empty repo detected (greenfield)." : "Existing project detected."
2371
+ );
2372
+ const profile = scan.isGreenfield ? await runGreenfieldWizard(scan) : await runExistingProjectWizard(scan);
2373
+ const configPath = path23.join(scan.rootDir, ".cursor", "agent-kit.config.json");
2374
+ await writeJson(configPath, profile);
2375
+ logger.success(`Profile saved in ${configPath}`);
2376
+ await generateFromProfile(profile);
2377
+ try {
2378
+ const registry = await resolveRegistryRoot({ cwd: scan.rootDir });
2379
+ const stats = await installSkillsByIds(
2380
+ registry.root,
2381
+ scan.rootDir,
2382
+ profile.selectedCoreComponents
2383
+ );
2384
+ if (stats.written.length > 0) {
2385
+ logger.success(`Skills installed: ${stats.written.join(", ")}`);
2386
+ } else if (stats.missing.length > 0) {
2387
+ logger.warn(`Skills not found in registry: ${stats.missing.join(", ")}`);
2388
+ } else {
2389
+ logger.info("No new skills to copy (already installed or empty selection).");
2390
+ }
2391
+ } catch {
2392
+ logger.info(
2393
+ "Registry unavailable in this directory: use agent-kit install / agent-kit add <id> with --registry or remote cache."
2394
+ );
2395
+ }
2396
+ outro("Setup completed.");
2397
+ }
2398
+ });
2399
+
2400
+ // src/commands/install.ts
2401
+ import { defineCommand as defineCommand6 } from "citty";
2402
+
2403
+ // src/lifecycle/sync.ts
2404
+ async function installL0(registryRoot, projectRoot, protectedGlobs) {
2405
+ const stats = emptyStats();
2406
+ for (const artifact of L0_ARTIFACTS) {
2407
+ const outcome = await copyRegistryFile(
2408
+ registryRoot,
2409
+ projectRoot,
2410
+ artifact.source,
2411
+ artifact.target,
2412
+ protectedGlobs
2413
+ );
2414
+ recordOutcome(stats, artifact.target, outcome);
2415
+ }
2416
+ return stats;
2417
+ }
2418
+ async function syncFromManifest(registryRoot, projectRoot, manifest) {
2419
+ const protectedGlobs = resolveProtectedGlobs(manifest);
2420
+ const stats = emptyStats();
2421
+ mergeStats(stats, await installL0(registryRoot, projectRoot, protectedGlobs));
2422
+ for (const packId of manifest.packs ?? []) {
2423
+ mergeStats(stats, await installPack(registryRoot, projectRoot, packId, { protectedGlobs }));
2424
+ }
2425
+ const skillIds = manifest.skills ?? [];
2426
+ if (skillIds.length > 0) {
2427
+ await loadRegistry(registryRoot);
2428
+ mergeStats(
2429
+ stats,
2430
+ await installSkillsByIds(registryRoot, projectRoot, skillIds, { protectedGlobs })
2431
+ );
2432
+ }
2433
+ return stats;
2434
+ }
2435
+
2436
+ // src/commands/install.ts
2437
+ function parsePackList(raw) {
2438
+ if (!raw?.trim()) return [];
2439
+ return [
2440
+ ...new Set(
2441
+ raw.split(",").map((s) => s.trim()).filter(Boolean)
2442
+ )
2443
+ ];
2444
+ }
2445
+ var installCommand = defineCommand6({
2446
+ meta: {
2447
+ name: "install",
2448
+ description: "Bootstrap L0 (+ optional packs) from the registry and write agent-kit.json."
2449
+ },
2450
+ args: {
2451
+ profile: {
2452
+ type: "positional",
2453
+ description: "Install profile name stored in the manifest (default: default)",
2454
+ required: false
2455
+ },
2456
+ pack: {
2457
+ type: "string",
2458
+ description: `Comma-separated L1 pack ids (e.g. cybersec,devops). Known: ${DOMAIN_PACK_IDS.join(", ")}`
2459
+ },
2460
+ cwd: {
2461
+ type: "string",
2462
+ default: process.cwd()
2463
+ },
2464
+ ...REGISTRY_CLI_ARGS
2465
+ },
2466
+ async run({ args }) {
2467
+ const packs = parsePackList(args.pack);
2468
+ for (const id of packs) {
2469
+ if (!DOMAIN_PACK_IDS.includes(id)) {
2470
+ logger.warn(`Pack '${id}' is not in the known L1 list \u2014 will still try registry.`);
2471
+ }
2472
+ }
2473
+ const existing = await loadAgentKitManifest(args.cwd);
2474
+ const registry = await resolveRegistryFromCli({
2475
+ cwd: args.cwd,
2476
+ registry: args.registry,
2477
+ url: args.url,
2478
+ ref: args.ref,
2479
+ refresh: args.refresh,
2480
+ manifest: existing
2481
+ });
2482
+ logger.info(`Registry: ${registry.root} (${registry.source})`);
2483
+ const draft = buildManifest({
2484
+ version: KIT_VERSION,
2485
+ profile: args.profile ?? existing?.profile ?? "default",
2486
+ packs: packs.length > 0 ? packs : existing?.packs,
2487
+ skills: existing?.skills,
2488
+ protected: existing?.protected,
2489
+ registryUrl: registry.url ?? existing?.registry?.url,
2490
+ registryRef: registry.ref ?? existing?.registry?.ref
2491
+ });
2492
+ let stats;
2493
+ if ((draft.packs?.length ?? 0) > 0 || (draft.skills?.length ?? 0) > 0) {
2494
+ stats = await syncFromManifest(registry.root, args.cwd, draft);
2495
+ } else {
2496
+ const protectedGlobs = resolveProtectedGlobs(draft);
2497
+ stats = await installL0(registry.root, args.cwd, protectedGlobs);
2498
+ }
2499
+ const manifestPath = await saveManifest(args.cwd, draft);
2500
+ logApplyStats(stats);
2501
+ logger.success(`Manifest written: ${manifestPath}`);
2502
+ }
2503
+ });
2504
+
2505
+ // src/commands/run-plan.ts
2506
+ import path27 from "path";
2507
+ import { defineCommand as defineCommand7 } from "citty";
2508
+
2509
+ // src/plan-loop/backends.ts
2510
+ import { execFileSync, spawn as spawn2 } from "child_process";
2511
+ import { createWriteStream } from "fs";
2512
+ async function which(bin) {
2513
+ try {
2514
+ const out = execFileSync("which", [bin], { encoding: "utf8" }).trim();
2515
+ return out || null;
2516
+ } catch {
2517
+ return null;
2518
+ }
2519
+ }
2520
+ function spawnLogged(command, args, logPath) {
2521
+ return new Promise((resolve, reject) => {
2522
+ const out = createWriteStream(logPath, { flags: "w" });
2523
+ const child = spawn2(command, args, {
2524
+ stdio: ["ignore", "pipe", "pipe"]
2525
+ });
2526
+ const onData = (chunk) => {
2527
+ process.stdout.write(chunk);
2528
+ out.write(chunk);
2529
+ };
2530
+ child.stdout?.on("data", onData);
2531
+ child.stderr?.on("data", onData);
2532
+ child.on("error", (err) => {
2533
+ out.end();
2534
+ reject(err);
2535
+ });
2536
+ child.on("close", (code) => {
2537
+ out.end(() => resolve({ exitCode: code ?? 1 }));
2538
+ });
2539
+ });
2540
+ }
2541
+ var cursorAgentBackend = {
2542
+ id: "cursor-agent",
2543
+ async resolve() {
2544
+ return which("cursor-agent");
2545
+ },
2546
+ async run(opts) {
2547
+ const args = [
2548
+ "-p",
2549
+ "--force",
2550
+ "--sandbox",
2551
+ "disabled",
2552
+ "--output-format",
2553
+ "stream-json",
2554
+ "--workspace",
2555
+ opts.workspace
2556
+ ];
2557
+ if (opts.model) {
2558
+ args.push("--model", opts.model);
2559
+ }
2560
+ args.push(opts.prompt);
2561
+ return spawnLogged("cursor-agent", args, opts.logPath);
2562
+ }
2563
+ };
2564
+ var claudeBackend = {
2565
+ id: "claude",
2566
+ async resolve() {
2567
+ return which("claude");
2568
+ },
2569
+ async run(_opts) {
2570
+ throw new Error(
2571
+ "Backend 'claude' is reserved but not implemented yet. Use --backend cursor-agent."
2572
+ );
2573
+ }
2574
+ };
2575
+ var BACKENDS = {
2576
+ "cursor-agent": cursorAgentBackend,
2577
+ claude: claudeBackend
2578
+ };
2579
+ function getBackend(id) {
2580
+ const backend = BACKENDS[id];
2581
+ if (!backend) {
2582
+ throw new Error(`Unknown backend '${id}'. Supported: ${Object.keys(BACKENDS).join(", ")}`);
2583
+ }
2584
+ return backend;
2585
+ }
2586
+ function listBackendIds() {
2587
+ return Object.keys(BACKENDS);
2588
+ }
2589
+
2590
+ // src/plan-loop/run-loop.ts
2591
+ import { mkdir as mkdir4, readFile as readFile10, rm, unlink } from "fs/promises";
2592
+ import path26 from "path";
2593
+
2594
+ // src/plan-loop/external-review.ts
2595
+ import { spawn as spawn3 } from "child_process";
2596
+ import path24 from "path";
2597
+ var SCRIPT_REL = path24.join("scripts", "plan-external-review.sh");
2598
+ function isPlanExhaustedReason(reason) {
2599
+ const r = reason.trim().toLowerCase();
2600
+ if (!r) return false;
2601
+ if (r.includes("plan exhausted")) return true;
2602
+ if (r.includes("nothing to do")) return true;
2603
+ if (r.includes("no implementable")) return true;
2604
+ if (/\ball\s+(implementable\s+)?to-?dos?\b/.test(r) && (r.includes("completed") || r.includes("done") || r.includes("exhausted"))) {
2605
+ return true;
2606
+ }
2607
+ return false;
2608
+ }
2609
+ function shouldArmExternalPlanReview(input) {
2610
+ if (input.pending === 0) return true;
2611
+ if (input.stopReason && isPlanExhaustedReason(input.stopReason)) return true;
2612
+ return false;
2613
+ }
2614
+ async function armExternalPlanReview(root, deps = {}) {
2615
+ const spawnFn = deps.spawnFn ?? spawn3;
2616
+ const existsFn = deps.existsFn ?? fileExists;
2617
+ const log = deps.log ?? ((line) => console.log(line));
2618
+ const scriptPath = path24.join(root, SCRIPT_REL);
2619
+ if (!await existsFn(scriptPath)) {
2620
+ log(
2621
+ `tip: ${SCRIPT_REL} missing. Manual: /plan-external-review (enable externalPlanReview in .cursor/context/config.json).`
2622
+ );
2623
+ return { invoked: false, exitCode: null, output: "" };
2624
+ }
2625
+ log("Plan exhausted: arming optional external plan review (opt-in handled by script)...");
2626
+ return new Promise((resolve) => {
2627
+ const child = spawnFn("bash", [scriptPath], {
2628
+ cwd: root,
2629
+ env: process.env
2630
+ });
2631
+ let output = "";
2632
+ const append = (chunk) => {
2633
+ const s = String(chunk);
2634
+ output += s;
2635
+ process.stdout.write(s);
2636
+ };
2637
+ child.stdout?.on("data", append);
2638
+ child.stderr?.on("data", (chunk) => {
2639
+ const s = String(chunk);
2640
+ output += s;
2641
+ process.stderr.write(s);
2642
+ });
2643
+ child.on("error", (err) => {
2644
+ log(
2645
+ `tip: external plan review launcher failed (${err.message}). Manual: ${SCRIPT_REL} or /plan-external-review`
2646
+ );
2647
+ resolve({ invoked: true, exitCode: null, output });
2648
+ });
2649
+ child.on("close", (code) => {
2650
+ if (code !== 0 && code !== null) {
2651
+ log(
2652
+ `tip: external plan review exited ${code} (ignored; does not fail the loop). Manual: ${SCRIPT_REL} or /plan-external-review`
2653
+ );
2654
+ }
2655
+ resolve({ invoked: true, exitCode: code, output });
2656
+ });
2657
+ });
2658
+ }
2659
+
2660
+ // src/plan-loop/plan-state.ts
2661
+ import { readFile as readFile8, readdir as readdir3 } from "fs/promises";
2662
+ import path25 from "path";
2663
+ function countPendingTodos(raw) {
2664
+ const lines = raw.split(/\r?\n/);
2665
+ let inFront = 0;
2666
+ let cont = false;
2667
+ let n = 0;
2668
+ for (const line of lines) {
2669
+ if (line === "---") {
2670
+ inFront += 1;
2671
+ continue;
2672
+ }
2673
+ if (inFront !== 1) continue;
2674
+ const idMatch = line.match(/^- id:\s*(\S+)/);
2675
+ if (idMatch?.[1]) {
2676
+ cont = idMatch[1] === "dogfood-poc";
2677
+ }
2678
+ if (/content:.*[Cc]ont[ií]nuo/.test(line)) {
2679
+ cont = true;
2680
+ }
2681
+ if (/status:\s*(pending|in_progress)\b/.test(line) && !cont) {
2682
+ n += 1;
2683
+ }
2684
+ }
2685
+ return n;
2686
+ }
2687
+ async function findActivePlanFile(plansDir) {
2688
+ if (!await fileExists(plansDir)) return null;
2689
+ const files = (await readdir3(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
2690
+ return files[0] ? path25.join(plansDir, files[0]) : null;
2691
+ }
2692
+ async function readPlan(planPath) {
2693
+ return readFile8(planPath, "utf8");
2694
+ }
2695
+
2696
+ // src/plan-loop/sentinel.ts
2697
+ import { readFile as readFile9 } from "fs/promises";
2698
+ var SENTINEL_RE = /LOOP_TICK_RESULT:\s*(continue|stop(?:\s*[—\-].*)?)/i;
2699
+ function takeFromText(text2) {
2700
+ if (!text2) return null;
2701
+ const m = SENTINEL_RE.exec(text2);
2702
+ if (!m?.[1]) return null;
2703
+ const raw = m[1].trim();
2704
+ if (raw.toLowerCase().startsWith("stop")) {
2705
+ const reason = raw.slice(4).replace(/^[—\-\s]+/, "").trim() || "requested by agent";
2706
+ return { kind: "stop", reason };
2707
+ }
2708
+ return { kind: "continue" };
2709
+ }
2710
+ function parseSentinelFromLog(content) {
2711
+ let last = null;
2712
+ for (const line of content.split(/\r?\n/)) {
2713
+ const trimmed = line.trim();
2714
+ if (!trimmed) continue;
2715
+ if (!trimmed.startsWith("{")) {
2716
+ const hit = takeFromText(trimmed);
2717
+ if (hit) last = hit;
2718
+ continue;
2719
+ }
2720
+ try {
2721
+ const ev = JSON.parse(trimmed);
2722
+ if (ev.type === "result" && typeof ev.result === "string") {
2723
+ const hit = takeFromText(ev.result);
2724
+ if (hit) last = hit;
2725
+ }
2726
+ const parts = ev.message?.content;
2727
+ if (Array.isArray(parts)) {
2728
+ for (const part of parts) {
2729
+ if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
2730
+ const hit = takeFromText(part.text);
2731
+ if (hit) last = hit;
2732
+ }
2733
+ }
2734
+ }
2735
+ } catch {
2736
+ const hit = takeFromText(trimmed);
2737
+ if (hit) last = hit;
2738
+ }
2739
+ }
2740
+ return last ?? { kind: "missing" };
2741
+ }
2742
+ async function parseSentinelFromLogFile(logPath) {
2743
+ try {
2744
+ const content = await readFile9(logPath, "utf8");
2745
+ return parseSentinelFromLog(content);
2746
+ } catch {
2747
+ return { kind: "missing" };
2748
+ }
2749
+ }
2750
+ function formatSentinelLine(sentinel) {
2751
+ if (sentinel.kind === "continue") return "LOOP_TICK_RESULT: continue";
2752
+ if (sentinel.kind === "stop") return `LOOP_TICK_RESULT: stop - ${sentinel.reason}`;
2753
+ return "";
2754
+ }
2755
+
2756
+ // src/plan-loop/run-loop.ts
2757
+ var TICK_PROMPT = '/run-plan - single tick from the headless runner (agent-kit run-plan). Read .cursor/HANDOFF.md and the active plan in .cursor/plans/. Mark the next to-do as in_progress in the frontmatter, execute ONLY that to-do, mark completed and update HANDOFF. If there is a commitable diff: run /git-staging without asking for confirmation. NEVER /git-prod. Do NOT re-arm an internal Loop skill - the external runner starts the next agent. End the response with exactly one line: "LOOP_TICK_RESULT: continue" if implementable to-dos remain, or "LOOP_TICK_RESULT: stop - <reason>" (plan exhausted, external blocker, or human decision needed).';
2758
+ function stamp() {
2759
+ const d = /* @__PURE__ */ new Date();
2760
+ const p = (n, w = 2) => String(n).padStart(w, "0");
2761
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}${p(d.getMilliseconds(), 3)}`;
2762
+ }
2763
+ function sleep(ms) {
2764
+ return new Promise((r) => setTimeout(r, ms));
2765
+ }
2766
+ async function runPlanLoop(opts) {
2767
+ const plansDir = path26.join(opts.root, ".cursor", "plans");
2768
+ const stopFile = path26.join(opts.root, ".cursor", "loop.stop");
2769
+ const logDir = path26.join(opts.root, ".cursor", "loop-logs");
2770
+ const planPath = await findActivePlanFile(plansDir);
2771
+ if (!planPath) {
2772
+ logger.error("No active plan in .cursor/plans/");
2773
+ return 1;
2774
+ }
2775
+ const pending = async () => countPendingTodos(await readPlan(planPath));
2776
+ await mkdir4(logDir, { recursive: true });
2777
+ try {
2778
+ await unlink(stopFile);
2779
+ } catch {
2780
+ }
2781
+ const onSigInt = () => {
2782
+ console.log("");
2783
+ console.log("Loop interrupted (Ctrl+C). State saved in HANDOFF by the last tick.");
2784
+ process.exit(130);
2785
+ };
2786
+ process.on("SIGINT", onSigInt);
2787
+ try {
2788
+ console.log(`Active plan: ${path26.basename(planPath)}`);
2789
+ console.log(`Pending to-dos: ${await pending()} | max ticks: ${opts.maxTicks}`);
2790
+ console.log(`Backend: ${opts.backend.id}`);
2791
+ if (opts.dryRun) {
2792
+ console.log("--dry-run: no agent will be started. Tick prompt:");
2793
+ console.log(TICK_PROMPT);
2794
+ return 0;
2795
+ }
2796
+ const binary = await opts.backend.resolve();
2797
+ if (!binary) {
2798
+ logger.error(`${opts.backend.id} not found on PATH`);
2799
+ return 1;
2800
+ }
2801
+ let tick = 0;
2802
+ let stopReason;
2803
+ let planExhausted = false;
2804
+ while (true) {
2805
+ tick += 1;
2806
+ if (tick > opts.maxTicks) {
2807
+ console.log(`Budget of ${opts.maxTicks} ticks reached - stopping. Run again to continue.`);
2808
+ break;
2809
+ }
2810
+ if (await fileExists(stopFile)) {
2811
+ console.log(`Stop file found (${stopFile}) - stopping.`);
2812
+ try {
2813
+ await rm(stopFile, { force: true });
2814
+ } catch {
2815
+ }
2816
+ break;
2817
+ }
2818
+ const before = await pending();
2819
+ if (before === 0) {
2820
+ console.log("Plan exhausted (0 pending to-dos) - nothing to do.");
2821
+ planExhausted = true;
2822
+ break;
2823
+ }
2824
+ const logPath = path26.join(logDir, `tick-${stamp()}.log`);
2825
+ const relLog = path26.relative(opts.root, logPath);
2826
+ console.log("");
2827
+ console.log(`=== tick ${tick}/${opts.maxTicks} - pending: ${before} - log: ${relLog} ===`);
2828
+ let agentExit = 0;
2829
+ try {
2830
+ const result = await opts.backend.run({
2831
+ workspace: opts.root,
2832
+ prompt: TICK_PROMPT,
2833
+ model: opts.model,
2834
+ logPath
2835
+ });
2836
+ agentExit = result.exitCode;
2837
+ } catch (err) {
2838
+ logger.error(String(err));
2839
+ return 1;
2840
+ }
2841
+ try {
2842
+ const logText = await readFile10(logPath, "utf8");
2843
+ if (logText.includes("Too many MCP tools")) {
2844
+ console.log(
2845
+ "Too many MCP tools for the headless model - disable servers (cursor-agent mcp disable <id>) and run again."
2846
+ );
2847
+ break;
2848
+ }
2849
+ } catch {
2850
+ }
2851
+ if (agentExit !== 0) {
2852
+ console.log(
2853
+ `${opts.backend.id} exited with code ${agentExit} - stopping. See log: ${logPath}`
2854
+ );
2855
+ break;
2856
+ }
2857
+ const sentinel = await parseSentinelFromLogFile(logPath);
2858
+ const after = await pending();
2859
+ if (sentinel.kind === "continue") {
2860
+ } else if (sentinel.kind === "stop") {
2861
+ console.log(`Agent requested stop: ${formatSentinelLine(sentinel)}`);
2862
+ stopReason = sentinel.reason;
2863
+ if (after === 0 || isPlanExhaustedReason(sentinel.reason)) {
2864
+ planExhausted = true;
2865
+ }
2866
+ break;
2867
+ } else if (after < before) {
2868
+ console.log(
2869
+ `warn: tick without sentinel, but progress (${before} -> ${after}) - continuing.`
2870
+ );
2871
+ } else {
2872
+ console.log(
2873
+ `Tick without sentinel and no progress (${before} -> ${after}) - stopping for safety.`
2874
+ );
2875
+ break;
2876
+ }
2877
+ if (after === 0) {
2878
+ console.log("All to-dos completed. Suggesting /git-prod stays with the human (HITL).");
2879
+ planExhausted = true;
2880
+ break;
2881
+ }
2882
+ if (opts.sleepSeconds > 0) {
2883
+ await sleep(opts.sleepSeconds * 1e3);
2884
+ }
2885
+ }
2886
+ const pendingNow = await pending();
2887
+ console.log("");
2888
+ console.log(
2889
+ `Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path26.relative(opts.root, logDir)}/`
2890
+ );
2891
+ if (planExhausted || shouldArmExternalPlanReview({ pending: pendingNow, stopReason })) {
2892
+ await armExternalPlanReview(opts.root);
2893
+ }
2894
+ return 0;
2895
+ } finally {
2896
+ process.off("SIGINT", onSigInt);
2897
+ }
2898
+ }
2899
+
2900
+ // src/commands/run-plan.ts
2901
+ var runPlanCommand = defineCommand7({
2902
+ meta: {
2903
+ name: "run-plan",
2904
+ description: "Headless continuous plan runner: one fresh agent per tick (LOOP_TICK_RESULT contract). Never git-prod."
2905
+ },
2906
+ args: {
2907
+ cwd: {
2908
+ type: "string",
2909
+ description: "Project root (default: current directory)",
2910
+ default: process.cwd()
2911
+ },
2912
+ "max-ticks": {
2913
+ type: "string",
2914
+ description: "Maximum ticks before stopping (default: 10)",
2915
+ default: "10"
2916
+ },
2917
+ model: {
2918
+ type: "string",
2919
+ description: "Optional model id passed to the agent backend",
2920
+ default: ""
2921
+ },
2922
+ sleep: {
2923
+ type: "string",
2924
+ description: "Seconds to sleep between ticks (default: 5)",
2925
+ default: "5"
2926
+ },
2927
+ backend: {
2928
+ type: "string",
2929
+ description: `Agent backend (${listBackendIds().join(" | ")}; default: cursor-agent)`,
2930
+ default: "cursor-agent"
2931
+ },
2932
+ "dry-run": {
2933
+ type: "boolean",
2934
+ description: "Print the tick prompt and exit without starting an agent",
2935
+ default: false
2936
+ }
2937
+ },
2938
+ async run({ args }) {
2939
+ const maxTicks = Number.parseInt(String(args["max-ticks"]), 10);
2940
+ const sleepSeconds = Number.parseFloat(String(args.sleep));
2941
+ if (!Number.isFinite(maxTicks) || maxTicks < 1) {
2942
+ logger.error("--max-ticks must be a positive integer");
2943
+ process.exitCode = 1;
2944
+ return;
2945
+ }
2946
+ if (!Number.isFinite(sleepSeconds) || sleepSeconds < 0) {
2947
+ logger.error("--sleep must be a non-negative number");
2948
+ process.exitCode = 1;
2949
+ return;
2950
+ }
2951
+ let backend;
2952
+ try {
2953
+ backend = getBackend(String(args.backend));
2954
+ } catch (err) {
2955
+ logger.error(String(err));
2956
+ process.exitCode = 1;
2957
+ return;
2958
+ }
2959
+ const code = await runPlanLoop({
2960
+ root: path27.resolve(args.cwd),
2961
+ maxTicks,
2962
+ sleepSeconds,
2963
+ model: args.model ? String(args.model) : void 0,
2964
+ dryRun: Boolean(args["dry-run"]),
2965
+ backend
2966
+ });
2967
+ process.exitCode = code;
2968
+ }
2969
+ });
2970
+
2971
+ // src/commands/scan.ts
2972
+ import { defineCommand as defineCommand8 } from "citty";
2973
+ var scanCommand = defineCommand8({
2974
+ meta: {
2975
+ name: "scan",
2976
+ description: "Scan the current repository and print detected profile."
2977
+ },
2978
+ args: {
2979
+ cwd: {
2980
+ type: "string",
2981
+ description: "Project root directory",
2982
+ default: process.cwd()
2983
+ }
2984
+ },
2985
+ async run({ args }) {
2986
+ const result = await runScanner(args.cwd);
2987
+ logger.info(`Scan completed for ${result.rootDir}`);
2988
+ logger.info(`Path: ${result.isGreenfield ? "greenfield" : "existing project"}`);
2989
+ console.log(JSON.stringify(result, null, 2));
2990
+ }
2991
+ });
2992
+
2993
+ // src/commands/status.ts
2994
+ import path28 from "path";
2995
+ import { defineCommand as defineCommand9 } from "citty";
2996
+ var statusCommand = defineCommand9({
2997
+ meta: {
2998
+ name: "status",
2999
+ description: "Show Agent Kit distribution status (manifest + optional wizard profile)."
3000
+ },
3001
+ args: {
3002
+ cwd: {
3003
+ type: "string",
3004
+ default: process.cwd()
3005
+ },
3006
+ json: {
3007
+ type: "boolean",
3008
+ description: "Print raw JSON for manifest and profile",
3009
+ default: false
3010
+ }
3011
+ },
3012
+ async run({ args }) {
3013
+ const manifest = await loadAgentKitManifest(args.cwd);
3014
+ const profilePath = path28.join(args.cwd, ".cursor", "agent-kit.config.json");
3015
+ const profile = await readJson(profilePath);
3016
+ if (args.json) {
3017
+ console.log(
3018
+ JSON.stringify(
3019
+ {
3020
+ manifest: manifest ?? null,
3021
+ profile: profile ?? null
3022
+ },
3023
+ null,
3024
+ 2
3025
+ )
3026
+ );
3027
+ return;
3028
+ }
3029
+ if (!manifest) {
3030
+ logger.warn(`No ${MANIFEST_RELATIVE_PATH} \u2014 run agent-kit install.`);
3031
+ } else {
3032
+ const protectedGlobs = resolveProtectedGlobs(manifest);
3033
+ console.log("Agent Kit status");
3034
+ console.log(` version: ${manifest.version}`);
3035
+ console.log(` profile: ${manifest.profile ?? "(none)"}`);
3036
+ console.log(` packs: ${(manifest.packs ?? []).join(", ") || "(none)"}`);
3037
+ console.log(` skills: ${(manifest.skills ?? []).length} listed`);
3038
+ console.log(` protected: ${protectedGlobs.length} glob(s) (L3 safe on update)`);
3039
+ console.log(
3040
+ ` registry: ${manifest.registry?.url ?? "(default)"} @ ${manifest.registry?.ref ?? "(default)"}`
3041
+ );
3042
+ if (manifest.installedAt) console.log(` installed: ${manifest.installedAt}`);
3043
+ }
3044
+ if (profile) {
3045
+ console.log("Wizard profile (agent-kit.config.json): present");
3046
+ console.log(` IDE: ${profile.ide?.ide ?? "?"}`);
3047
+ console.log(` core picks: ${(profile.selectedCoreComponents ?? []).join(", ") || "(none)"}`);
3048
+ } else {
3049
+ logger.info("No wizard profile \u2014 optional; run agent-kit init for generators.");
3050
+ }
3051
+ }
3052
+ });
3053
+
3054
+ // src/commands/update.ts
3055
+ import { defineCommand as defineCommand10 } from "citty";
3056
+ var updateCommand = defineCommand10({
3057
+ meta: {
3058
+ name: "update",
3059
+ description: "Re-apply L0/packs/skills from the registry; never overwrites L3 protected paths."
3060
+ },
3061
+ args: {
3062
+ cwd: {
3063
+ type: "string",
3064
+ default: process.cwd()
3065
+ },
3066
+ ...REGISTRY_CLI_ARGS
3067
+ },
3068
+ async run({ args }) {
3069
+ const existing = await loadAgentKitManifest(args.cwd);
3070
+ if (!existing) {
3071
+ logger.warn("No .cursor/agent-kit.json \u2014 run agent-kit install first.");
3072
+ return;
3073
+ }
3074
+ const registry = await resolveRegistryFromCli({
3075
+ cwd: args.cwd,
3076
+ registry: args.registry,
3077
+ url: args.url,
3078
+ ref: args.ref,
3079
+ refresh: args.refresh,
3080
+ manifest: existing
3081
+ });
3082
+ logger.info(`Registry: ${registry.root} (${registry.source})`);
3083
+ const next = buildManifest({
3084
+ version: KIT_VERSION,
3085
+ profile: existing.profile,
3086
+ packs: existing.packs,
3087
+ skills: existing.skills,
3088
+ protected: existing.protected,
3089
+ registryUrl: registry.url ?? existing.registry?.url,
3090
+ registryRef: registry.ref ?? existing.registry?.ref
3091
+ });
3092
+ if (existing.overrides?.length) next.overrides = existing.overrides;
3093
+ const stats = await syncFromManifest(registry.root, args.cwd, next);
3094
+ await saveManifest(args.cwd, next);
3095
+ logApplyStats(stats);
3096
+ logger.success("Update complete (L3 protected paths left untouched).");
3097
+ }
3098
+ });
3099
+
3100
+ // src/index.ts
3101
+ var main = defineCommand11({
3102
+ meta: {
3103
+ name: "agent-kit",
3104
+ description: "Developer bootstrapper for AI-assisted IDEs"
3105
+ },
3106
+ subCommands: {
3107
+ init: initCommand,
3108
+ install: installCommand,
3109
+ scan: scanCommand,
3110
+ add: addCommand,
3111
+ status: statusCommand,
3112
+ update: updateCommand,
3113
+ diff: diffCommand,
3114
+ contribute: contributeCommand,
3115
+ handoff: handoffCommand,
3116
+ "run-plan": runPlanCommand
3117
+ }
3118
+ });
3119
+ runMain(main);