atris 3.40.0 → 3.41.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/bin/atris.js +1 -1
- package/commands/computer.js +20 -0
- package/commands/site-deploy.js +19 -4
- package/commands/team.js +95 -2
- package/package.json +1 -1
package/bin/atris.js
CHANGED
|
@@ -2732,7 +2732,7 @@ if (command === 'init') {
|
|
|
2732
2732
|
const subcommand = process.argv[3];
|
|
2733
2733
|
const args = process.argv.slice(4);
|
|
2734
2734
|
require('../commands/business').businessCommand(subcommand, ...args)
|
|
2735
|
-
.then(() => process.exit(0))
|
|
2735
|
+
.then(() => process.exit(process.exitCode || 0))
|
|
2736
2736
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2737
2737
|
} else if (command === 'soul') {
|
|
2738
2738
|
const args = process.argv.slice(3);
|
package/commands/computer.js
CHANGED
|
@@ -4384,6 +4384,7 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
|
|
|
4384
4384
|
default:
|
|
4385
4385
|
console.error(`Unknown subcommand: ${sub}`);
|
|
4386
4386
|
console.log('Run: atris computer --help');
|
|
4387
|
+
process.exitCode = 1;
|
|
4387
4388
|
}
|
|
4388
4389
|
}
|
|
4389
4390
|
|
|
@@ -4399,4 +4400,23 @@ module.exports = {
|
|
|
4399
4400
|
extractAttachedWorkspaceMismatch,
|
|
4400
4401
|
contextForAttachedWorkspaceMismatch,
|
|
4401
4402
|
printRecruitingLocalSyncOutcome,
|
|
4403
|
+
// Hermetic parsing/formatting layer, exported for test/computer.test.js.
|
|
4404
|
+
parseComputerOptions,
|
|
4405
|
+
parseComputerCreateArgs,
|
|
4406
|
+
computerCreateArgsHaveName,
|
|
4407
|
+
normalizeComputerType,
|
|
4408
|
+
formatComputerTypeList,
|
|
4409
|
+
parseComputerDeleteArgs,
|
|
4410
|
+
parseComputerCardArgs,
|
|
4411
|
+
renderComputerCard,
|
|
4412
|
+
renderComputerCardMarkdown,
|
|
4413
|
+
formatLeaseAge,
|
|
4414
|
+
formatWorkspaceRef,
|
|
4415
|
+
workspaceMatchesInput,
|
|
4416
|
+
resolveWorkspaceFromList,
|
|
4417
|
+
workspaceMatchesComputerType,
|
|
4418
|
+
looksLikeWorkspaceId,
|
|
4419
|
+
shellQuote,
|
|
4420
|
+
withoutRecruitingWrapperFlags,
|
|
4421
|
+
formatCloudSelection,
|
|
4402
4422
|
};
|
package/commands/site-deploy.js
CHANGED
|
@@ -221,10 +221,21 @@ function parseResponseData(result) {
|
|
|
221
221
|
try { return JSON.parse(text); } catch { return null; }
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
function validationDetail(items) {
|
|
225
|
+
const messages = items.map((item) => {
|
|
226
|
+
if (!item || typeof item !== 'object') return String(item);
|
|
227
|
+
const message = item.msg || item.message || item.type || 'validation failed';
|
|
228
|
+
const location = Array.isArray(item.loc) ? item.loc.join('.') : '';
|
|
229
|
+
return location ? `${location}: ${message}` : message;
|
|
230
|
+
});
|
|
231
|
+
return messages.join('; ');
|
|
232
|
+
}
|
|
233
|
+
|
|
224
234
|
function errorDetail(result) {
|
|
225
235
|
const data = parseResponseData(result);
|
|
226
236
|
if (data && typeof data === 'object') {
|
|
227
237
|
const detail = data.detail || data.error || data.message;
|
|
238
|
+
if (Array.isArray(detail)) return validationDetail(detail);
|
|
228
239
|
if (detail) return typeof detail === 'string' ? detail : JSON.stringify(detail);
|
|
229
240
|
}
|
|
230
241
|
return responseText(result).trim() || 'request failed';
|
|
@@ -314,9 +325,13 @@ async function uploadPages(sitesUrl, slug, pages, token, deps = {}) {
|
|
|
314
325
|
const url = `${sitesUrl}/${slug}/pages`;
|
|
315
326
|
for (let start = 0; start < pages.length; start += BATCH_SIZE) {
|
|
316
327
|
const batch = pages.slice(start, start + BATCH_SIZE);
|
|
317
|
-
const result = await requestJson(
|
|
318
|
-
|
|
319
|
-
|
|
328
|
+
const result = await requestJson(
|
|
329
|
+
'PUT',
|
|
330
|
+
url,
|
|
331
|
+
token,
|
|
332
|
+
batch.map((page) => pagePayload(page, fileSystem)),
|
|
333
|
+
deps,
|
|
334
|
+
);
|
|
320
335
|
if (result.status < 200 || result.status >= 300) throw requestError('PUT', url, result);
|
|
321
336
|
for (const page of batch) log(` published ${page.path} (${formatBytes(page.size)})`);
|
|
322
337
|
}
|
|
@@ -846,13 +861,13 @@ async function run(argv, deps = {}) {
|
|
|
846
861
|
log(`\n deploying ${pages.length} file${pages.length === 1 ? '' : 's'} to ${liveUrl}`);
|
|
847
862
|
try {
|
|
848
863
|
await createSite(sitesUrl, options.name, options.spa, credentials.token, { ...deps, log });
|
|
864
|
+
await registerSubdomain(options.name, { ...deps, log });
|
|
849
865
|
await uploadPages(sitesUrl, options.name, pages, credentials.token, { ...deps, log });
|
|
850
866
|
} catch (error) {
|
|
851
867
|
errorLog(` deploy failed: ${error.message}`);
|
|
852
868
|
return 1;
|
|
853
869
|
}
|
|
854
870
|
|
|
855
|
-
await registerSubdomain(options.name, { ...deps, log });
|
|
856
871
|
log(`\n live at ${liveUrl}`);
|
|
857
872
|
return 0;
|
|
858
873
|
}
|
package/commands/team.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
|
|
5
6
|
const { canonicalEngineName } = require('../lib/engine-registry');
|
|
@@ -115,12 +116,83 @@ function renderTeamRoster(roster) {
|
|
|
115
116
|
.join('\n');
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
// The pruning pass keeps the team lean like a real company: it flags members
|
|
120
|
+
// with no recent signal, and it never deletes anything. A signal is the newest
|
|
121
|
+
// of MEMBER.md, any logs/*.md, or a mission the member owns that is still
|
|
122
|
+
// active or running.
|
|
123
|
+
const PRUNE_ACTIVE_MISSION_STATUSES = new Set(['active', 'running']);
|
|
124
|
+
const DEFAULT_PRUNE_DAYS = 30;
|
|
125
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
126
|
+
|
|
127
|
+
function newestSignalMs(member) {
|
|
128
|
+
const times = [];
|
|
129
|
+
const stamp = (file) => {
|
|
130
|
+
try { times.push(fs.statSync(file).mtimeMs); } catch { /* missing file is just no signal */ }
|
|
131
|
+
};
|
|
132
|
+
if (member?.path) stamp(member.path);
|
|
133
|
+
if (member?.dir) {
|
|
134
|
+
const logsDir = path.join(member.dir, 'logs');
|
|
135
|
+
let entries = [];
|
|
136
|
+
try { entries = fs.readdirSync(logsDir); } catch { entries = []; }
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (entry.endsWith('.md')) stamp(path.join(logsDir, entry));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return times.length ? Math.max(...times) : 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectTeamPrune(deps = {}) {
|
|
145
|
+
const root = deps.root || repoRoot(deps.cwd || process.cwd());
|
|
146
|
+
const days = Number.isFinite(deps.days) && deps.days > 0 ? deps.days : DEFAULT_PRUNE_DAYS;
|
|
147
|
+
const nowMs = typeof deps.now === 'function' ? deps.now() : Date.now();
|
|
148
|
+
const activeOwners = new Set();
|
|
149
|
+
for (const mission of collectMissions(root, deps)) {
|
|
150
|
+
if (!PRUNE_ACTIVE_MISSION_STATUSES.has(String(mission?.status || '').toLowerCase())) continue;
|
|
151
|
+
const owner = String(mission?.owner || mission?.member || '').trim().toLowerCase();
|
|
152
|
+
if (owner) activeOwners.add(owner);
|
|
153
|
+
}
|
|
154
|
+
const quiet = [];
|
|
155
|
+
let activeCount = 0;
|
|
156
|
+
for (const member of collectMembers(root, deps)) {
|
|
157
|
+
const name = String(member?.name || '').trim().toLowerCase();
|
|
158
|
+
if (!name) continue;
|
|
159
|
+
const signalMs = newestSignalMs(member);
|
|
160
|
+
if (activeOwners.has(name) || (signalMs && nowMs - signalMs < days * DAY_MS)) {
|
|
161
|
+
activeCount += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
quiet.push({
|
|
165
|
+
name,
|
|
166
|
+
days_quiet: signalMs ? Math.floor((nowMs - signalMs) / DAY_MS) : null,
|
|
167
|
+
last_signal: signalMs ? new Date(signalMs).toISOString() : null,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
quiet.sort((a, b) => a.name.localeCompare(b.name));
|
|
171
|
+
return { quiet, active_count: activeCount };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderTeamPrune(report, days = DEFAULT_PRUNE_DAYS) {
|
|
175
|
+
if (!report.quiet.length && !report.active_count) {
|
|
176
|
+
return 'no team members yet. create one with: atris member create <name> --role="..."';
|
|
177
|
+
}
|
|
178
|
+
if (!report.quiet.length) {
|
|
179
|
+
return `everyone on the team has a signal newer than ${days} days. nothing to prune.`;
|
|
180
|
+
}
|
|
181
|
+
const lines = report.quiet.map((entry) => (entry.days_quiet === null
|
|
182
|
+
? `${entry.name} has no recorded activity; keep, hand off, or retire.`
|
|
183
|
+
: `${entry.name} has been quiet for ${entry.days_quiet} days; keep, hand off, or retire.`));
|
|
184
|
+
lines.push(`${report.active_count} member${report.active_count === 1 ? ' is' : 's are'} still active. nothing was deleted; this is a report.`);
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
118
188
|
function helpText() {
|
|
119
189
|
return [
|
|
120
190
|
'atris team - one team view: every member, their role, and any engine running their work',
|
|
121
191
|
'atris team presence - show who is awake and what they are doing',
|
|
192
|
+
'atris team prune - flag members with no recent activity; deletes nothing',
|
|
122
193
|
'',
|
|
123
194
|
'usage: atris team [roster|presence] [--json]',
|
|
195
|
+
'usage: atris team prune [--days N] [--json]',
|
|
124
196
|
].join('\n');
|
|
125
197
|
}
|
|
126
198
|
|
|
@@ -129,6 +201,27 @@ function teamCommand(args = [], deps = {}) {
|
|
|
129
201
|
(deps.write || process.stdout.write.bind(process.stdout))(`${helpText()}\n`);
|
|
130
202
|
return 0;
|
|
131
203
|
}
|
|
204
|
+
if (args[0] === 'prune') {
|
|
205
|
+
const rest = args.slice(1);
|
|
206
|
+
let days = DEFAULT_PRUNE_DAYS;
|
|
207
|
+
let json = false;
|
|
208
|
+
let bad = false;
|
|
209
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
210
|
+
const arg = rest[i];
|
|
211
|
+
if (arg === '--json') { json = true; continue; }
|
|
212
|
+
if (arg === '--days') { i += 1; days = Number(rest[i]); continue; }
|
|
213
|
+
if (arg.startsWith('--days=')) { days = Number(arg.slice('--days='.length)); continue; }
|
|
214
|
+
bad = true;
|
|
215
|
+
}
|
|
216
|
+
if (bad || !Number.isFinite(days) || days <= 0) {
|
|
217
|
+
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team prune [--days N] [--json]\n');
|
|
218
|
+
return 2;
|
|
219
|
+
}
|
|
220
|
+
const report = deps.prune || collectTeamPrune({ ...deps, days });
|
|
221
|
+
const output = json ? JSON.stringify(report, null, 2) : renderTeamPrune(report, days);
|
|
222
|
+
(deps.write || process.stdout.write.bind(process.stdout))(`${output}\n`);
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
132
225
|
const rosterArgs = args.filter((arg) => arg !== 'roster');
|
|
133
226
|
if (args[0] !== 'presence' && rosterArgs.every((arg) => arg === '--json')) {
|
|
134
227
|
const roster = deps.roster || collectTeamRoster(deps);
|
|
@@ -139,7 +232,7 @@ function teamCommand(args = [], deps = {}) {
|
|
|
139
232
|
return 0;
|
|
140
233
|
}
|
|
141
234
|
if (args[0] !== 'presence' || args.some((arg, index) => index > 0 && arg !== '--json')) {
|
|
142
|
-
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team [roster|presence] [--json]\n');
|
|
235
|
+
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team [roster|presence|prune] [--json]\n');
|
|
143
236
|
return 2;
|
|
144
237
|
}
|
|
145
238
|
const presence = deps.presence || collectTeamPresence(deps);
|
|
@@ -150,4 +243,4 @@ function teamCommand(args = [], deps = {}) {
|
|
|
150
243
|
return 0;
|
|
151
244
|
}
|
|
152
245
|
|
|
153
|
-
module.exports = { collectMissions, collectTasks, collectTeamPresence, collectTeamRoster, renderTeamRoster, teamCommand };
|
|
246
|
+
module.exports = { collectMissions, collectTasks, collectTeamPresence, collectTeamPrune, collectTeamRoster, renderTeamPrune, renderTeamRoster, teamCommand };
|