agentcohort 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +187 -0
  3. package/dist/args.d.ts +13 -0
  4. package/dist/args.js +92 -0
  5. package/dist/claudeMd.d.ts +33 -0
  6. package/dist/claudeMd.js +156 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +108 -0
  9. package/dist/fileOps.d.ts +15 -0
  10. package/dist/fileOps.js +50 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.js +30 -0
  13. package/dist/installer.d.ts +32 -0
  14. package/dist/installer.js +208 -0
  15. package/dist/logger.d.ts +43 -0
  16. package/dist/logger.js +63 -0
  17. package/dist/manifest.d.ts +18 -0
  18. package/dist/manifest.js +44 -0
  19. package/dist/paths.d.ts +15 -0
  20. package/dist/paths.js +52 -0
  21. package/dist/prompt.d.ts +32 -0
  22. package/dist/prompt.js +57 -0
  23. package/dist/templates/CLAUDE.section.md +62 -0
  24. package/dist/templates/agents/bug-fixer.md +67 -0
  25. package/dist/templates/agents/bug-hunter.md +67 -0
  26. package/dist/templates/agents/expert-council.md +83 -0
  27. package/dist/templates/agents/feature-implementer.md +69 -0
  28. package/dist/templates/agents/feature-planner.md +75 -0
  29. package/dist/templates/agents/final-reviewer.md +71 -0
  30. package/dist/templates/agents/perf-optimizer.md +63 -0
  31. package/dist/templates/agents/perf-reviewer.md +66 -0
  32. package/dist/templates/agents/performance-hunter.md +68 -0
  33. package/dist/templates/agents/regression-guard.md +61 -0
  34. package/dist/templates/agents/repo-scout.md +77 -0
  35. package/dist/templates/agents/reproduction-engineer.md +65 -0
  36. package/dist/templates/agents/root-cause-analyst.md +71 -0
  37. package/dist/templates/agents/solution-architect.md +71 -0
  38. package/dist/templates/agents/test-verifier.md +68 -0
  39. package/dist/templates/commands/auto-flow.md +41 -0
  40. package/dist/templates/commands/bug-audit.md +51 -0
  41. package/dist/templates/commands/bug-fix-approved.md +44 -0
  42. package/dist/templates/commands/dev-flow.md +41 -0
  43. package/dist/templates/commands/fix-blockers.md +36 -0
  44. package/dist/templates/commands/perf-hunt.md +40 -0
  45. package/dist/templates/commands/review-diff.md +39 -0
  46. package/package.json +56 -0
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Programmatic API.
3
+ *
4
+ * `agentcohort` is primarily a CLI, but the core install logic is exported
5
+ * so it can be embedded in other tooling and exercised directly by tests.
6
+ */
7
+ export { runInit } from './installer';
8
+ export type { InitOptions, InitResult, ActionRecord, Disposition, } from './installer';
9
+ export { buildManifest } from './manifest';
10
+ export type { ManifestEntry, EntryKind } from './manifest';
11
+ export { upsertSection, hasSection, sectionMatches, buildInitialClaudeMd, SECTION_TITLE, } from './claudeMd';
12
+ export { formatBackupSuffix, backupPathFor } from './fileOps';
13
+ export { getVersion, getTemplatesDir } from './paths';
14
+ export { createLogger } from './logger';
15
+ export type { Logger } from './logger';
16
+ export { createInteractiveResolver, fixedResolver } from './prompt';
17
+ export type { ConflictResolver, ConflictDecision, Choice } from './prompt';
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fixedResolver = exports.createInteractiveResolver = exports.createLogger = exports.getTemplatesDir = exports.getVersion = exports.backupPathFor = exports.formatBackupSuffix = exports.SECTION_TITLE = exports.buildInitialClaudeMd = exports.sectionMatches = exports.hasSection = exports.upsertSection = exports.buildManifest = exports.runInit = void 0;
4
+ /**
5
+ * Programmatic API.
6
+ *
7
+ * `agentcohort` is primarily a CLI, but the core install logic is exported
8
+ * so it can be embedded in other tooling and exercised directly by tests.
9
+ */
10
+ var installer_1 = require("./installer");
11
+ Object.defineProperty(exports, "runInit", { enumerable: true, get: function () { return installer_1.runInit; } });
12
+ var manifest_1 = require("./manifest");
13
+ Object.defineProperty(exports, "buildManifest", { enumerable: true, get: function () { return manifest_1.buildManifest; } });
14
+ var claudeMd_1 = require("./claudeMd");
15
+ Object.defineProperty(exports, "upsertSection", { enumerable: true, get: function () { return claudeMd_1.upsertSection; } });
16
+ Object.defineProperty(exports, "hasSection", { enumerable: true, get: function () { return claudeMd_1.hasSection; } });
17
+ Object.defineProperty(exports, "sectionMatches", { enumerable: true, get: function () { return claudeMd_1.sectionMatches; } });
18
+ Object.defineProperty(exports, "buildInitialClaudeMd", { enumerable: true, get: function () { return claudeMd_1.buildInitialClaudeMd; } });
19
+ Object.defineProperty(exports, "SECTION_TITLE", { enumerable: true, get: function () { return claudeMd_1.SECTION_TITLE; } });
20
+ var fileOps_1 = require("./fileOps");
21
+ Object.defineProperty(exports, "formatBackupSuffix", { enumerable: true, get: function () { return fileOps_1.formatBackupSuffix; } });
22
+ Object.defineProperty(exports, "backupPathFor", { enumerable: true, get: function () { return fileOps_1.backupPathFor; } });
23
+ var paths_1 = require("./paths");
24
+ Object.defineProperty(exports, "getVersion", { enumerable: true, get: function () { return paths_1.getVersion; } });
25
+ Object.defineProperty(exports, "getTemplatesDir", { enumerable: true, get: function () { return paths_1.getTemplatesDir; } });
26
+ var logger_1 = require("./logger");
27
+ Object.defineProperty(exports, "createLogger", { enumerable: true, get: function () { return logger_1.createLogger; } });
28
+ var prompt_1 = require("./prompt");
29
+ Object.defineProperty(exports, "createInteractiveResolver", { enumerable: true, get: function () { return prompt_1.createInteractiveResolver; } });
30
+ Object.defineProperty(exports, "fixedResolver", { enumerable: true, get: function () { return prompt_1.fixedResolver; } });
@@ -0,0 +1,32 @@
1
+ import { EntryKind } from './manifest';
2
+ import type { ConflictResolver } from './prompt';
3
+ import type { Logger } from './logger';
4
+ export type Disposition = 'created' | 'overwritten' | 'appended-section' | 'replaced-section' | 'skipped' | 'unchanged';
5
+ export interface ActionRecord {
6
+ targetRelPath: string;
7
+ kind: EntryKind;
8
+ disposition: Disposition;
9
+ /** Absolute path of the backup that was (or would be) written, if any. */
10
+ backupPath?: string;
11
+ dryRun: boolean;
12
+ }
13
+ export interface InitOptions {
14
+ cwd: string;
15
+ yes: boolean;
16
+ dryRun: boolean;
17
+ force: boolean;
18
+ /** Force a backup before any destructive write, regardless of other choices. */
19
+ backup: boolean;
20
+ /** When false, conflicts are resolved by safe automatic defaults. */
21
+ interactive: boolean;
22
+ resolver?: ConflictResolver;
23
+ now?: () => Date;
24
+ logger?: Logger;
25
+ templatesDir?: string;
26
+ }
27
+ export interface InitResult {
28
+ projectRoot: string;
29
+ actions: ActionRecord[];
30
+ dryRun: boolean;
31
+ }
32
+ export declare function runInit(options: InitOptions): Promise<InitResult>;
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runInit = runInit;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const fileOps_1 = require("./fileOps");
7
+ const claudeMd_1 = require("./claudeMd");
8
+ const manifest_1 = require("./manifest");
9
+ const paths_1 = require("./paths");
10
+ /** Pick a non-clobbering backup path (never overwrite an existing backup). */
11
+ function uniqueBackupPath(target, date) {
12
+ const base = (0, fileOps_1.backupPathFor)(target, date);
13
+ if (!(0, node_fs_1.existsSync)(base))
14
+ return base;
15
+ let i = 1;
16
+ while ((0, node_fs_1.existsSync)(`${base}-${i}`))
17
+ i += 1;
18
+ return `${base}-${i}`;
19
+ }
20
+ async function runInit(options) {
21
+ const projectRoot = options.cwd;
22
+ const now = options.now ?? (() => new Date());
23
+ const log = options.logger;
24
+ const templatesDir = options.templatesDir ?? (0, paths_1.getTemplatesDir)();
25
+ const manifest = (0, manifest_1.buildManifest)(templatesDir, projectRoot);
26
+ if (manifest.length === 0) {
27
+ throw new Error('agentcohort: no templates found to install.');
28
+ }
29
+ const actions = [];
30
+ let sticky = null;
31
+ const decide = async (kind, targetRelPath) => {
32
+ // 1. --force always proceeds; backup only if explicitly requested.
33
+ if (options.force) {
34
+ return { proceed: true, backup: options.backup };
35
+ }
36
+ // 2. Non-interactive (--yes, --dry-run, or non-TTY): safe defaults.
37
+ if (!options.interactive) {
38
+ if (kind === 'claude-section') {
39
+ // Don't silently rewrite hand-editable routing rules.
40
+ return { proceed: false, backup: false };
41
+ }
42
+ // Deliver latest templates but never lose old content.
43
+ return { proceed: true, backup: true };
44
+ }
45
+ // 3. Interactive: ask (honoring a prior "apply to all" decision).
46
+ if (!options.resolver) {
47
+ throw new Error('agentcohort: interactive mode requires a resolver.');
48
+ }
49
+ const decision = sticky ?? (await options.resolver({ targetRelPath, kind }));
50
+ if (decision.applyToAll && !sticky)
51
+ sticky = decision;
52
+ if (decision.choice === 'skip')
53
+ return { proceed: false, backup: false };
54
+ if (decision.choice === 'overwrite') {
55
+ return { proceed: true, backup: options.backup };
56
+ }
57
+ return { proceed: true, backup: true }; // 'backup'
58
+ };
59
+ const record = (a) => {
60
+ actions.push(a);
61
+ if (!log)
62
+ return;
63
+ const tag = options.dryRun ? '[dry-run] ' : '';
64
+ const bk = a.backupPath ? ` (backup: ${(0, node_path_1.relative)(projectRoot, a.backupPath)})` : '';
65
+ switch (a.disposition) {
66
+ case 'created':
67
+ log.success(`${tag}create ${a.targetRelPath}`);
68
+ break;
69
+ case 'overwritten':
70
+ log.success(`${tag}update ${a.targetRelPath}${bk}`);
71
+ break;
72
+ case 'appended-section':
73
+ log.success(`${tag}append ${a.targetRelPath} (Agentcohort Routing Rules)`);
74
+ break;
75
+ case 'replaced-section':
76
+ log.success(`${tag}update ${a.targetRelPath} (Agentcohort Routing Rules)${bk}`);
77
+ break;
78
+ case 'unchanged':
79
+ log.info(`${tag}unchanged ${a.targetRelPath}`);
80
+ break;
81
+ case 'skipped':
82
+ log.warn(`${tag}skip ${a.targetRelPath}`);
83
+ break;
84
+ }
85
+ };
86
+ const doBackup = (target) => {
87
+ const path = uniqueBackupPath(target, now());
88
+ if (!options.dryRun)
89
+ (0, fileOps_1.backupFile)(target, path);
90
+ return path;
91
+ };
92
+ for (const entry of manifest) {
93
+ if (entry.kind === 'regular') {
94
+ await handleRegular(entry);
95
+ }
96
+ else {
97
+ await handleClaudeSection(entry);
98
+ }
99
+ }
100
+ return { projectRoot, actions, dryRun: options.dryRun };
101
+ // ---- per-entry handlers (closures over decide/record/doBackup) ----
102
+ async function handleRegular(entry) {
103
+ const template = (0, node_fs_1.readFileSync)(entry.templateAbsPath, 'utf8');
104
+ const existing = (0, fileOps_1.readIfExists)(entry.targetAbsPath);
105
+ if (existing === null) {
106
+ if (!options.dryRun)
107
+ (0, fileOps_1.writeFileEnsuringDir)(entry.targetAbsPath, template);
108
+ record({
109
+ targetRelPath: entry.targetRelPath,
110
+ kind: entry.kind,
111
+ disposition: 'created',
112
+ dryRun: options.dryRun,
113
+ });
114
+ return;
115
+ }
116
+ if ((0, fileOps_1.contentEquals)(existing, template)) {
117
+ record({
118
+ targetRelPath: entry.targetRelPath,
119
+ kind: entry.kind,
120
+ disposition: 'unchanged',
121
+ dryRun: options.dryRun,
122
+ });
123
+ return;
124
+ }
125
+ const policy = await decide(entry.kind, entry.targetRelPath);
126
+ if (!policy.proceed) {
127
+ record({
128
+ targetRelPath: entry.targetRelPath,
129
+ kind: entry.kind,
130
+ disposition: 'skipped',
131
+ dryRun: options.dryRun,
132
+ });
133
+ return;
134
+ }
135
+ let backupPath;
136
+ if (policy.backup)
137
+ backupPath = doBackup(entry.targetAbsPath);
138
+ if (!options.dryRun)
139
+ (0, fileOps_1.writeFileEnsuringDir)(entry.targetAbsPath, template);
140
+ record({
141
+ targetRelPath: entry.targetRelPath,
142
+ kind: entry.kind,
143
+ disposition: 'overwritten',
144
+ backupPath,
145
+ dryRun: options.dryRun,
146
+ });
147
+ }
148
+ async function handleClaudeSection(entry) {
149
+ const sectionMarkdown = (0, node_fs_1.readFileSync)(entry.templateAbsPath, 'utf8');
150
+ const existing = (0, fileOps_1.readIfExists)(entry.targetAbsPath);
151
+ if (existing === null) {
152
+ const content = (0, claudeMd_1.buildInitialClaudeMd)(sectionMarkdown);
153
+ if (!options.dryRun)
154
+ (0, fileOps_1.writeFileEnsuringDir)(entry.targetAbsPath, content);
155
+ record({
156
+ targetRelPath: entry.targetRelPath,
157
+ kind: entry.kind,
158
+ disposition: 'created',
159
+ dryRun: options.dryRun,
160
+ });
161
+ return;
162
+ }
163
+ if (!(0, claudeMd_1.hasSection)(existing)) {
164
+ const content = (0, claudeMd_1.upsertSection)(existing, sectionMarkdown).result;
165
+ if (!options.dryRun)
166
+ (0, fileOps_1.writeFileEnsuringDir)(entry.targetAbsPath, content);
167
+ record({
168
+ targetRelPath: entry.targetRelPath,
169
+ kind: entry.kind,
170
+ disposition: 'appended-section',
171
+ dryRun: options.dryRun,
172
+ });
173
+ return;
174
+ }
175
+ if ((0, claudeMd_1.sectionMatches)(existing, sectionMarkdown)) {
176
+ record({
177
+ targetRelPath: entry.targetRelPath,
178
+ kind: entry.kind,
179
+ disposition: 'unchanged',
180
+ dryRun: options.dryRun,
181
+ });
182
+ return;
183
+ }
184
+ const policy = await decide(entry.kind, entry.targetRelPath);
185
+ if (!policy.proceed) {
186
+ record({
187
+ targetRelPath: entry.targetRelPath,
188
+ kind: entry.kind,
189
+ disposition: 'skipped',
190
+ dryRun: options.dryRun,
191
+ });
192
+ return;
193
+ }
194
+ let backupPath;
195
+ if (policy.backup)
196
+ backupPath = doBackup(entry.targetAbsPath);
197
+ const content = (0, claudeMd_1.upsertSection)(existing, sectionMarkdown).result;
198
+ if (!options.dryRun)
199
+ (0, fileOps_1.writeFileEnsuringDir)(entry.targetAbsPath, content);
200
+ record({
201
+ targetRelPath: entry.targetRelPath,
202
+ kind: entry.kind,
203
+ disposition: 'replaced-section',
204
+ backupPath,
205
+ dryRun: options.dryRun,
206
+ });
207
+ }
208
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Minimal, dependency-free, cross-platform colored logger.
3
+ *
4
+ * Color is disabled automatically when:
5
+ * - stdout is not a TTY (piped / redirected output), or
6
+ * - the NO_COLOR env var is set (https://no-color.org), or
7
+ * - FORCE_COLOR=0.
8
+ *
9
+ * The Logger is an injectable interface so tests can capture output instead
10
+ * of writing to the real console.
11
+ */
12
+ export interface Logger {
13
+ info(msg: string): void;
14
+ success(msg: string): void;
15
+ warn(msg: string): void;
16
+ error(msg: string): void;
17
+ plain(msg: string): void;
18
+ }
19
+ declare const RAW: {
20
+ readonly reset: `${string}[0m`;
21
+ readonly bold: `${string}[1m`;
22
+ readonly dim: `${string}[2m`;
23
+ readonly red: `${string}[31m`;
24
+ readonly green: `${string}[32m`;
25
+ readonly yellow: `${string}[33m`;
26
+ readonly blue: `${string}[34m`;
27
+ readonly cyan: `${string}[36m`;
28
+ readonly gray: `${string}[90m`;
29
+ };
30
+ type ColorName = keyof typeof RAW;
31
+ export declare function colorEnabled(env?: NodeJS.ProcessEnv): boolean;
32
+ export declare function makePaint(enabled: boolean): (text: string, ...names: ColorName[]) => string;
33
+ export interface ConsoleLike {
34
+ log(msg: string): void;
35
+ error(msg: string): void;
36
+ }
37
+ export declare function createLogger(opts?: {
38
+ console?: ConsoleLike;
39
+ color?: boolean;
40
+ }): Logger;
41
+ /** Shared painter for one-off styling outside the Logger (banners, summaries). */
42
+ export declare const paint: (text: string, ...names: ColorName[]) => string;
43
+ export {};
package/dist/logger.js ADDED
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal, dependency-free, cross-platform colored logger.
4
+ *
5
+ * Color is disabled automatically when:
6
+ * - stdout is not a TTY (piped / redirected output), or
7
+ * - the NO_COLOR env var is set (https://no-color.org), or
8
+ * - FORCE_COLOR=0.
9
+ *
10
+ * The Logger is an injectable interface so tests can capture output instead
11
+ * of writing to the real console.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.paint = void 0;
15
+ exports.colorEnabled = colorEnabled;
16
+ exports.makePaint = makePaint;
17
+ exports.createLogger = createLogger;
18
+ const ESC = String.fromCharCode(27);
19
+ const RAW = {
20
+ reset: `${ESC}[0m`,
21
+ bold: `${ESC}[1m`,
22
+ dim: `${ESC}[2m`,
23
+ red: `${ESC}[31m`,
24
+ green: `${ESC}[32m`,
25
+ yellow: `${ESC}[33m`,
26
+ blue: `${ESC}[34m`,
27
+ cyan: `${ESC}[36m`,
28
+ gray: `${ESC}[90m`,
29
+ };
30
+ function colorEnabled(env = process.env) {
31
+ if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
32
+ return false;
33
+ if (env.FORCE_COLOR === '0')
34
+ return false;
35
+ if (env.FORCE_COLOR && env.FORCE_COLOR !== '0')
36
+ return true;
37
+ return Boolean(process.stdout && process.stdout.isTTY);
38
+ }
39
+ function makePaint(enabled) {
40
+ return (text, ...names) => {
41
+ if (!enabled || names.length === 0)
42
+ return text;
43
+ const open = names.map((n) => RAW[n]).join('');
44
+ return `${open}${text}${RAW.reset}`;
45
+ };
46
+ }
47
+ function createLogger(opts = {}) {
48
+ const sink = opts.console ?? {
49
+ log: (m) => process.stdout.write(m + '\n'),
50
+ error: (m) => process.stderr.write(m + '\n'),
51
+ };
52
+ const enabled = opts.color ?? colorEnabled();
53
+ const p = makePaint(enabled);
54
+ return {
55
+ plain: (m) => sink.log(m),
56
+ info: (m) => sink.log(`${p('•', 'cyan')} ${m}`),
57
+ success: (m) => sink.log(`${p('✓', 'green')} ${m}`),
58
+ warn: (m) => sink.log(`${p('!', 'yellow')} ${m}`),
59
+ error: (m) => sink.error(`${p('✗', 'red')} ${m}`),
60
+ };
61
+ }
62
+ /** Shared painter for one-off styling outside the Logger (banners, summaries). */
63
+ exports.paint = makePaint(colorEnabled());
@@ -0,0 +1,18 @@
1
+ export type EntryKind = 'regular' | 'claude-section';
2
+ export interface ManifestEntry {
3
+ kind: EntryKind;
4
+ /** Absolute path to the source template inside the package. */
5
+ templateAbsPath: string;
6
+ /** Absolute path where it will be installed in the target project. */
7
+ targetAbsPath: string;
8
+ /** Forward-slash relative path for stable, readable CLI output. */
9
+ targetRelPath: string;
10
+ }
11
+ /**
12
+ * Build the install manifest by enumerating the bundled templates.
13
+ *
14
+ * Enumeration (rather than a hard-coded list) keeps the package
15
+ * maintainable: dropping a new `agents/*.md` template ships it with no
16
+ * code change.
17
+ */
18
+ export declare function buildManifest(templatesDir: string, projectRoot: string): ManifestEntry[];
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildManifest = buildManifest;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const SECTION_FILE = 'CLAUDE.section.md';
7
+ function listMarkdown(dir) {
8
+ if (!(0, node_fs_1.existsSync)(dir))
9
+ return [];
10
+ return (0, node_fs_1.readdirSync)(dir)
11
+ .filter((f) => f.endsWith('.md'))
12
+ .sort();
13
+ }
14
+ /**
15
+ * Build the install manifest by enumerating the bundled templates.
16
+ *
17
+ * Enumeration (rather than a hard-coded list) keeps the package
18
+ * maintainable: dropping a new `agents/*.md` template ships it with no
19
+ * code change.
20
+ */
21
+ function buildManifest(templatesDir, projectRoot) {
22
+ const entries = [];
23
+ for (const group of ['agents', 'commands']) {
24
+ const dir = (0, node_path_1.join)(templatesDir, group);
25
+ for (const file of listMarkdown(dir)) {
26
+ entries.push({
27
+ kind: 'regular',
28
+ templateAbsPath: (0, node_path_1.join)(dir, file),
29
+ targetAbsPath: (0, node_path_1.join)(projectRoot, '.claude', group, file),
30
+ targetRelPath: `.claude/${group}/${file}`,
31
+ });
32
+ }
33
+ }
34
+ const sectionTemplate = (0, node_path_1.join)(templatesDir, SECTION_FILE);
35
+ if ((0, node_fs_1.existsSync)(sectionTemplate)) {
36
+ entries.push({
37
+ kind: 'claude-section',
38
+ templateAbsPath: sectionTemplate,
39
+ targetAbsPath: (0, node_path_1.join)(projectRoot, 'CLAUDE.md'),
40
+ targetRelPath: 'CLAUDE.md',
41
+ });
42
+ }
43
+ return entries;
44
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resolve the bundled templates directory.
3
+ *
4
+ * Works in two layouts:
5
+ * - Compiled: dist/paths.js -> dist/templates (copied by build script)
6
+ * - Tests/ts: src/paths.ts -> src/templates
7
+ *
8
+ * Both keep `templates/` as a sibling of this module, so __dirname is the
9
+ * single source of truth and no environment guessing is needed.
10
+ */
11
+ export declare function getTemplatesDir(): string;
12
+ /** Read the package version from package.json (sibling of dist/ or src/). */
13
+ export declare function getVersion(): string;
14
+ /** Normalize a user-supplied target directory to an absolute path. */
15
+ export declare function resolveProjectRoot(cwd: string): string;
package/dist/paths.js ADDED
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTemplatesDir = getTemplatesDir;
4
+ exports.getVersion = getVersion;
5
+ exports.resolveProjectRoot = resolveProjectRoot;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ /**
9
+ * Resolve the bundled templates directory.
10
+ *
11
+ * Works in two layouts:
12
+ * - Compiled: dist/paths.js -> dist/templates (copied by build script)
13
+ * - Tests/ts: src/paths.ts -> src/templates
14
+ *
15
+ * Both keep `templates/` as a sibling of this module, so __dirname is the
16
+ * single source of truth and no environment guessing is needed.
17
+ */
18
+ function getTemplatesDir() {
19
+ const local = (0, node_path_1.join)(__dirname, 'templates');
20
+ if ((0, node_fs_1.existsSync)(local))
21
+ return local;
22
+ throw new Error(`agentcohort: templates directory not found next to ${__dirname}. ` +
23
+ `The package may be corrupted; try reinstalling.`);
24
+ }
25
+ /** Read the package version from package.json (sibling of dist/ or src/). */
26
+ function getVersion() {
27
+ const candidates = [
28
+ (0, node_path_1.join)(__dirname, '..', 'package.json'),
29
+ (0, node_path_1.join)(__dirname, '..', '..', 'package.json'),
30
+ ];
31
+ for (const c of candidates) {
32
+ try {
33
+ if (!(0, node_fs_1.existsSync)(c))
34
+ continue;
35
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)(c, 'utf8'));
36
+ // Scope-resilient: matches "agentcohort" and "@scope/agentcohort".
37
+ if (typeof pkg.version === 'string' &&
38
+ typeof pkg.name === 'string' &&
39
+ pkg.name.replace(/^@[^/]+\//, '') === 'agentcohort') {
40
+ return pkg.version;
41
+ }
42
+ }
43
+ catch {
44
+ /* keep trying */
45
+ }
46
+ }
47
+ return '0.0.0';
48
+ }
49
+ /** Normalize a user-supplied target directory to an absolute path. */
50
+ function resolveProjectRoot(cwd) {
51
+ return (0, node_path_1.resolve)(cwd);
52
+ }
@@ -0,0 +1,32 @@
1
+ import { Readable, Writable } from 'node:stream';
2
+ export type Choice = 'skip' | 'overwrite' | 'backup';
3
+ export interface ConflictContext {
4
+ targetRelPath: string;
5
+ /** 'regular' => file already exists & differs. 'claude-section' => our section already exists & differs. */
6
+ kind: 'regular' | 'claude-section';
7
+ }
8
+ export interface ConflictDecision {
9
+ choice: Choice;
10
+ /** Apply this same choice to every remaining conflict without re-asking. */
11
+ applyToAll: boolean;
12
+ }
13
+ export type ConflictResolver = (ctx: ConflictContext) => Promise<ConflictDecision>;
14
+ /**
15
+ * Interactive resolver backed by readline.
16
+ *
17
+ * Key map (lowercase = this file only, UPPERCASE = all remaining conflicts):
18
+ * s/S skip
19
+ * o/O overwrite (claude-section: replace our section in place)
20
+ * b/B backup + overwrite <-- default for regular files
21
+ * For claude-section the safe default is "skip" (don't rewrite hand-edited
22
+ * routing rules unless asked).
23
+ */
24
+ export declare function createInteractiveResolver(io?: {
25
+ input?: Readable;
26
+ output?: Writable;
27
+ }): {
28
+ resolve: ConflictResolver;
29
+ close: () => void;
30
+ };
31
+ /** Non-interactive resolver: always returns the supplied safe decision. */
32
+ export declare function fixedResolver(decision: ConflictDecision): ConflictResolver;
package/dist/prompt.js ADDED
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createInteractiveResolver = createInteractiveResolver;
4
+ exports.fixedResolver = fixedResolver;
5
+ const promises_1 = require("node:readline/promises");
6
+ /**
7
+ * Interactive resolver backed by readline.
8
+ *
9
+ * Key map (lowercase = this file only, UPPERCASE = all remaining conflicts):
10
+ * s/S skip
11
+ * o/O overwrite (claude-section: replace our section in place)
12
+ * b/B backup + overwrite <-- default for regular files
13
+ * For claude-section the safe default is "skip" (don't rewrite hand-edited
14
+ * routing rules unless asked).
15
+ */
16
+ function createInteractiveResolver(io = {}) {
17
+ const rl = (0, promises_1.createInterface)({
18
+ input: io.input ?? process.stdin,
19
+ output: io.output ?? process.stdout,
20
+ });
21
+ const resolve = async (ctx) => {
22
+ const defaultChoice = ctx.kind === 'claude-section' ? 'skip' : 'backup';
23
+ const what = ctx.kind === 'claude-section'
24
+ ? `CLAUDE.md already has an "Agentcohort Routing Rules" section that differs`
25
+ : `${ctx.targetRelPath} already exists and differs`;
26
+ const defLabel = defaultChoice === 'skip' ? 's' : 'b';
27
+ const question = `\n${what}.\n` +
28
+ ` [s] skip [o] overwrite [b] backup + overwrite\n` +
29
+ ` (UPPERCASE applies to all remaining, default: ${defLabel}) > `;
30
+ // Loop until a valid answer (or EOF -> default).
31
+ for (;;) {
32
+ let answer;
33
+ try {
34
+ answer = (await rl.question(question)).trim();
35
+ }
36
+ catch {
37
+ return { choice: defaultChoice, applyToAll: true };
38
+ }
39
+ if (answer === '')
40
+ return { choice: defaultChoice, applyToAll: false };
41
+ const applyToAll = answer === answer.toUpperCase();
42
+ const key = answer[0].toLowerCase();
43
+ if (key === 's')
44
+ return { choice: 'skip', applyToAll };
45
+ if (key === 'o')
46
+ return { choice: 'overwrite', applyToAll };
47
+ if (key === 'b')
48
+ return { choice: 'backup', applyToAll };
49
+ // invalid -> ask again
50
+ }
51
+ };
52
+ return { resolve, close: () => rl.close() };
53
+ }
54
+ /** Non-interactive resolver: always returns the supplied safe decision. */
55
+ function fixedResolver(decision) {
56
+ return async () => decision;
57
+ }