atris 2.6.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -34
- package/atris/CLAUDE.md +5 -1
- package/atris/atris.md +4 -0
- package/atris/features/README.md +24 -0
- package/atris/skills/autopilot/SKILL.md +74 -75
- package/atris/skills/endgame/SKILL.md +179 -0
- package/atris/skills/flow/SKILL.md +121 -0
- package/atris/skills/improve/SKILL.md +84 -0
- package/atris/skills/loop/SKILL.md +72 -0
- package/atris/skills/wiki/SKILL.md +61 -0
- package/atris/team/executor/MEMBER.md +10 -4
- package/atris/team/navigator/MEMBER.md +2 -0
- package/atris/team/validator/MEMBER.md +8 -5
- package/atris.md +33 -0
- package/bin/atris.js +210 -41
- package/commands/activate.js +28 -2
- package/commands/align.js +720 -0
- package/commands/auth.js +75 -2
- package/commands/autopilot.js +1213 -270
- package/commands/browse.js +100 -0
- package/commands/business.js +785 -12
- package/commands/clean.js +107 -2
- package/commands/computer.js +429 -0
- package/commands/context-sync.js +78 -8
- package/commands/experiments.js +351 -0
- package/commands/feedback.js +150 -0
- package/commands/fleet.js +395 -0
- package/commands/fork.js +127 -0
- package/commands/init.js +50 -1
- package/commands/learn.js +407 -0
- package/commands/lifecycle.js +94 -0
- package/commands/loop.js +114 -0
- package/commands/publish.js +129 -0
- package/commands/pull.js +434 -48
- package/commands/push.js +312 -164
- package/commands/review.js +149 -0
- package/commands/run.js +76 -43
- package/commands/serve.js +360 -0
- package/commands/setup.js +1 -1
- package/commands/soul.js +381 -0
- package/commands/status.js +119 -1
- package/commands/sync.js +147 -1
- package/commands/terminal.js +201 -0
- package/commands/wiki.js +376 -0
- package/commands/workflow.js +191 -74
- package/commands/workspace-clean.js +3 -3
- package/lib/endstate.js +259 -0
- package/lib/learnings.js +235 -0
- package/lib/manifest.js +1 -0
- package/lib/todo.js +9 -5
- package/lib/wiki.js +578 -0
- package/package.json +2 -2
- package/utils/api.js +48 -36
- package/utils/auth.js +1 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atris Fleet — Manage the agent swarm via Swarlo
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* atris fleet — Show fleet status
|
|
6
|
+
* atris fleet status — Same as above
|
|
7
|
+
* atris fleet post <message> — Post to the board
|
|
8
|
+
* atris fleet task <prompt> — Post a task for agents to claim
|
|
9
|
+
* atris fleet claim <task_key> — Claim a task
|
|
10
|
+
* atris fleet done <task_key> — Report task complete
|
|
11
|
+
* atris fleet members — List members
|
|
12
|
+
* atris fleet prune — Remove stale members
|
|
13
|
+
* atris fleet join — Register this session
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const http = require('http');
|
|
17
|
+
const https = require('https');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
|
|
23
|
+
// ── Config ──────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const DEFAULT_HUB_URL = 'http://localhost:8090';
|
|
26
|
+
const DEFAULT_HUB_ID = 'atris';
|
|
27
|
+
|
|
28
|
+
function loadConfig() {
|
|
29
|
+
const paths = [
|
|
30
|
+
path.join(os.homedir(), '.swarlo', 'config.json'),
|
|
31
|
+
path.join(process.cwd(), '.swarlo', 'config.json'),
|
|
32
|
+
];
|
|
33
|
+
for (const p of paths) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getHubUrl() {
|
|
43
|
+
const cfg = loadConfig();
|
|
44
|
+
return cfg.server || process.env.SWARLO_SERVER || DEFAULT_HUB_URL;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getHubId() {
|
|
48
|
+
const cfg = loadConfig();
|
|
49
|
+
return cfg.hub || process.env.SWARLO_HUB || DEFAULT_HUB_ID;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── HTTP helpers ────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function request(method, urlStr, body, headers = {}) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const url = new URL(urlStr);
|
|
57
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
58
|
+
const opts = {
|
|
59
|
+
hostname: url.hostname,
|
|
60
|
+
port: url.port,
|
|
61
|
+
path: url.pathname + url.search,
|
|
62
|
+
method,
|
|
63
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
64
|
+
};
|
|
65
|
+
const req = mod.request(opts, (res) => {
|
|
66
|
+
let data = '';
|
|
67
|
+
res.on('data', (chunk) => (data += chunk));
|
|
68
|
+
res.on('end', () => {
|
|
69
|
+
try {
|
|
70
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
71
|
+
} catch {
|
|
72
|
+
resolve({ status: res.statusCode, data: data });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
req.on('error', reject);
|
|
77
|
+
if (body) req.write(JSON.stringify(body));
|
|
78
|
+
req.end();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── State file for API key persistence ──────────────────
|
|
83
|
+
|
|
84
|
+
const STATE_FILE = path.join(os.homedir(), '.swarlo', 'fleet-state.json');
|
|
85
|
+
|
|
86
|
+
function loadState() {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
89
|
+
} catch {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function saveState(state) {
|
|
95
|
+
const dir = path.dirname(STATE_FILE);
|
|
96
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
97
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Auto-register ───────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
async function ensureRegistered() {
|
|
103
|
+
const state = loadState();
|
|
104
|
+
const hubUrl = getHubUrl();
|
|
105
|
+
const hubId = getHubId();
|
|
106
|
+
|
|
107
|
+
// Check if hub is up
|
|
108
|
+
try {
|
|
109
|
+
await request('GET', `${hubUrl}/api/health`);
|
|
110
|
+
} catch {
|
|
111
|
+
console.error('✗ Swarlo hub not running at', hubUrl);
|
|
112
|
+
console.error(' Start it: swarlo serve --port 8090');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Re-use existing key if valid
|
|
117
|
+
if (state.api_key && state.hub_id === hubId) {
|
|
118
|
+
try {
|
|
119
|
+
const check = await request('GET', `${hubUrl}/api/${hubId}/members`, null, {
|
|
120
|
+
Authorization: `Bearer ${state.api_key}`,
|
|
121
|
+
});
|
|
122
|
+
if (check.status === 200) return state;
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Register fresh
|
|
127
|
+
const memberId = `cli-${os.hostname().split('.')[0]}-${process.pid}`;
|
|
128
|
+
const memberName = `CLI (${os.userInfo().username})`;
|
|
129
|
+
const res = await request('POST', `${hubUrl}/api/register`, {
|
|
130
|
+
hub_id: hubId,
|
|
131
|
+
member_id: memberId,
|
|
132
|
+
member_name: memberName,
|
|
133
|
+
member_type: 'agent',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (res.status >= 300) {
|
|
137
|
+
console.error('✗ Failed to register:', res.data);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const newState = {
|
|
142
|
+
api_key: res.data.api_key,
|
|
143
|
+
hub_id: hubId,
|
|
144
|
+
member_id: res.data.member_id,
|
|
145
|
+
hub_url: hubUrl,
|
|
146
|
+
};
|
|
147
|
+
saveState(newState);
|
|
148
|
+
return newState;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function authHeaders(state) {
|
|
152
|
+
return { Authorization: `Bearer ${state.api_key}` };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Commands ────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
async function fleetStatus(state) {
|
|
158
|
+
const hubUrl = getHubUrl();
|
|
159
|
+
const hubId = getHubId();
|
|
160
|
+
const h = authHeaders(state);
|
|
161
|
+
|
|
162
|
+
const [members, posts, claims] = await Promise.all([
|
|
163
|
+
request('GET', `${hubUrl}/api/${hubId}/members`, null, h),
|
|
164
|
+
request('GET', `${hubUrl}/api/${hubId}/channels/general/posts?limit=5`, null, h),
|
|
165
|
+
request('GET', `${hubUrl}/api/${hubId}/claims`, null, h),
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
const m = members.data;
|
|
169
|
+
const p = posts.data;
|
|
170
|
+
const c = claims.data;
|
|
171
|
+
|
|
172
|
+
console.log(`\n ⚡ Swarlo Fleet`);
|
|
173
|
+
console.log(` ─────────────────────────────────`);
|
|
174
|
+
console.log(` Hub: ${hubId} (${hubUrl})`);
|
|
175
|
+
console.log(` You: ${state.member_id}`);
|
|
176
|
+
console.log(` Members: ${m.count || 0}`);
|
|
177
|
+
if (m.members) {
|
|
178
|
+
for (const mem of m.members) {
|
|
179
|
+
const seen = mem.last_seen ? ` (seen ${mem.last_seen.slice(0, 16)})` : '';
|
|
180
|
+
console.log(` ${mem.member_type === 'human' ? '👤' : '🤖'} ${mem.member_name}${seen}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
console.log(` Claims: ${c.count || 0} open`);
|
|
184
|
+
if (c.claims) {
|
|
185
|
+
for (const cl of c.claims) {
|
|
186
|
+
console.log(` [${cl.status}] ${cl.task_key || 'no-key'}: ${(cl.content || '').slice(0, 60)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
console.log(` Recent: ${p.posts?.length || 0} posts`);
|
|
190
|
+
if (p.posts) {
|
|
191
|
+
for (const post of p.posts.slice(0, 3)) {
|
|
192
|
+
console.log(` [${post.member_name}] ${(post.content || '').slice(0, 60)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
console.log();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function fleetPost(state, message) {
|
|
199
|
+
const hubUrl = getHubUrl();
|
|
200
|
+
const hubId = getHubId();
|
|
201
|
+
const res = await request('POST', `${hubUrl}/api/${hubId}/channels/general/posts`, {
|
|
202
|
+
content: message,
|
|
203
|
+
kind: 'message',
|
|
204
|
+
}, authHeaders(state));
|
|
205
|
+
if (res.status < 300) {
|
|
206
|
+
console.log('✓ Posted to general');
|
|
207
|
+
} else {
|
|
208
|
+
console.error('✗ Failed:', res.data);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function fleetTask(state, prompt) {
|
|
213
|
+
const hubUrl = getHubUrl();
|
|
214
|
+
const hubId = getHubId();
|
|
215
|
+
const taskKey = `task:${prompt.slice(0, 40).replace(/[^a-z0-9]/gi, '-').toLowerCase()}`;
|
|
216
|
+
const res = await request('POST', `${hubUrl}/api/${hubId}/channels/general/posts`, {
|
|
217
|
+
content: prompt,
|
|
218
|
+
kind: 'message',
|
|
219
|
+
task_key: taskKey,
|
|
220
|
+
}, authHeaders(state));
|
|
221
|
+
if (res.status < 300) {
|
|
222
|
+
console.log(`✓ Task posted: ${taskKey}`);
|
|
223
|
+
} else {
|
|
224
|
+
console.error('✗ Failed:', res.data);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function fleetClaim(state, taskKey) {
|
|
229
|
+
const hubUrl = getHubUrl();
|
|
230
|
+
const hubId = getHubId();
|
|
231
|
+
const res = await request('POST', `${hubUrl}/api/${hubId}/channels/general/posts`, {
|
|
232
|
+
content: `Claiming: ${taskKey}`,
|
|
233
|
+
kind: 'claim',
|
|
234
|
+
task_key: taskKey,
|
|
235
|
+
}, authHeaders(state));
|
|
236
|
+
if (res.status < 300) {
|
|
237
|
+
console.log(`✓ Claimed: ${taskKey}`);
|
|
238
|
+
} else {
|
|
239
|
+
console.error('✗ Failed:', res.data);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function fleetDone(state, taskKey, result = '') {
|
|
244
|
+
const hubUrl = getHubUrl();
|
|
245
|
+
const hubId = getHubId();
|
|
246
|
+
const res = await request('POST', `${hubUrl}/api/${hubId}/channels/general/posts`, {
|
|
247
|
+
content: result || `Done: ${taskKey}`,
|
|
248
|
+
kind: 'result',
|
|
249
|
+
task_key: taskKey,
|
|
250
|
+
}, authHeaders(state));
|
|
251
|
+
if (res.status < 300) {
|
|
252
|
+
console.log(`✓ Reported done: ${taskKey}`);
|
|
253
|
+
} else {
|
|
254
|
+
console.error('✗ Failed:', res.data);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function fleetMembers(state) {
|
|
259
|
+
const hubUrl = getHubUrl();
|
|
260
|
+
const hubId = getHubId();
|
|
261
|
+
const res = await request('GET', `${hubUrl}/api/${hubId}/members`, null, authHeaders(state));
|
|
262
|
+
if (res.data?.members) {
|
|
263
|
+
console.log(`\n Members (${res.data.count}):`);
|
|
264
|
+
for (const m of res.data.members) {
|
|
265
|
+
const seen = m.last_seen ? m.last_seen.slice(0, 16) : 'never';
|
|
266
|
+
console.log(` ${m.member_type === 'human' ? '👤' : '🤖'} ${m.member_name} (${m.member_id}) — seen: ${seen}`);
|
|
267
|
+
}
|
|
268
|
+
console.log();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function fleetPrune(state, minutes = 60) {
|
|
273
|
+
const hubUrl = getHubUrl();
|
|
274
|
+
const hubId = getHubId();
|
|
275
|
+
const res = await request('POST', `${hubUrl}/api/${hubId}/prune`, {
|
|
276
|
+
stale_minutes: minutes,
|
|
277
|
+
}, authHeaders(state));
|
|
278
|
+
if (res.data?.pruned) {
|
|
279
|
+
console.log(`✓ Pruned ${res.data.count} stale members: ${res.data.pruned.join(', ') || 'none'}`);
|
|
280
|
+
} else {
|
|
281
|
+
console.log('✓ No stale members to prune');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function fleetWatch(state, intervalSec = 10) {
|
|
286
|
+
const hubUrl = getHubUrl();
|
|
287
|
+
const hubId = getHubId();
|
|
288
|
+
const h = authHeaders(state);
|
|
289
|
+
let lastPostId = null;
|
|
290
|
+
|
|
291
|
+
console.log(`\n ⚡ Watching Swarlo board (every ${intervalSec}s, Ctrl+C to stop)\n`);
|
|
292
|
+
|
|
293
|
+
const poll = async () => {
|
|
294
|
+
try {
|
|
295
|
+
const res = await request('GET', `${hubUrl}/api/${hubId}/channels/general/posts?limit=5`, null, h);
|
|
296
|
+
const posts = res.data?.posts || [];
|
|
297
|
+
|
|
298
|
+
if (posts.length > 0 && posts[0].post_id !== lastPostId) {
|
|
299
|
+
// Show new posts (reverse to show oldest first)
|
|
300
|
+
const newPosts = lastPostId
|
|
301
|
+
? posts.filter(p => {
|
|
302
|
+
// Show posts newer than last seen
|
|
303
|
+
return !lastPostId || new Date(p.created_at) > new Date(posts.find(x => x.post_id === lastPostId)?.created_at || 0);
|
|
304
|
+
}).reverse()
|
|
305
|
+
: [posts[0]]; // First poll, just show latest
|
|
306
|
+
|
|
307
|
+
for (const p of newPosts) {
|
|
308
|
+
const time = new Date(p.created_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
309
|
+
const kind = p.kind !== 'message' ? ` [${p.kind}]` : '';
|
|
310
|
+
console.log(` ${time} ${p.member_name}${kind}: ${(p.content || '').slice(0, 100)}`);
|
|
311
|
+
}
|
|
312
|
+
lastPostId = posts[0].post_id;
|
|
313
|
+
}
|
|
314
|
+
} catch {}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
await poll();
|
|
318
|
+
const interval = setInterval(poll, intervalSec * 1000);
|
|
319
|
+
|
|
320
|
+
// Keep alive until Ctrl+C
|
|
321
|
+
await new Promise((resolve) => {
|
|
322
|
+
process.on('SIGINT', () => {
|
|
323
|
+
clearInterval(interval);
|
|
324
|
+
console.log('\n Stopped watching.\n');
|
|
325
|
+
resolve();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Main ────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
async function fleet(args = []) {
|
|
333
|
+
const subcommand = (args[0] || 'status').toLowerCase();
|
|
334
|
+
const rest = args.slice(1).join(' ');
|
|
335
|
+
|
|
336
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
337
|
+
console.log('');
|
|
338
|
+
console.log(' atris fleet — coordinate agent swarm via Swarlo');
|
|
339
|
+
console.log('');
|
|
340
|
+
console.log(' fleet show board status');
|
|
341
|
+
console.log(' fleet post <msg> post message to board');
|
|
342
|
+
console.log(' fleet task <prompt> create a task for agents');
|
|
343
|
+
console.log(' fleet claim <key> claim a task');
|
|
344
|
+
console.log(' fleet done <key> report task complete');
|
|
345
|
+
console.log(' fleet members who is online');
|
|
346
|
+
console.log(' fleet prune [min] remove stale members (default 60m)');
|
|
347
|
+
console.log(' fleet watch [sec] live-tail the board (default 10s)');
|
|
348
|
+
console.log('');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const state = await ensureRegistered();
|
|
353
|
+
|
|
354
|
+
switch (subcommand) {
|
|
355
|
+
case 'status':
|
|
356
|
+
case 'st':
|
|
357
|
+
await fleetStatus(state);
|
|
358
|
+
break;
|
|
359
|
+
case 'post':
|
|
360
|
+
case 'say':
|
|
361
|
+
if (!rest) { console.error('Usage: atris fleet post <message>'); return; }
|
|
362
|
+
await fleetPost(state, rest);
|
|
363
|
+
break;
|
|
364
|
+
case 'task':
|
|
365
|
+
if (!rest) { console.error('Usage: atris fleet task <prompt>'); return; }
|
|
366
|
+
await fleetTask(state, rest);
|
|
367
|
+
break;
|
|
368
|
+
case 'claim':
|
|
369
|
+
if (!rest) { console.error('Usage: atris fleet claim <task_key>'); return; }
|
|
370
|
+
await fleetClaim(state, rest);
|
|
371
|
+
break;
|
|
372
|
+
case 'done':
|
|
373
|
+
case 'report':
|
|
374
|
+
if (!rest) { console.error('Usage: atris fleet done <task_key> [result]'); return; }
|
|
375
|
+
const [key, ...resultParts] = rest.split(' ');
|
|
376
|
+
await fleetDone(state, key, resultParts.join(' '));
|
|
377
|
+
break;
|
|
378
|
+
case 'members':
|
|
379
|
+
case 'who':
|
|
380
|
+
await fleetMembers(state);
|
|
381
|
+
break;
|
|
382
|
+
case 'prune':
|
|
383
|
+
await fleetPrune(state, parseInt(rest) || 60);
|
|
384
|
+
break;
|
|
385
|
+
case 'watch':
|
|
386
|
+
case 'tail':
|
|
387
|
+
await fleetWatch(state, parseInt(rest) || 10);
|
|
388
|
+
break;
|
|
389
|
+
default:
|
|
390
|
+
// If no subcommand match, treat the whole thing as status
|
|
391
|
+
await fleetStatus(state);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = { fleet };
|
package/commands/fork.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { loadCredentials } = require('../utils/auth');
|
|
5
|
+
const { apiRequestJson } = require('../utils/api');
|
|
6
|
+
|
|
7
|
+
async function forkAtris() {
|
|
8
|
+
const template = process.argv[3];
|
|
9
|
+
const targetArg = process.argv[4];
|
|
10
|
+
|
|
11
|
+
if (!template || template === '--help') {
|
|
12
|
+
console.log('Usage: atris fork <template> [target-dir]');
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(' atris fork music-artist Fork the music-artist template');
|
|
15
|
+
console.log(' atris fork event-promoter myband Fork into ./myband/');
|
|
16
|
+
console.log(' atris fork pallet Fork from a business slug');
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log('Templates can be a name, business slug, or URL.');
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const creds = loadCredentials();
|
|
23
|
+
if (!creds || !creds.token) {
|
|
24
|
+
console.error('Not logged in. Run: atris login');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Resolve target directory
|
|
29
|
+
const targetName = targetArg && !targetArg.startsWith('-') ? targetArg : template;
|
|
30
|
+
const targetDir = path.resolve(process.cwd(), targetName);
|
|
31
|
+
|
|
32
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
33
|
+
console.error(`Directory "${targetName}" already exists and is not empty.`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(`Forking template "${template}"...`);
|
|
39
|
+
|
|
40
|
+
// Try API first
|
|
41
|
+
let files = null;
|
|
42
|
+
const downloadResult = await apiRequestJson(
|
|
43
|
+
`/workspace/templates/${encodeURIComponent(template)}/download`,
|
|
44
|
+
{ method: 'GET', token: creds.token }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (downloadResult.ok && downloadResult.data && downloadResult.data.files) {
|
|
48
|
+
files = downloadResult.data.files;
|
|
49
|
+
} else {
|
|
50
|
+
// Fall back to local template at ~/.atris/templates/{template}/
|
|
51
|
+
const localTemplatePath = path.join(os.homedir(), '.atris', 'templates', template);
|
|
52
|
+
if (fs.existsSync(localTemplatePath)) {
|
|
53
|
+
console.log(' API unavailable, using local template...');
|
|
54
|
+
files = readLocalTemplate(localTemplatePath, localTemplatePath);
|
|
55
|
+
} else {
|
|
56
|
+
const msg = downloadResult.errorMessage || downloadResult.error || `HTTP ${downloadResult.status}`;
|
|
57
|
+
console.error(`\n Template "${template}" not found. ${msg}`);
|
|
58
|
+
console.error(' Check available templates or provide a valid business slug.');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!files || files.length === 0) {
|
|
64
|
+
console.error(`\n Template "${template}" is empty.`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create target directory and .atris metadata
|
|
69
|
+
const atrisDir = path.join(targetDir, '.atris');
|
|
70
|
+
fs.mkdirSync(atrisDir, { recursive: true });
|
|
71
|
+
|
|
72
|
+
// Write .atris/business.json pointing to template source
|
|
73
|
+
fs.writeFileSync(path.join(atrisDir, 'business.json'), JSON.stringify({
|
|
74
|
+
slug: targetName,
|
|
75
|
+
forked_from: template,
|
|
76
|
+
}, null, 2));
|
|
77
|
+
|
|
78
|
+
// Write .atris/fork.json with metadata
|
|
79
|
+
fs.writeFileSync(path.join(atrisDir, 'fork.json'), JSON.stringify({
|
|
80
|
+
forked_from: template,
|
|
81
|
+
forked_at: new Date().toISOString(),
|
|
82
|
+
version: '1.0.0',
|
|
83
|
+
}, null, 2));
|
|
84
|
+
|
|
85
|
+
// Write all template files to disk
|
|
86
|
+
let written = 0;
|
|
87
|
+
console.log('');
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
if (!file.path || file.content === null || file.content === undefined) continue;
|
|
90
|
+
const relPath = file.path.replace(/^\//, '');
|
|
91
|
+
const filePath = path.join(targetDir, relPath);
|
|
92
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
93
|
+
fs.writeFileSync(filePath, file.content);
|
|
94
|
+
console.log(` + ${relPath}`);
|
|
95
|
+
written++;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Summary
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(` ${written} file${written !== 1 ? 's' : ''} written to ${targetName}/`);
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(' Next steps:');
|
|
103
|
+
console.log(' 1. Customize your context files');
|
|
104
|
+
console.log(' 2. Run: atris push to go live');
|
|
105
|
+
console.log('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readLocalTemplate(baseDir, currentDir) {
|
|
109
|
+
const files = [];
|
|
110
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
113
|
+
if (entry.isDirectory()) {
|
|
114
|
+
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
115
|
+
files.push(...readLocalTemplate(baseDir, fullPath));
|
|
116
|
+
} else {
|
|
117
|
+
try {
|
|
118
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
119
|
+
const relPath = '/' + path.relative(baseDir, fullPath);
|
|
120
|
+
files.push({ path: relPath, content });
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return files;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { forkAtris };
|
package/commands/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { ensureExperimentsFramework } = require('./experiments');
|
|
4
|
+
const { ensureWikiScaffold } = require('../lib/wiki');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Detect project context by scanning project structure
|
|
@@ -232,7 +233,48 @@ ${profile.hasCode ? `**Validation:** Run \`${profile.testCommand}\` to verify ch
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
function initAtris() {
|
|
235
|
-
|
|
236
|
+
// GUARD: Refuse nested init.
|
|
237
|
+
// Bug: running `atris init` inside an existing `atris/` folder creates
|
|
238
|
+
// `atris/atris/` nesting hell. Cloud doordash had this exact problem.
|
|
239
|
+
// Fix: detect three nesting conditions and refuse with a clear error.
|
|
240
|
+
const cwd = process.cwd();
|
|
241
|
+
const cwdBase = path.basename(cwd);
|
|
242
|
+
const force = process.argv.includes('--force');
|
|
243
|
+
|
|
244
|
+
if (cwdBase === 'atris' && !force) {
|
|
245
|
+
console.error('✗ Cannot run atris init inside an atris/ directory.');
|
|
246
|
+
console.error(' You appear to be inside the atris/ folder of an existing workspace.');
|
|
247
|
+
console.error(' Run init from the parent directory, or use --force to proceed anyway.');
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check cwd itself for .atris/business.json — already a business workspace
|
|
252
|
+
const cwdBusinessJson = path.join(cwd, '.atris', 'business.json');
|
|
253
|
+
if (fs.existsSync(cwdBusinessJson) && !force) {
|
|
254
|
+
console.error('✗ This directory is already a business workspace (found .atris/business.json).');
|
|
255
|
+
console.error(' To update canonical files: atris update');
|
|
256
|
+
console.error(' To re-init anyway: atris init --force');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Walk up to 6 parent dirs looking for an .atris/business.json — if found, we're inside a workspace
|
|
261
|
+
let walker = path.dirname(cwd);
|
|
262
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
263
|
+
const businessJson = path.join(walker, '.atris', 'business.json');
|
|
264
|
+
if (fs.existsSync(businessJson)) {
|
|
265
|
+
if (!force) {
|
|
266
|
+
console.error(`✗ Cannot run atris init: parent directory ${walker} is already an atris workspace.`);
|
|
267
|
+
console.error(' Found .atris/business.json in a parent directory.');
|
|
268
|
+
console.error(' Run init from outside the workspace, or use --force to proceed anyway.');
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const parent = path.dirname(walker);
|
|
273
|
+
if (parent === walker) break;
|
|
274
|
+
walker = parent;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const targetDir = path.join(cwd, 'atris');
|
|
236
278
|
const teamDir = path.join(targetDir, 'team');
|
|
237
279
|
const legacyAgentTeamDir = path.join(targetDir, 'agent_team');
|
|
238
280
|
const sourceFile = path.join(__dirname, '..', 'atris.md');
|
|
@@ -304,6 +346,13 @@ function initAtris() {
|
|
|
304
346
|
console.log('✓ Created PERSONA.md');
|
|
305
347
|
}
|
|
306
348
|
|
|
349
|
+
const wikiDir = path.join(targetDir, 'wiki');
|
|
350
|
+
const wikiAlreadyExists = fs.existsSync(wikiDir);
|
|
351
|
+
ensureWikiScaffold(process.cwd());
|
|
352
|
+
if (!wikiAlreadyExists) {
|
|
353
|
+
console.log('✓ Created wiki/ scaffold');
|
|
354
|
+
}
|
|
355
|
+
|
|
307
356
|
if (!fs.existsSync(mapFile)) {
|
|
308
357
|
fs.writeFileSync(mapFile, '# MAP.md\n\n> Generated by your AI agent after reading atris.md\n\nRun your AI agent with atris.md to populate this file.\n');
|
|
309
358
|
console.log('✓ Created MAP.md placeholder');
|