atris 3.15.13 → 3.15.22
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/AGENTS.md +84 -8
- package/README.md +5 -1
- package/atris/AGENTS.md +46 -1
- package/atris/CLAUDE.md +36 -1
- package/atris/GEMINI.md +14 -1
- package/atris/atris.md +12 -1
- package/atris/atrisDev.md +3 -2
- package/atris/context/README.md +11 -0
- package/atris/features/company-brain-sync/validate.md +5 -5
- package/atris/learnings.jsonl +1 -0
- package/atris/policies/atris-design.md +2 -0
- package/atris/skills/aeo/SKILL.md +2 -2
- package/atris/skills/atris/SKILL.md +15 -62
- package/atris/skills/design/SKILL.md +2 -0
- package/atris/skills/imessage/SKILL.md +19 -2
- package/atris/skills/loop/SKILL.md +6 -5
- package/atris/skills/magic-inbox/SKILL.md +1 -1
- package/atris/team/_template/MEMBER.md +23 -1
- package/atris/team/brainstormer/START_HERE.md +6 -0
- package/atris/team/executor/MEMBER.md +13 -0
- package/atris/team/executor/START_HERE.md +6 -0
- package/atris/team/launcher/START_HERE.md +6 -0
- package/atris/team/mission-lead/MEMBER.md +39 -0
- package/atris/team/mission-lead/MISSION.md +33 -0
- package/atris/team/mission-lead/START_HERE.md +6 -0
- package/atris/team/navigator/MEMBER.md +11 -0
- package/atris/team/navigator/START_HERE.md +6 -0
- package/atris/team/opus-overnight/MEMBER.md +39 -0
- package/atris/team/opus-overnight/MISSION.md +61 -0
- package/atris/team/opus-overnight/START_HERE.md +6 -0
- package/atris/team/opus-overnight/STEERING.md +35 -0
- package/atris/team/researcher/START_HERE.md +6 -0
- package/atris/team/validator/MEMBER.md +26 -6
- package/atris/team/validator/START_HERE.md +6 -0
- package/atris/wiki/concepts/agent-activation-contract.md +79 -0
- package/atris/wiki/concepts/workspace-initialization-contract.md +73 -0
- package/atris/wiki/index.md +27 -0
- package/atris/wiki/sources/atris-labs-2026-05-10.txt +17 -0
- package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +15 -0
- package/atris/wiki/sources/atrisos-generative-ui-product-surface-2026-05-10.txt +10 -0
- package/atris/wiki/sources/jack-dorsey-2026-05-10.txt +12 -0
- package/atris.md +49 -13
- package/bin/atris.js +660 -22
- package/commands/activate.js +12 -3
- package/commands/aeo.js +1 -1
- package/commands/align.js +10 -10
- package/commands/analytics.js +9 -4
- package/commands/app.js +2 -0
- package/commands/apps.js +276 -0
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +74 -5
- package/commands/brain.js +536 -61
- package/commands/brainstorm.js +12 -12
- package/commands/business-sync.js +142 -24
- package/commands/clean.js +9 -6
- package/commands/codex-goal.js +311 -0
- package/commands/errors.js +11 -1
- package/commands/feedback.js +55 -17
- package/commands/fork.js +2 -2
- package/commands/gm.js +376 -0
- package/commands/init.js +80 -3
- package/commands/integrations.js +524 -0
- package/commands/learn.js +25 -16
- package/commands/lesson.js +41 -0
- package/commands/lifecycle.js +2 -2
- package/commands/member.js +2416 -9
- package/commands/mission.js +1776 -0
- package/commands/now.js +48 -7
- package/commands/play.js +425 -0
- package/commands/publish.js +2 -1
- package/commands/pull.js +72 -29
- package/commands/push.js +199 -17
- package/commands/review.js +51 -13
- package/commands/skill.js +2 -2
- package/commands/soul.js +19 -13
- package/commands/status.js +6 -1
- package/commands/sync.js +5 -4
- package/commands/task.js +1041 -147
- package/commands/terminal.js +5 -5
- package/commands/verify.js +7 -5
- package/commands/visualize.js +7 -0
- package/commands/wiki.js +53 -16
- package/commands/workflow.js +298 -54
- package/commands/workspace-clean.js +1 -1
- package/commands/worktree.js +468 -0
- package/commands/xp.js +1608 -0
- package/lib/manifest.js +34 -4
- package/lib/scorecard.js +3 -2
- package/lib/task-db.js +408 -27
- package/lib/todo-fallback.js +28 -2
- package/lib/todo.js +5 -3
- package/package.json +23 -2
- package/utils/update-check.js +51 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const SCHEMA = 'atris.codex_goal.v1';
|
|
7
|
+
const CONFIRM_RESET_FLAG = '--confirm-complete-goal-reset';
|
|
8
|
+
|
|
9
|
+
function hasFlag(args, name) {
|
|
10
|
+
return args.includes(name);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readFlag(args, name, fallback = '') {
|
|
14
|
+
const index = args.indexOf(name);
|
|
15
|
+
if (index === -1 || index + 1 >= args.length) return fallback;
|
|
16
|
+
return args[index + 1];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function expandHome(filePath) {
|
|
20
|
+
if (!filePath) return filePath;
|
|
21
|
+
if (filePath === '~') return os.homedir();
|
|
22
|
+
if (filePath.startsWith('~/')) return path.join(os.homedir(), filePath.slice(2));
|
|
23
|
+
return filePath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeStamp(value = new Date().toISOString()) {
|
|
27
|
+
return value.replace(/[:.]/g, '-');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sqlString(value) {
|
|
31
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveStatePath(args = []) {
|
|
35
|
+
return path.resolve(expandHome(readFlag(args, '--state', process.env.CODEX_STATE_DB || path.join(os.homedir(), '.codex', 'state_5.sqlite'))));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function defaultRunsDir(args = []) {
|
|
39
|
+
return path.resolve(readFlag(args, '--out-dir', path.join(process.cwd(), '.atris', 'runs')));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensurePrivateDir(dir) {
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
try {
|
|
45
|
+
fs.chmodSync(dir, 0o700);
|
|
46
|
+
} catch {
|
|
47
|
+
// Best effort: some filesystems do not support POSIX permissions.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function chmodPrivate(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
fs.chmodSync(filePath, 0o600);
|
|
54
|
+
} catch {
|
|
55
|
+
// Best effort: some filesystems do not support POSIX permissions.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function runSqliteJson(dbPath, sql, { readonly = true } = {}) {
|
|
60
|
+
const sqliteArgs = [];
|
|
61
|
+
if (readonly) sqliteArgs.push('-readonly');
|
|
62
|
+
sqliteArgs.push('-json', dbPath, sql);
|
|
63
|
+
const result = spawnSync('sqlite3', sqliteArgs, { encoding: 'utf8' });
|
|
64
|
+
if (result.error) throw new Error(`sqlite3 failed: ${result.error.message}`);
|
|
65
|
+
if (result.status !== 0) {
|
|
66
|
+
const detail = (result.stderr || result.stdout || '').trim();
|
|
67
|
+
throw new Error(detail || `sqlite3 exited with status ${result.status}`);
|
|
68
|
+
}
|
|
69
|
+
const out = String(result.stdout || '').trim();
|
|
70
|
+
if (!out) return [];
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(out);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new Error(`sqlite3 returned invalid JSON: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ensureStateDb(dbPath) {
|
|
79
|
+
if (!fs.existsSync(dbPath)) {
|
|
80
|
+
throw new Error(`Codex state database not found: ${dbPath}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function selectGoalSql(whereClause, limit = 10) {
|
|
85
|
+
return `
|
|
86
|
+
SELECT
|
|
87
|
+
tg.thread_id,
|
|
88
|
+
tg.goal_id,
|
|
89
|
+
tg.objective,
|
|
90
|
+
tg.status,
|
|
91
|
+
tg.token_budget,
|
|
92
|
+
tg.tokens_used,
|
|
93
|
+
tg.time_used_seconds,
|
|
94
|
+
tg.created_at_ms,
|
|
95
|
+
tg.updated_at_ms,
|
|
96
|
+
t.cwd AS thread_cwd,
|
|
97
|
+
t.title AS thread_title,
|
|
98
|
+
t.updated_at_ms AS thread_updated_at_ms
|
|
99
|
+
FROM thread_goals tg
|
|
100
|
+
LEFT JOIN threads t ON t.id = tg.thread_id
|
|
101
|
+
${whereClause}
|
|
102
|
+
ORDER BY COALESCE(t.updated_at_ms, tg.updated_at_ms) DESC
|
|
103
|
+
LIMIT ${Number(limit) || 10}
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readGoalByThread(dbPath, threadId) {
|
|
108
|
+
const rows = runSqliteJson(dbPath, selectGoalSql(`WHERE tg.thread_id = ${sqlString(threadId)}`, 1));
|
|
109
|
+
return rows[0] || null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readLatestGoalForCwd(dbPath, cwd) {
|
|
113
|
+
const realCwd = fs.realpathSync.native ? fs.realpathSync.native(cwd) : fs.realpathSync(cwd);
|
|
114
|
+
const pwd = process.env.PWD || '';
|
|
115
|
+
const pwdReal = pwd && fs.existsSync(pwd) ? (fs.realpathSync.native ? fs.realpathSync.native(pwd) : fs.realpathSync(pwd)) : '';
|
|
116
|
+
const candidates = [...new Set([cwd, realCwd, pwdReal === realCwd ? pwd : ''].filter(Boolean))];
|
|
117
|
+
const rows = runSqliteJson(dbPath, selectGoalSql(`WHERE t.cwd IN (${candidates.map(sqlString).join(', ')})`, 1));
|
|
118
|
+
return rows[0] || null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readRecentGoals(dbPath, limit = 10) {
|
|
122
|
+
return runSqliteJson(dbPath, selectGoalSql('', limit));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveThreadGoal(dbPath, args) {
|
|
126
|
+
const explicitThread = readFlag(args, '--thread', '');
|
|
127
|
+
if (explicitThread) return readGoalByThread(dbPath, explicitThread);
|
|
128
|
+
if (hasFlag(args, '--latest')) return readLatestGoalForCwd(dbPath, process.cwd());
|
|
129
|
+
const envThread = process.env.CODEX_THREAD_ID || '';
|
|
130
|
+
if (envThread) return readGoalByThread(dbPath, envThread);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeReceipt(outDir, payload) {
|
|
135
|
+
ensurePrivateDir(outDir);
|
|
136
|
+
const receiptPath = path.join(outDir, `codex-goal-${payload.action}-${safeStamp(payload.finished_at || payload.started_at)}.json`);
|
|
137
|
+
const withPath = { ...payload, receipt_path: receiptPath };
|
|
138
|
+
fs.writeFileSync(receiptPath, `${JSON.stringify(withPath, null, 2)}\n`, 'utf8');
|
|
139
|
+
chmodPrivate(receiptPath);
|
|
140
|
+
return withPath;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function backupSqliteDb(dbPath, backupPath) {
|
|
144
|
+
const result = spawnSync('sqlite3', [dbPath, `VACUUM INTO ${sqlString(backupPath)};`], { encoding: 'utf8' });
|
|
145
|
+
if (result.error) throw new Error(`sqlite3 backup failed: ${result.error.message}`);
|
|
146
|
+
if (result.status !== 0) {
|
|
147
|
+
const detail = (result.stderr || result.stdout || '').trim();
|
|
148
|
+
throw new Error(detail || `sqlite3 backup exited with status ${result.status}`);
|
|
149
|
+
}
|
|
150
|
+
chmodPrivate(backupPath);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function printJsonOrText(payload, lines, asJson) {
|
|
154
|
+
if (asJson) {
|
|
155
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log(lines.join('\n'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function statusCommand(args) {
|
|
162
|
+
const asJson = hasFlag(args, '--json');
|
|
163
|
+
const dbPath = resolveStatePath(args);
|
|
164
|
+
ensureStateDb(dbPath);
|
|
165
|
+
|
|
166
|
+
const goal = resolveThreadGoal(dbPath, args);
|
|
167
|
+
if (goal) {
|
|
168
|
+
const payload = { ok: true, schema: SCHEMA, action: 'status', state_path: dbPath, goal };
|
|
169
|
+
printJsonOrText(payload, [
|
|
170
|
+
`Codex goal: ${goal.status}`,
|
|
171
|
+
`Thread: ${goal.thread_id}`,
|
|
172
|
+
`Objective: ${goal.objective}`,
|
|
173
|
+
`Tokens/time: ${goal.tokens_used || 0} tokens, ${goal.time_used_seconds || 0}s`,
|
|
174
|
+
], asJson);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const limit = Math.max(1, Math.min(50, Number(readFlag(args, '--limit', '10')) || 10));
|
|
179
|
+
const goals = readRecentGoals(dbPath, limit);
|
|
180
|
+
const payload = { ok: true, schema: SCHEMA, action: 'status', state_path: dbPath, goals };
|
|
181
|
+
printJsonOrText(payload, [
|
|
182
|
+
`Codex goals: ${goals.length} recent`,
|
|
183
|
+
...goals.map((row) => `- ${row.status} ${row.thread_id}: ${row.objective}`),
|
|
184
|
+
'Reset requires --thread <id> or --latest.',
|
|
185
|
+
], asJson);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resetCommand(args) {
|
|
189
|
+
const asJson = hasFlag(args, '--json');
|
|
190
|
+
const dbPath = resolveStatePath(args);
|
|
191
|
+
const outDir = defaultRunsDir(args);
|
|
192
|
+
ensureStateDb(dbPath);
|
|
193
|
+
|
|
194
|
+
const startedAt = new Date().toISOString();
|
|
195
|
+
const goal = resolveThreadGoal(dbPath, args);
|
|
196
|
+
if (!goal) {
|
|
197
|
+
throw new Error('No Codex goal found. Pass --thread <thread-id> or --latest.');
|
|
198
|
+
}
|
|
199
|
+
if (goal.status !== 'complete') {
|
|
200
|
+
throw new Error(`Refusing to reset ${goal.status} goal. Complete the native Codex goal first.`);
|
|
201
|
+
}
|
|
202
|
+
if (!hasFlag(args, CONFIRM_RESET_FLAG)) {
|
|
203
|
+
const payload = {
|
|
204
|
+
ok: false,
|
|
205
|
+
schema: SCHEMA,
|
|
206
|
+
action: 'reset',
|
|
207
|
+
status: 'needs_confirmation',
|
|
208
|
+
state_path: dbPath,
|
|
209
|
+
goal,
|
|
210
|
+
required_flag: CONFIRM_RESET_FLAG,
|
|
211
|
+
finished_at: new Date().toISOString(),
|
|
212
|
+
};
|
|
213
|
+
printJsonOrText(payload, [
|
|
214
|
+
'Codex goal reset blocked: confirmation required.',
|
|
215
|
+
`Thread: ${goal.thread_id}`,
|
|
216
|
+
`Objective: ${goal.objective}`,
|
|
217
|
+
`Run again with ${CONFIRM_RESET_FLAG} to back up state and clear this completed goal slot.`,
|
|
218
|
+
], asJson);
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
ensurePrivateDir(outDir);
|
|
224
|
+
const stamp = safeStamp(startedAt);
|
|
225
|
+
const backupPath = path.join(outDir, `codex-state-before-goal-reset-${goal.thread_id}-${stamp}.sqlite`);
|
|
226
|
+
const dumpPath = path.join(outDir, `codex-goal-row-before-reset-${goal.thread_id}-${stamp}.json`);
|
|
227
|
+
backupSqliteDb(dbPath, backupPath);
|
|
228
|
+
fs.writeFileSync(dumpPath, `${JSON.stringify(goal, null, 2)}\n`, 'utf8');
|
|
229
|
+
chmodPrivate(dumpPath);
|
|
230
|
+
|
|
231
|
+
const rows = runSqliteJson(dbPath, `
|
|
232
|
+
BEGIN IMMEDIATE;
|
|
233
|
+
DELETE FROM thread_goals
|
|
234
|
+
WHERE thread_id = ${sqlString(goal.thread_id)}
|
|
235
|
+
AND goal_id = ${sqlString(goal.goal_id)}
|
|
236
|
+
AND status = 'complete';
|
|
237
|
+
SELECT changes() AS deleted;
|
|
238
|
+
COMMIT;
|
|
239
|
+
`, { readonly: false });
|
|
240
|
+
const deleted = Number(rows[0]?.deleted || 0);
|
|
241
|
+
const remaining = readGoalByThread(dbPath, goal.thread_id);
|
|
242
|
+
const ok = deleted === 1 && !remaining;
|
|
243
|
+
const payload = writeReceipt(outDir, {
|
|
244
|
+
ok,
|
|
245
|
+
schema: SCHEMA,
|
|
246
|
+
action: 'reset',
|
|
247
|
+
status: ok ? 'reset' : 'failed',
|
|
248
|
+
thread_id: goal.thread_id,
|
|
249
|
+
goal_id: goal.goal_id,
|
|
250
|
+
objective: goal.objective,
|
|
251
|
+
previous_status: goal.status,
|
|
252
|
+
deleted,
|
|
253
|
+
state_path: dbPath,
|
|
254
|
+
backup_path: backupPath,
|
|
255
|
+
dump_path: dumpPath,
|
|
256
|
+
started_at: startedAt,
|
|
257
|
+
finished_at: new Date().toISOString(),
|
|
258
|
+
next_action: ok ? 'Call the native Codex create_goal tool in this same thread, then keep Atris Mission/member state as the durable loop.' : 'Inspect backup/dump before retrying.',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
printJsonOrText(payload, [
|
|
262
|
+
ok ? 'Codex goal slot reset.' : 'Codex goal reset failed.',
|
|
263
|
+
`Thread: ${goal.thread_id}`,
|
|
264
|
+
`Backup: ${path.relative(process.cwd(), backupPath)}`,
|
|
265
|
+
`Dump: ${path.relative(process.cwd(), dumpPath)}`,
|
|
266
|
+
`Receipt: ${path.relative(process.cwd(), payload.receipt_path)}`,
|
|
267
|
+
`Next: ${payload.next_action}`,
|
|
268
|
+
], asJson);
|
|
269
|
+
if (!ok) process.exitCode = 1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function usage() {
|
|
273
|
+
return [
|
|
274
|
+
'atris codex-goal - guarded bridge for native Codex thread goals',
|
|
275
|
+
'',
|
|
276
|
+
' atris codex-goal status [--thread <id>|--latest] [--json]',
|
|
277
|
+
` atris codex-goal reset --thread <id> ${CONFIRM_RESET_FLAG}`,
|
|
278
|
+
'',
|
|
279
|
+
'Flags:',
|
|
280
|
+
' --state <path> Codex state DB (default ~/.codex/state_5.sqlite)',
|
|
281
|
+
' --latest Use the latest Codex goal whose thread cwd matches the current directory',
|
|
282
|
+
' --out-dir <path> Receipt/backup directory (default .atris/runs)',
|
|
283
|
+
'',
|
|
284
|
+
'Reset guardrails:',
|
|
285
|
+
'- only completed native Codex goals can be reset',
|
|
286
|
+
'- reset backs up the SQLite DB before mutation',
|
|
287
|
+
'- reset dumps the exact deleted row and writes a receipt',
|
|
288
|
+
'- the next native goal must still be created by the active Codex thread',
|
|
289
|
+
].join('\n');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function codexGoalCommand(args = []) {
|
|
293
|
+
const subcommand = args[0];
|
|
294
|
+
const rest = args.slice(1);
|
|
295
|
+
if (!subcommand || ['help', '--help', '-h'].includes(subcommand)) {
|
|
296
|
+
console.log(usage());
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (subcommand === 'status') return statusCommand(rest);
|
|
300
|
+
if (subcommand === 'reset') return resetCommand(rest);
|
|
301
|
+
console.error(`Unknown codex-goal subcommand: ${subcommand}`);
|
|
302
|
+
console.error(usage());
|
|
303
|
+
process.exitCode = 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
codexGoalCommand,
|
|
308
|
+
CONFIRM_RESET_FLAG,
|
|
309
|
+
resolveStatePath,
|
|
310
|
+
sqlString,
|
|
311
|
+
};
|
package/commands/errors.js
CHANGED
|
@@ -41,6 +41,7 @@ function extractFlag(args, ...names) {
|
|
|
41
41
|
async function listErrors(args) {
|
|
42
42
|
const [hoursArg, r1] = extractFlag(args, '--hours', '-H');
|
|
43
43
|
const [limitArg, r2] = extractFlag(r1, '--limit', '-L');
|
|
44
|
+
const json = r2.includes('--json');
|
|
44
45
|
const hours = hoursArg ? parseInt(hoursArg, 10) : 24;
|
|
45
46
|
const limit = limitArg ? parseInt(limitArg, 10) : 500;
|
|
46
47
|
|
|
@@ -51,13 +52,22 @@ async function listErrors(args) {
|
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
if (!result.ok) {
|
|
54
|
-
|
|
55
|
+
if (json) {
|
|
56
|
+
console.log(JSON.stringify({ ok: false, error: result.error || 'Failed to fetch errors' }));
|
|
57
|
+
} else {
|
|
58
|
+
console.error(`Error: ${result.error || 'Failed to fetch errors'}`);
|
|
59
|
+
}
|
|
55
60
|
process.exit(1);
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
const data = result.data || {};
|
|
59
64
|
const groups = data.groups || [];
|
|
60
65
|
|
|
66
|
+
if (json) {
|
|
67
|
+
console.log(JSON.stringify({ ok: true, ...data }, null, 2));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
if (groups.length === 0) {
|
|
62
72
|
console.log(`No errors in the last ${hours}h. Clean.`);
|
|
63
73
|
return;
|
package/commands/feedback.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
5
|
* atris feedback "message here" Submit feedback
|
|
6
|
+
* atris feedback submit "message here" Submit feedback
|
|
6
7
|
* atris feedback List feedback
|
|
7
8
|
* atris feedback list List feedback
|
|
8
9
|
* atris feedback resolve <id> "<resolution>" Mark resolved (admin)
|
|
@@ -18,6 +19,17 @@ const path = require('path');
|
|
|
18
19
|
const { loadCredentials } = require('../utils/auth');
|
|
19
20
|
const { apiRequestJson } = require('../utils/api');
|
|
20
21
|
|
|
22
|
+
const KNOWN_FEEDBACK_COMMANDS = new Set([
|
|
23
|
+
'list',
|
|
24
|
+
'submit',
|
|
25
|
+
'resolve',
|
|
26
|
+
'close',
|
|
27
|
+
'delete',
|
|
28
|
+
'help',
|
|
29
|
+
'--help',
|
|
30
|
+
'-h',
|
|
31
|
+
]);
|
|
32
|
+
|
|
21
33
|
function getAuth() {
|
|
22
34
|
const creds = loadCredentials();
|
|
23
35
|
if (!creds || !creds.token) {
|
|
@@ -206,20 +218,21 @@ async function deleteFeedback(idPrefix) {
|
|
|
206
218
|
console.log(`Deleted ${lookup.id.substring(0, 8)}`);
|
|
207
219
|
}
|
|
208
220
|
|
|
209
|
-
function printHelp() {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
function printHelp(write = console.log) {
|
|
222
|
+
write('');
|
|
223
|
+
write('Usage:');
|
|
224
|
+
write(' atris feedback "message" Submit quoted feedback (global)');
|
|
225
|
+
write(' atris feedback submit "message" Submit feedback (global)');
|
|
226
|
+
write(' atris feedback submit "msg" --business <slug>');
|
|
227
|
+
write(' atris feedback List feedback');
|
|
228
|
+
write(' atris feedback list List feedback');
|
|
229
|
+
write(' atris feedback resolve <id> "<note>" Mark resolved (admin)');
|
|
230
|
+
write(' atris feedback close <id> Close as wontfix (admin)');
|
|
231
|
+
write(' atris feedback delete <id> Delete feedback (admin)');
|
|
232
|
+
write('');
|
|
233
|
+
write('IDs may be the first 8 chars of the UUID.');
|
|
234
|
+
write('Business slugs come from ~/.atris/businesses.json (e.g. acme, atris-labs).');
|
|
235
|
+
write('');
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
function resolveBusinessArg(value) {
|
|
@@ -256,6 +269,21 @@ function extractFlag(args, ...names) {
|
|
|
256
269
|
return [value, remaining];
|
|
257
270
|
}
|
|
258
271
|
|
|
272
|
+
function directFeedbackMessage(args) {
|
|
273
|
+
if (args.length !== 1) return null;
|
|
274
|
+
const message = String(args[0] || '').trim();
|
|
275
|
+
if (!message || KNOWN_FEEDBACK_COMMANDS.has(message)) return null;
|
|
276
|
+
// The direct form is intentionally only for quoted prose. Otherwise tokens
|
|
277
|
+
// like "show <id>" or "bogus-subcommand" look like commands and must fail.
|
|
278
|
+
return /\s/.test(message) ? message : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function rejectUnknownFeedbackCommand(subcommand) {
|
|
282
|
+
console.error(`Unknown feedback command: ${subcommand || '(empty)'}`);
|
|
283
|
+
printHelp(console.error);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
|
|
259
287
|
async function feedbackCommand() {
|
|
260
288
|
const rawArgs = process.argv.slice(3);
|
|
261
289
|
const [businessArg, args] = extractFlag(rawArgs, '--business', '-b');
|
|
@@ -277,6 +305,12 @@ async function feedbackCommand() {
|
|
|
277
305
|
return;
|
|
278
306
|
}
|
|
279
307
|
|
|
308
|
+
if (subcommand === 'submit') {
|
|
309
|
+
const message = args.slice(1).join(' ');
|
|
310
|
+
await submitFeedback(message, { businessId });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
280
314
|
if (subcommand === 'resolve') {
|
|
281
315
|
const id = args[1];
|
|
282
316
|
const resolution = args.slice(2).join(' ');
|
|
@@ -294,9 +328,13 @@ async function feedbackCommand() {
|
|
|
294
328
|
return;
|
|
295
329
|
}
|
|
296
330
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
331
|
+
const message = directFeedbackMessage(args);
|
|
332
|
+
if (message) {
|
|
333
|
+
await submitFeedback(message, { businessId });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
rejectUnknownFeedbackCommand(subcommand);
|
|
300
338
|
}
|
|
301
339
|
|
|
302
340
|
module.exports = { feedbackCommand };
|
package/commands/fork.js
CHANGED
|
@@ -8,12 +8,12 @@ async function forkAtris() {
|
|
|
8
8
|
const template = process.argv[3];
|
|
9
9
|
const targetArg = process.argv[4];
|
|
10
10
|
|
|
11
|
-
if (!template || template === '--help') {
|
|
11
|
+
if (!template || template === '--help' || template === '-h' || template === 'help') {
|
|
12
12
|
console.log('Usage: atris fork <template> [target-dir]');
|
|
13
13
|
console.log('');
|
|
14
14
|
console.log(' atris fork music-artist Fork the music-artist template');
|
|
15
15
|
console.log(' atris fork event-promoter myband Fork into ./myband/');
|
|
16
|
-
console.log(' atris fork
|
|
16
|
+
console.log(' atris fork example-co Fork from a business slug');
|
|
17
17
|
console.log('');
|
|
18
18
|
console.log('Templates can be a name, business slug, or URL.');
|
|
19
19
|
process.exit(0);
|