atris 3.68.7 → 3.68.9
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/commands/member.js +26 -0
- package/commands/sync.js +56 -3
- package/lib/member-export.js +240 -0
- package/package.json +1 -1
package/commands/member.js
CHANGED
|
@@ -10904,6 +10904,29 @@ async function memberRoom(...args) {
|
|
|
10904
10904
|
return room;
|
|
10905
10905
|
}
|
|
10906
10906
|
|
|
10907
|
+
// --- EXPORT subcommand ---
|
|
10908
|
+
|
|
10909
|
+
function memberExport(...args) {
|
|
10910
|
+
const { exportMembers, summaryLine } = require('../lib/member-export');
|
|
10911
|
+
const dryRun = args.includes('--dry-run');
|
|
10912
|
+
const root = process.cwd();
|
|
10913
|
+
if (!fs.existsSync(path.join(root, 'atris', 'team'))) {
|
|
10914
|
+
console.error('No atris/team folder here, so there are no members to export.');
|
|
10915
|
+
process.exitCode = 1;
|
|
10916
|
+
return null;
|
|
10917
|
+
}
|
|
10918
|
+
const result = exportMembers(root, { dryRun });
|
|
10919
|
+
if (args.includes('--json')) {
|
|
10920
|
+
console.log(JSON.stringify({ dry_run: dryRun, ...result }, null, 2));
|
|
10921
|
+
return result;
|
|
10922
|
+
}
|
|
10923
|
+
for (const file of result.written) console.log(`${dryRun ? 'Would write' : '✓ Wrote'} ${file}`);
|
|
10924
|
+
for (const file of result.removed) console.log(`${dryRun ? 'Would remove' : '✓ Removed'} ${file}`);
|
|
10925
|
+
for (const file of result.kept) console.log(`Left alone ${file} (hand-written, not made by export)`);
|
|
10926
|
+
console.log(summaryLine(result, { dryRun }));
|
|
10927
|
+
return result;
|
|
10928
|
+
}
|
|
10929
|
+
|
|
10907
10930
|
async function memberCommand(subcommand, ...args) {
|
|
10908
10931
|
// Subcommands that take a member name as args[0] otherwise treat `--help` as
|
|
10909
10932
|
// a name and error with "Member '--help' not found". `create`/`new` handle
|
|
@@ -11013,6 +11036,8 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11013
11036
|
return memberObjectiveGeneratorCommand(args[0], ...args.slice(1));
|
|
11014
11037
|
case 'generalist':
|
|
11015
11038
|
return memberGeneralistCommand(args[0], ...args.slice(1));
|
|
11039
|
+
case 'export':
|
|
11040
|
+
return memberExport(...args);
|
|
11016
11041
|
case 'archive':
|
|
11017
11042
|
return memberArchive(args[0]);
|
|
11018
11043
|
case 'purge-archived':
|
|
@@ -11047,6 +11072,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11047
11072
|
console.log(' install <slug> [--update] Install missing files, or refresh template skills');
|
|
11048
11073
|
console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
|
|
11049
11074
|
console.log(' list [--json] Show all team members');
|
|
11075
|
+
console.log(' export [--dry-run] Write each member as a subagent for Claude Code, Cursor, and Codex');
|
|
11050
11076
|
console.log(' activate <name> Symlink member skills, show context and permissions');
|
|
11051
11077
|
console.log(' upgrade <name> Convert flat file (name.md) to directory format');
|
|
11052
11078
|
console.log(' push <name> Push a local team member to the cloud');
|
package/commands/sync.js
CHANGED
|
@@ -83,6 +83,23 @@ function ensureRealDirectory(dir) {
|
|
|
83
83
|
fs.mkdirSync(dir, { recursive: true });
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// A hook script the project already has is never replaced. Projects harden
|
|
87
|
+
// their hooks (nonce checks, queues, proof before pop); a sync must not
|
|
88
|
+
// downgrade them to the shipped copy.
|
|
89
|
+
function isProtectedHook(destPath) {
|
|
90
|
+
return path.basename(path.dirname(destPath)) === 'hooks' && fs.existsSync(destPath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A member retired to team/_archived/<name>-<date>/ must never come back.
|
|
94
|
+
function isArchivedMember(teamDir, name) {
|
|
95
|
+
const archiveDir = path.join(teamDir, '_archived');
|
|
96
|
+
try {
|
|
97
|
+
return fs.readdirSync(archiveDir).some(entry => entry === name || (entry.startsWith(`${name}-`) && /^\d{4}-\d{2}-\d{2}$/.test(entry.slice(name.length + 1))));
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
/**
|
|
87
104
|
* Sync the canonical skill set from atris-cli/atris/skills/* into a
|
|
88
105
|
* workspace's atris/skills/* (plus ensure .claude/skills/ symlinks).
|
|
@@ -128,7 +145,9 @@ function syncPackageSkills(targetAtrisDir, opts = {}) {
|
|
|
128
145
|
} else {
|
|
129
146
|
const srcContent = fs.readFileSync(srcPath, 'utf8');
|
|
130
147
|
const destContent = fs.existsSync(destPath) ? fs.readFileSync(destPath, 'utf8') : '';
|
|
131
|
-
if (srcContent !== destContent) {
|
|
148
|
+
if (srcContent !== destContent && isProtectedHook(destPath)) {
|
|
149
|
+
if (verbose) console.log(`- Kept atris/skills/${skillName}/${relPath} (differs from shipped hook, not replaced)`);
|
|
150
|
+
} else if (srcContent !== destContent) {
|
|
132
151
|
if (!dryRun) {
|
|
133
152
|
fs.writeFileSync(destPath, srcContent);
|
|
134
153
|
if (entry.endsWith('.sh')) fs.chmodSync(destPath, 0o755);
|
|
@@ -348,6 +367,12 @@ function syncWorkspaceTemplate(targetRoot, bizMeta, options = {}) {
|
|
|
348
367
|
try { templateContent = fs.readFileSync(templatePath, 'utf-8'); } catch { continue; }
|
|
349
368
|
const finalContent = _substituteParams(templateContent, params);
|
|
350
369
|
|
|
370
|
+
const memberMatch = targetRelPath.split(path.sep).join('/').match(/^team\/([^/_][^/]*)\//);
|
|
371
|
+
if (memberMatch && isArchivedMember(path.join(targetAtrisDir, 'team'), memberMatch[1])) {
|
|
372
|
+
skipped++;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
351
376
|
if (!fs.existsSync(targetPath)) {
|
|
352
377
|
addedList.push(targetRelPath); added++;
|
|
353
378
|
if (!dryRun) {
|
|
@@ -358,7 +383,7 @@ function syncWorkspaceTemplate(targetRoot, bizMeta, options = {}) {
|
|
|
358
383
|
const existing = fs.readFileSync(targetPath, 'utf-8');
|
|
359
384
|
if (existing === finalContent) {
|
|
360
385
|
skipped++;
|
|
361
|
-
} else if (force) {
|
|
386
|
+
} else if (force && !memberMatch) {
|
|
362
387
|
updatedList.push(targetRelPath); updated++;
|
|
363
388
|
if (!dryRun) fs.writeFileSync(targetPath, finalContent);
|
|
364
389
|
} else {
|
|
@@ -437,6 +462,18 @@ function syncBusinessCanonical(targetRoot, bizMeta, options = {}) {
|
|
|
437
462
|
});
|
|
438
463
|
}
|
|
439
464
|
|
|
465
|
+
// Team members follow every written update, business or legacy, into Claude
|
|
466
|
+
// Code, Cursor, and Codex as subagents. An export failure never fails the update.
|
|
467
|
+
function exportTeamAfterUpdate(root, { dryRun = false } = {}) {
|
|
468
|
+
if (dryRun || !fs.existsSync(path.join(root, 'atris', 'team'))) return;
|
|
469
|
+
try {
|
|
470
|
+
const { exportMembers, summaryLine } = require('../lib/member-export');
|
|
471
|
+
console.log(`✓ Team: ${summaryLine(exportMembers(root))}`);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
console.log(`Team export skipped: ${err.message || err}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
440
477
|
function syncAtris(options = {}) {
|
|
441
478
|
const explicitDryRun = options.dryRun != null ? options.dryRun : process.argv.includes('--dry-run');
|
|
442
479
|
const yes = options.yes != null
|
|
@@ -455,6 +492,7 @@ function syncAtris(options = {}) {
|
|
|
455
492
|
dryRun,
|
|
456
493
|
force,
|
|
457
494
|
});
|
|
495
|
+
exportTeamAfterUpdate(process.cwd(), { dryRun });
|
|
458
496
|
if (!yes) {
|
|
459
497
|
console.log('\nPlan only. Re-run with --yes to write these files.');
|
|
460
498
|
if (!explicitDryRun && isNonInteractive()) process.exit(2);
|
|
@@ -552,6 +590,17 @@ function syncAtris(options = {}) {
|
|
|
552
590
|
return;
|
|
553
591
|
}
|
|
554
592
|
|
|
593
|
+
// Team members belong to the project. Update never adds a member (init
|
|
594
|
+
// does that, and a missing member may have been retired to
|
|
595
|
+
// team/_archived/) and never replaces one the project has changed.
|
|
596
|
+
if (target.startsWith('team/')) {
|
|
597
|
+
if (fs.existsSync(targetFile)) {
|
|
598
|
+
console.log(`- Kept ${target} (newer template exists in the package, yours not replaced)`);
|
|
599
|
+
}
|
|
600
|
+
skipped++;
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
555
604
|
if (!dryRun) {
|
|
556
605
|
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
557
606
|
fs.copyFileSync(sourceFile, targetFile);
|
|
@@ -695,6 +744,8 @@ Key behaviors:
|
|
|
695
744
|
console.log(`\n✓ Updated ${updated} file(s), ${skipped} unchanged`);
|
|
696
745
|
console.log('\nRun your AI agent again to use the latest specs and agent templates.');
|
|
697
746
|
}
|
|
747
|
+
|
|
748
|
+
exportTeamAfterUpdate(process.cwd(), { dryRun });
|
|
698
749
|
}
|
|
699
750
|
|
|
700
751
|
/**
|
|
@@ -713,7 +764,9 @@ function syncRecursiveCount(src, dest, label, silent) {
|
|
|
713
764
|
} else {
|
|
714
765
|
const srcBuf = fs.readFileSync(srcPath);
|
|
715
766
|
const destBuf = fs.existsSync(destPath) ? fs.readFileSync(destPath) : Buffer.alloc(0);
|
|
716
|
-
if (!srcBuf.equals(destBuf)) {
|
|
767
|
+
if (!srcBuf.equals(destBuf) && isProtectedHook(destPath)) {
|
|
768
|
+
if (!silent) console.log(`- Kept ${label}/${entry} (differs from shipped hook, not replaced)`);
|
|
769
|
+
} else if (!srcBuf.equals(destBuf)) {
|
|
717
770
|
fs.writeFileSync(destPath, srcBuf);
|
|
718
771
|
if (entry.endsWith('.sh')) {
|
|
719
772
|
fs.chmodSync(destPath, 0o755);
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `atris member export`: each active atris/team/<member>/MEMBER.md becomes a
|
|
4
|
+
// native subagent file for the coding tools that read one.
|
|
5
|
+
//
|
|
6
|
+
// claude .claude/agents/<name>.md markdown, YAML frontmatter (name, description, model)
|
|
7
|
+
// cursor .cursor/agents/<name>.md same markdown shape; cursor-agent globs **/.cursor/agents/**/*.md
|
|
8
|
+
// codex .codex/agents/<name>.toml agent role file (name, description, developer_instructions);
|
|
9
|
+
// codex 0.162 discovers roles in .codex/agents
|
|
10
|
+
//
|
|
11
|
+
// Every file carries GENERATED_MARK. Export only overwrites or removes files
|
|
12
|
+
// that carry it, so a hand-written agent with the same name is left alone.
|
|
13
|
+
// Parked members are skipped, and their old generated files are removed.
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { isParkedText } = require('./member-park');
|
|
18
|
+
|
|
19
|
+
const GENERATED_MARK = 'generated by atris member export';
|
|
20
|
+
const TARGETS = ['claude', 'cursor', 'codex'];
|
|
21
|
+
const SKIP_DIRS = new Set(['_template', '_archive', '_archived']);
|
|
22
|
+
|
|
23
|
+
function frontmatterOf(text) {
|
|
24
|
+
const match = String(text || '').replace(/^/, '').match(/^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/);
|
|
25
|
+
if (!match) return { fields: {}, body: String(text || '') };
|
|
26
|
+
const fields = {};
|
|
27
|
+
let listKey = null;
|
|
28
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
29
|
+
const item = line.match(/^\s+-\s+(.+)$/);
|
|
30
|
+
if (item && listKey) {
|
|
31
|
+
fields[listKey].push(item[1].trim().replace(/^["']|["']$/g, ''));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const kv = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
35
|
+
if (!kv) continue;
|
|
36
|
+
const value = kv[2].replace(/\s+#.*$/, '').trim();
|
|
37
|
+
if (value === '') {
|
|
38
|
+
fields[kv[1]] = [];
|
|
39
|
+
listKey = kv[1];
|
|
40
|
+
} else if (value.startsWith('[') && value.endsWith(']')) {
|
|
41
|
+
fields[kv[1]] = value.slice(1, -1).split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
42
|
+
listKey = null;
|
|
43
|
+
} else {
|
|
44
|
+
fields[kv[1]] = value.replace(/^["']|["']$/g, '');
|
|
45
|
+
listKey = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { fields, body: String(text).replace(/^/, '').slice(match[0].length) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scalar(value) {
|
|
52
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function listOf(value) {
|
|
56
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === 'string' && v.trim()) : [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Skills named in frontmatter plus any skills/<name>/SKILL.md on disk.
|
|
60
|
+
function memberSkills(dir, fields) {
|
|
61
|
+
const names = new Set(listOf(fields.skills));
|
|
62
|
+
const skillsDir = path.join(dir, 'skills');
|
|
63
|
+
try {
|
|
64
|
+
for (const entry of fs.readdirSync(skillsDir)) {
|
|
65
|
+
if (fs.existsSync(path.join(skillsDir, entry, 'SKILL.md'))) names.add(entry);
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
return [...names].sort();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readMembers(root) {
|
|
72
|
+
const teamDir = path.join(root, 'atris', 'team');
|
|
73
|
+
let entries = [];
|
|
74
|
+
try {
|
|
75
|
+
entries = fs.readdirSync(teamDir).sort();
|
|
76
|
+
} catch {
|
|
77
|
+
return { members: [], parked: [] };
|
|
78
|
+
}
|
|
79
|
+
const members = [];
|
|
80
|
+
const parked = [];
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (SKIP_DIRS.has(entry) || entry.startsWith('.')) continue;
|
|
83
|
+
const dir = path.join(teamDir, entry);
|
|
84
|
+
const file = path.join(dir, 'MEMBER.md');
|
|
85
|
+
let text;
|
|
86
|
+
try {
|
|
87
|
+
text = fs.readFileSync(file, 'utf8');
|
|
88
|
+
} catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (isParkedText(text)) {
|
|
92
|
+
parked.push(entry);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const { fields, body } = frontmatterOf(text);
|
|
96
|
+
members.push({
|
|
97
|
+
slug: entry,
|
|
98
|
+
role: scalar(fields.role),
|
|
99
|
+
description: scalar(fields.description),
|
|
100
|
+
model: scalar(fields.model),
|
|
101
|
+
skills: memberSkills(dir, fields),
|
|
102
|
+
body: body.trim(),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return { members, parked };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Claude Code wants lowercase letters, digits, and hyphens; codex allows
|
|
109
|
+
// letters, digits, spaces, hyphens, and underscores. One safe form fits both.
|
|
110
|
+
function agentName(slug) {
|
|
111
|
+
return String(slug).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '') || 'member';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Written for the tool's router: what the member does, then when to hand it work.
|
|
115
|
+
function agentDescription(member) {
|
|
116
|
+
const what = member.description.replace(/\s+/g, ' ').trim();
|
|
117
|
+
const role = member.role || member.slug;
|
|
118
|
+
const lead = what ? (/[.!?]$/.test(what) ? what : `${what}.`) : `The ${role} on this project's Atris team.`;
|
|
119
|
+
return `${lead} Use this agent when a task fits the ${role} role on this project's Atris team.`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function instructions(member) {
|
|
123
|
+
const lines = [
|
|
124
|
+
`You are ${member.slug}, the ${member.role || member.slug} on this project's Atris team.`,
|
|
125
|
+
`Your full card is atris/team/${member.slug}/MEMBER.md; read it when this summary is not enough.`,
|
|
126
|
+
'',
|
|
127
|
+
];
|
|
128
|
+
if (member.body) lines.push(member.body, '');
|
|
129
|
+
if (member.skills.length) {
|
|
130
|
+
lines.push('## Skills', '');
|
|
131
|
+
for (const skill of member.skills) {
|
|
132
|
+
lines.push(`- ${skill}: atris/team/${member.slug}/skills/${skill}/SKILL.md`);
|
|
133
|
+
}
|
|
134
|
+
lines.push('');
|
|
135
|
+
}
|
|
136
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function markdownAgent(member, { withModel }) {
|
|
140
|
+
const source = `atris/team/${member.slug}/MEMBER.md`;
|
|
141
|
+
const front = [
|
|
142
|
+
'---',
|
|
143
|
+
`# ${GENERATED_MARK} from ${source}. edit MEMBER.md; this file gets overwritten.`,
|
|
144
|
+
`name: ${agentName(member.slug)}`,
|
|
145
|
+
`description: ${JSON.stringify(agentDescription(member))}`,
|
|
146
|
+
];
|
|
147
|
+
if (withModel && member.model) front.push(`model: ${JSON.stringify(member.model)}`);
|
|
148
|
+
front.push('---', '', '');
|
|
149
|
+
return front.join('\n') + instructions(member);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// TOML basic strings take the same escapes JSON.stringify emits.
|
|
153
|
+
function codexAgent(member) {
|
|
154
|
+
const source = `atris/team/${member.slug}/MEMBER.md`;
|
|
155
|
+
return [
|
|
156
|
+
`# ${GENERATED_MARK} from ${source}. edit MEMBER.md; this file gets overwritten.`,
|
|
157
|
+
`name = ${JSON.stringify(agentName(member.slug))}`,
|
|
158
|
+
`description = ${JSON.stringify(agentDescription(member))}`,
|
|
159
|
+
`developer_instructions = ${JSON.stringify(instructions(member))}`,
|
|
160
|
+
'',
|
|
161
|
+
].join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const FORMATS = {
|
|
165
|
+
claude: { dir: ['.claude', 'agents'], ext: '.md', render: (m) => markdownAgent(m, { withModel: true }) },
|
|
166
|
+
cursor: { dir: ['.cursor', 'agents'], ext: '.md', render: (m) => markdownAgent(m, { withModel: false }) },
|
|
167
|
+
codex: { dir: ['.codex', 'agents'], ext: '.toml', render: codexAgent },
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
function isGenerated(file) {
|
|
171
|
+
try {
|
|
172
|
+
return fs.readFileSync(file, 'utf8').slice(0, 400).includes(GENERATED_MARK);
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function exportMembers(root, { targets = TARGETS, dryRun = false } = {}) {
|
|
179
|
+
const { members, parked } = readMembers(root);
|
|
180
|
+
const result = { members: members.map((m) => m.slug), parked, written: [], unchanged: [], removed: [], kept: [] };
|
|
181
|
+
if (!fs.existsSync(path.join(root, 'atris', 'team'))) return result;
|
|
182
|
+
|
|
183
|
+
for (const target of targets) {
|
|
184
|
+
const format = FORMATS[target];
|
|
185
|
+
if (!format) continue;
|
|
186
|
+
const dir = path.join(root, ...format.dir);
|
|
187
|
+
const wanted = new Set();
|
|
188
|
+
for (const member of members) {
|
|
189
|
+
const file = path.join(dir, agentName(member.slug) + format.ext);
|
|
190
|
+
const rel = path.relative(root, file);
|
|
191
|
+
wanted.add(file);
|
|
192
|
+
const next = format.render(member);
|
|
193
|
+
if (fs.existsSync(file)) {
|
|
194
|
+
if (!isGenerated(file)) {
|
|
195
|
+
result.kept.push(rel);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (fs.readFileSync(file, 'utf8') === next) {
|
|
199
|
+
result.unchanged.push(rel);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!dryRun) {
|
|
204
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
205
|
+
fs.writeFileSync(file, next, 'utf8');
|
|
206
|
+
}
|
|
207
|
+
result.written.push(rel);
|
|
208
|
+
}
|
|
209
|
+
// A member that was parked or removed leaves no stale generated file behind.
|
|
210
|
+
let existing = [];
|
|
211
|
+
try {
|
|
212
|
+
existing = fs.readdirSync(dir);
|
|
213
|
+
} catch {}
|
|
214
|
+
for (const entry of existing) {
|
|
215
|
+
const file = path.join(dir, entry);
|
|
216
|
+
if (!entry.endsWith(format.ext) || wanted.has(file) || !isGenerated(file)) continue;
|
|
217
|
+
if (!dryRun) fs.unlinkSync(file);
|
|
218
|
+
result.removed.push(path.relative(root, file));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function summaryLine(result, { dryRun = false } = {}) {
|
|
225
|
+
const count = result.members.length;
|
|
226
|
+
const parts = [`${count} ${count === 1 ? 'member' : 'members'} exported to claude code, cursor, and codex`];
|
|
227
|
+
if (result.written.length) parts.push(`${result.written.length} ${dryRun ? 'would change' : 'files written'}`);
|
|
228
|
+
if (result.removed.length) parts.push(`${result.removed.length} old ${dryRun ? 'would be removed' : 'removed'}`);
|
|
229
|
+
if (!result.written.length && !result.removed.length) parts.push('already up to date');
|
|
230
|
+
if (result.parked.length) parts.push(`${result.parked.length} parked skipped`);
|
|
231
|
+
return parts.join(', ');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
GENERATED_MARK,
|
|
236
|
+
agentName,
|
|
237
|
+
exportMembers,
|
|
238
|
+
frontmatterOf,
|
|
239
|
+
summaryLine,
|
|
240
|
+
};
|