atris 3.30.12 → 3.32.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/AGENTS.md +16 -3
- package/FOR_AGENTS.md +81 -0
- package/README.md +6 -4
- package/atris/CLAUDE.md +8 -0
- package/atris/atris.md +51 -19
- package/atris/skills/README.md +1 -0
- package/atris/skills/blocks/SKILL.md +134 -0
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
- package/atris/skills/youtube/SKILL.md +31 -11
- package/atris.md +15 -0
- package/ax +189 -7
- package/bin/atris.js +273 -225
- package/commands/autoland.js +379 -0
- package/commands/autopilot-front.js +273 -0
- package/commands/autopilot.js +94 -4
- package/commands/business.js +1 -1
- package/commands/clean.js +72 -9
- package/commands/codex-goal.js +72 -22
- package/commands/computer.js +48 -3
- package/commands/gm.js +1 -1
- package/commands/harvest.js +179 -0
- package/commands/init.js +22 -1
- package/commands/land.js +442 -0
- package/commands/loop-front.js +122 -4
- package/commands/member.js +551 -19
- package/commands/mission.js +3674 -278
- package/commands/play.js +1 -1
- package/commands/pulse.js +65 -7
- package/commands/run-front.js +144 -0
- package/commands/run.js +10 -7
- package/commands/sign.js +90 -0
- package/commands/strings.js +301 -0
- package/commands/task.js +782 -108
- package/commands/truth.js +171 -0
- package/commands/xp.js +32 -8
- package/commands/youtube.js +72 -5
- package/decks/README.md +6 -12
- package/lib/auto-accept-certified.js +10 -0
- package/lib/autoland.js +391 -0
- package/lib/context-gatherer.js +0 -8
- package/lib/mission-artifact.js +504 -0
- package/lib/mission-room.js +846 -0
- package/lib/next-moves.js +212 -6
- package/lib/operator-next.js +7 -0
- package/lib/pulse.js +78 -4
- package/lib/runner-command.js +51 -20
- package/lib/runs-prune.js +242 -0
- package/lib/task-db.js +19 -2
- package/lib/task-proof.js +1 -1
- package/package.json +4 -4
- package/decks/atris-seed-pitch-v3.json +0 -118
- package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
- package/decks/atris-seed-pitch-v5.json +0 -109
- package/decks/atris-seed-pitch-v6.json +0 -137
- package/decks/atris-seed-pitch-v7.json +0 -133
- package/decks/mark-pincus-narrative.json +0 -102
- package/decks/mark-pincus-sourcery.json +0 -94
- package/decks/yash-applied-compute-detailed.json +0 -150
- package/decks/yash-applied-compute-generalist.json +0 -82
- package/decks/yash-applied-compute-narrative.json +0 -54
- package/lib/ax-chat-input.js +0 -164
- package/lib/ax-goal.js +0 -307
- package/lib/ax-prefs.js +0 -63
- package/lib/ax-shimmer.js +0 -63
package/commands/play.js
CHANGED
|
@@ -417,7 +417,7 @@ function modeState(args = []) {
|
|
|
417
417
|
prompt: latestMessage(events),
|
|
418
418
|
} : null,
|
|
419
419
|
proof_recipe: proofRecipe(),
|
|
420
|
-
xp_rule: 'AgentXP
|
|
420
|
+
xp_rule: 'AgentXP is awarded only after a useful artifact has verifier proof and accepted review.',
|
|
421
421
|
global_sync_rule: AGENTXP_GLOBAL_SYNC_RULE,
|
|
422
422
|
leaderboard_url: AGENTXP_LEADERBOARD_URL,
|
|
423
423
|
next_commands: commandList,
|
package/commands/pulse.js
CHANGED
|
@@ -18,6 +18,7 @@ const os = require('os');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { spawnSync } = require('child_process');
|
|
20
20
|
const pulse = require('../lib/pulse');
|
|
21
|
+
const { DEFAULT_CLAUDE_RUNNER_MODEL } = require('../lib/runner-command');
|
|
21
22
|
|
|
22
23
|
function hasFlag(args, name) {
|
|
23
24
|
return args.includes(name);
|
|
@@ -51,6 +52,7 @@ Options:
|
|
|
51
52
|
--verify "<cmd>" Verifier for changed-work ticks (default: npm test)
|
|
52
53
|
--cadence "<cron>" Cron cadence for install
|
|
53
54
|
--days <n> Auto-expire installed heartbeat after n days
|
|
55
|
+
--hours <n> Auto-expire installed heartbeat after n hours
|
|
54
56
|
--model <id> Runner model alias/id for installed heartbeat
|
|
55
57
|
--runner-profile <n> Runner profile for installed heartbeat (e.g. atris-fast)
|
|
56
58
|
--runner-bin <path> Runner binary for installed heartbeat
|
|
@@ -197,6 +199,26 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
const tickIndex = pulse.nextTickIndex(root);
|
|
202
|
+
const signalExitCode = { SIGINT: 130, SIGTERM: 143 };
|
|
203
|
+
let interrupted = false;
|
|
204
|
+
const finishInterrupted = (signal) => {
|
|
205
|
+
if (interrupted) return;
|
|
206
|
+
interrupted = true;
|
|
207
|
+
try {
|
|
208
|
+
pulse.appendPulseReceipt(root, pulse.buildInterruptedPulseReceipt({
|
|
209
|
+
tickIndex,
|
|
210
|
+
signal,
|
|
211
|
+
startedAt,
|
|
212
|
+
prevTickStale: priorStale.stale,
|
|
213
|
+
}));
|
|
214
|
+
} catch {}
|
|
215
|
+
try {
|
|
216
|
+
pulse.releaseLock(root);
|
|
217
|
+
} catch {}
|
|
218
|
+
process.exit(signalExitCode[signal] || 1);
|
|
219
|
+
};
|
|
220
|
+
process.once('SIGINT', finishInterrupted);
|
|
221
|
+
process.once('SIGTERM', finishInterrupted);
|
|
200
222
|
|
|
201
223
|
// 'started' receipt - if the tick dies after this, the orphan surfaces as a ghost.
|
|
202
224
|
pulse.appendPulseReceipt(root, pulse.buildPulseReceipt({
|
|
@@ -260,7 +282,7 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
260
282
|
what,
|
|
261
283
|
changedFiles,
|
|
262
284
|
elapsedMs,
|
|
263
|
-
model: process.env.ATRIS_RUNNER_MODEL || process.env.ATRIS_CLAUDE_MODEL ||
|
|
285
|
+
model: process.env.ATRIS_RUNNER_MODEL || process.env.ATRIS_CLAUDE_MODEL || DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
264
286
|
}));
|
|
265
287
|
scorecardWritten = true;
|
|
266
288
|
}
|
|
@@ -301,6 +323,8 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
301
323
|
if (!asJson) process.stdout.write(`pulse tick #${tickIndex} crashed: ${out.error}\n`);
|
|
302
324
|
return emit(out, asJson);
|
|
303
325
|
} finally {
|
|
326
|
+
process.removeListener('SIGINT', finishInterrupted);
|
|
327
|
+
process.removeListener('SIGTERM', finishInterrupted);
|
|
304
328
|
pulse.releaseLock(root);
|
|
305
329
|
}
|
|
306
330
|
}
|
|
@@ -372,14 +396,45 @@ function resolveEngineBinDirs(extraBins = []) {
|
|
|
372
396
|
|
|
373
397
|
function installCommand(args, root = process.cwd()) {
|
|
374
398
|
const asJson = wantsJson(args);
|
|
375
|
-
const
|
|
376
|
-
|
|
399
|
+
const cadenceInput = readFlag(args, '--cadence', pulse.DEFAULT_CADENCE_CRON);
|
|
400
|
+
let cron;
|
|
401
|
+
try {
|
|
402
|
+
cron = pulse.normalizeCronCadence(cadenceInput);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
const out = {
|
|
405
|
+
ok: false,
|
|
406
|
+
action: 'pulse_install',
|
|
407
|
+
reason: 'invalid_cadence',
|
|
408
|
+
detail: error && error.message ? error.message : String(error),
|
|
409
|
+
cadence: cadenceInput,
|
|
410
|
+
};
|
|
411
|
+
if (!asJson) process.stdout.write(`pulse install failed: ${out.detail}\n`);
|
|
412
|
+
return emit(out, asJson);
|
|
413
|
+
}
|
|
414
|
+
let expiry;
|
|
415
|
+
try {
|
|
416
|
+
expiry = pulse.normalizeExpiryDuration({
|
|
417
|
+
hours: readFlag(args, '--hours', null),
|
|
418
|
+
days: readFlag(args, '--days', '7'),
|
|
419
|
+
});
|
|
420
|
+
} catch (error) {
|
|
421
|
+
const out = {
|
|
422
|
+
ok: false,
|
|
423
|
+
action: 'pulse_install',
|
|
424
|
+
reason: 'invalid_expiry',
|
|
425
|
+
detail: error && error.message ? error.message : String(error),
|
|
426
|
+
hours: readFlag(args, '--hours', null),
|
|
427
|
+
days: readFlag(args, '--days', '7'),
|
|
428
|
+
};
|
|
429
|
+
if (!asJson) process.stdout.write(`pulse install failed: ${out.detail}\n`);
|
|
430
|
+
return emit(out, asJson);
|
|
431
|
+
}
|
|
377
432
|
const verifyCmd = readFlag(args, '--verify', 'npm test');
|
|
378
|
-
const model = readFlag(args, '--model', process.env.ATRIS_RUNNER_MODEL || process.env.ATRIS_CLAUDE_MODEL ||
|
|
433
|
+
const model = readFlag(args, '--model', process.env.ATRIS_RUNNER_MODEL || process.env.ATRIS_CLAUDE_MODEL || DEFAULT_CLAUDE_RUNNER_MODEL);
|
|
379
434
|
const runnerProfile = readFlag(args, '--runner-profile', process.env.ATRIS_RUNNER_PROFILE || '');
|
|
380
435
|
const runnerBin = readFlag(args, '--runner-bin', process.env.ATRIS_RUNNER_BIN || process.env.ATRIS_CLAUDE_BIN || '');
|
|
381
436
|
const runnerCommandTemplate = readFlag(args, '--runner-template', process.env.ATRIS_RUNNER_COMMAND_TEMPLATE || process.env.ATRIS_CLAUDE_COMMAND_TEMPLATE || '');
|
|
382
|
-
const deadlineEpoch = Math.floor(Date.now() / 1000) +
|
|
437
|
+
const deadlineEpoch = Math.floor(Date.now() / 1000) + expiry.seconds;
|
|
383
438
|
|
|
384
439
|
fs.mkdirSync(STATE_HOME, { recursive: true });
|
|
385
440
|
const scriptPath = path.join(STATE_HOME, 'tick.sh');
|
|
@@ -414,7 +469,10 @@ function installCommand(args, root = process.cwd()) {
|
|
|
414
469
|
script_path: scriptPath,
|
|
415
470
|
crontab_line: line,
|
|
416
471
|
cadence: cron,
|
|
417
|
-
|
|
472
|
+
cadence_input: cadenceInput,
|
|
473
|
+
expires_in_days: expiry.days,
|
|
474
|
+
expires_in_hours: expiry.hours,
|
|
475
|
+
expires_in_seconds: expiry.seconds,
|
|
418
476
|
deadline_epoch: deadlineEpoch,
|
|
419
477
|
runner_profile: runnerProfile || null,
|
|
420
478
|
runner_bin: runnerBin || null,
|
|
@@ -425,7 +483,7 @@ function installCommand(args, root = process.cwd()) {
|
|
|
425
483
|
process.stdout.write([
|
|
426
484
|
`pulse installed. heartbeat fires '${cron}' against ${root}.`,
|
|
427
485
|
`script: ${scriptPath}`,
|
|
428
|
-
`auto-expires in ${days} days. stop early: atris pulse uninstall`,
|
|
486
|
+
`auto-expires in ${expiry.hours ? `${expiry.hours} hours` : `${expiry.days} days`}. stop early: atris pulse uninstall`,
|
|
429
487
|
].join('\n') + '\n');
|
|
430
488
|
} else {
|
|
431
489
|
process.stdout.write(`pulse install failed to write crontab: ${apply.stderr || apply.status}\nscript written to ${scriptPath}; add this line to your crontab manually:\n${line}\n`);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// atris run: thin front door over the mission runtime loop.
|
|
2
|
+
// One bounded pursuit: start a mission from the objective (or resume the most
|
|
3
|
+
// logical runnable mission), tick it, complete on pass, then exit.
|
|
4
|
+
// The old plan→do→review loop lives on behind `atris run --legacy`.
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
9
|
+
|
|
10
|
+
const RUN_VALUE_FLAGS = new Set([
|
|
11
|
+
'--owner',
|
|
12
|
+
'--cadence',
|
|
13
|
+
'--minutes',
|
|
14
|
+
'--hours',
|
|
15
|
+
'--max-ticks',
|
|
16
|
+
'--max-wall',
|
|
17
|
+
'--runner',
|
|
18
|
+
'--runner-bin',
|
|
19
|
+
'--runner-template',
|
|
20
|
+
'--runner-model',
|
|
21
|
+
'--runner-profile',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function readValueFlag(args, name) {
|
|
25
|
+
const index = args.indexOf(name);
|
|
26
|
+
return index >= 0 && index + 1 < args.length ? args[index + 1] : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function positiveNumber(value) {
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runBudgetSeconds(args = []) {
|
|
35
|
+
const hours = positiveNumber(readValueFlag(args, '--hours'));
|
|
36
|
+
if (hours) return Math.round(hours * 3600);
|
|
37
|
+
const minutes = positiveNumber(readValueFlag(args, '--minutes'));
|
|
38
|
+
if (minutes) return Math.round(minutes * 60);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runObjective(args = []) {
|
|
43
|
+
const words = [];
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
const arg = args[i];
|
|
46
|
+
if (RUN_VALUE_FLAGS.has(arg)) { i++; continue; }
|
|
47
|
+
if (arg.startsWith('-')) continue;
|
|
48
|
+
words.push(arg);
|
|
49
|
+
}
|
|
50
|
+
return words.join(' ').trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Runners a spawned `mission run` can drive unattended.
|
|
54
|
+
const HEADLESS_DRIVABLE_RUNNERS = new Set(['claude', 'atris2']);
|
|
55
|
+
const CALLER_SESSION_RUNNERS = new Set(['codex_goal', 'caller_session', 'current_agent']);
|
|
56
|
+
|
|
57
|
+
function liveCodexSession(env = process.env) {
|
|
58
|
+
return Boolean(String(env.CODEX_THREAD_ID || '').trim());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runnerDrivableForFrontDoor(runner, options = {}) {
|
|
62
|
+
const normalized = String(runner || '').trim().toLowerCase();
|
|
63
|
+
if (HEADLESS_DRIVABLE_RUNNERS.has(normalized)) return true;
|
|
64
|
+
if (options.allowCallerSessionRunners && CALLER_SESSION_RUNNERS.has(normalized)) return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Most logical mission: a moving one beats a planned one, newer beats older.
|
|
69
|
+
// ready is excluded — it waits for review, not for another worker pass.
|
|
70
|
+
function pickRunnableMission(root = process.cwd(), missionMap = null, options = {}) {
|
|
71
|
+
let map = missionMap;
|
|
72
|
+
if (!map) {
|
|
73
|
+
try { map = require('./mission').loadMissionMap(root); } catch { return null; }
|
|
74
|
+
}
|
|
75
|
+
const candidates = Array.from(map.values())
|
|
76
|
+
.filter((mission) => mission && (mission.status === 'running' || mission.status === 'planning'))
|
|
77
|
+
.filter((mission) => runnerDrivableForFrontDoor(mission?.runner, options))
|
|
78
|
+
.filter((mission) => !/^decide and start the next useful mission after:/i.test(String(mission?.objective || '')))
|
|
79
|
+
.sort((a, b) => {
|
|
80
|
+
if (a.status !== b.status) return a.status === 'running' ? -1 : 1;
|
|
81
|
+
return String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || ''));
|
|
82
|
+
});
|
|
83
|
+
return candidates[0] || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runTickBudget(args, budgetSeconds) {
|
|
87
|
+
const explicit = positiveNumber(readValueFlag(args, '--max-ticks'));
|
|
88
|
+
if (explicit) return Math.floor(explicit);
|
|
89
|
+
// A timed run is a loop contract: keep picking the next useful move until
|
|
90
|
+
// the wall clock spends the budget (same rule as member run).
|
|
91
|
+
return budgetSeconds ? Math.max(4, Math.ceil(budgetSeconds / 300)) : 4;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runMissionFront(args = []) {
|
|
95
|
+
const objective = runObjective(args);
|
|
96
|
+
const budgetSeconds = runBudgetSeconds(args);
|
|
97
|
+
const maxTicks = runTickBudget(args, budgetSeconds);
|
|
98
|
+
const maxWallSeconds = positiveNumber(readValueFlag(args, '--max-wall')) || budgetSeconds || 1200;
|
|
99
|
+
|
|
100
|
+
if (objective) {
|
|
101
|
+
const { runRuntimeMissionLoop } = require('../lib/mission-runtime-loop');
|
|
102
|
+
const result = await runRuntimeMissionLoop({
|
|
103
|
+
objective,
|
|
104
|
+
owner: readValueFlag(args, '--owner') || 'mission-lead',
|
|
105
|
+
cadence: readValueFlag(args, '--cadence') || '',
|
|
106
|
+
maxTicks,
|
|
107
|
+
maxWallSeconds,
|
|
108
|
+
noComplete: args.includes('--no-complete'),
|
|
109
|
+
}, {
|
|
110
|
+
cliPath: CLI_PATH,
|
|
111
|
+
onProgress: (line) => console.log(` ${line}`),
|
|
112
|
+
});
|
|
113
|
+
console.log(result.text || '');
|
|
114
|
+
return result.ok ? 0 : 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const mission = pickRunnableMission(process.cwd(), null, {
|
|
118
|
+
allowCallerSessionRunners: liveCodexSession(),
|
|
119
|
+
});
|
|
120
|
+
if (!mission) {
|
|
121
|
+
console.log('No objective given and no runnable mission found.');
|
|
122
|
+
console.log('Start one: atris run "<objective>" [--minutes N] [--owner <member>]');
|
|
123
|
+
console.log('Or keep going indefinitely: atris autopilot');
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
console.log(`Resuming mission ${mission.id} (${mission.status}): ${mission.objective}`);
|
|
127
|
+
const result = spawnSync(process.execPath, [
|
|
128
|
+
CLI_PATH, 'mission', 'run', mission.id,
|
|
129
|
+
'--max-ticks', String(maxTicks),
|
|
130
|
+
'--max-wall', String(maxWallSeconds),
|
|
131
|
+
'--complete-on-pass',
|
|
132
|
+
], { cwd: process.cwd(), stdio: 'inherit' });
|
|
133
|
+
return result.status || 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
runMissionFront,
|
|
138
|
+
pickRunnableMission,
|
|
139
|
+
liveCodexSession,
|
|
140
|
+
runnerDrivableForFrontDoor,
|
|
141
|
+
runObjective,
|
|
142
|
+
runBudgetSeconds,
|
|
143
|
+
runTickBudget,
|
|
144
|
+
};
|
package/commands/run.js
CHANGED
|
@@ -15,7 +15,7 @@ const { parseTodo } = require('../lib/todo');
|
|
|
15
15
|
const {
|
|
16
16
|
buildRunnerCommand,
|
|
17
17
|
buildRunnerAvailabilityCommand,
|
|
18
|
-
|
|
18
|
+
runnerAvailabilityFailureMessage,
|
|
19
19
|
} = require('../lib/runner-command');
|
|
20
20
|
const { cleanAtris } = require('./clean');
|
|
21
21
|
|
|
@@ -381,12 +381,15 @@ async function runAtris(options = {}) {
|
|
|
381
381
|
process.exit(1);
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
// Check configured runner CLI is available.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
384
|
+
// Check configured runner CLI is available. Dry runs preview context only —
|
|
385
|
+
// they must work on machines (and CI) without the runner installed.
|
|
386
|
+
if (!dryRun) {
|
|
387
|
+
try {
|
|
388
|
+
execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' });
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.error(runnerAvailabilityFailureMessage(err));
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
390
393
|
}
|
|
391
394
|
|
|
392
395
|
console.log('');
|
package/commands/sign.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// atris sign: co-author trailer on every commit in an atris workspace.
|
|
5
|
+
// Installs a prepare-commit-msg hook that appends "Co-authored-by: Atris"
|
|
6
|
+
// when the repo has an atris/ folder — same credit line Claude and Cursor use.
|
|
7
|
+
// Idempotent and non-destructive: appends a marked block, removes only its own block.
|
|
8
|
+
|
|
9
|
+
const MARKER = '# atris co-author';
|
|
10
|
+
const TRAILER = 'Co-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>';
|
|
11
|
+
|
|
12
|
+
const HOOK_BLOCK = `
|
|
13
|
+
${MARKER}
|
|
14
|
+
if [ -d "$(git rev-parse --show-toplevel 2>/dev/null)/atris" ] && [ "$2" != "merge" ] && [ "$2" != "squash" ]; then
|
|
15
|
+
if ! grep -qi "^co-authored-by: atris" "$1"; then
|
|
16
|
+
printf '\\nCo-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>\\n' >> "$1"
|
|
17
|
+
fi
|
|
18
|
+
fi
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
function hookPathFor(root) {
|
|
22
|
+
return path.join(root, '.git', 'hooks', 'prepare-commit-msg');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Install the marked block into .git/hooks/prepare-commit-msg. Returns { hookPath, already }.
|
|
26
|
+
function installSignHook(root = process.cwd()) {
|
|
27
|
+
if (!fs.existsSync(path.join(root, '.git'))) throw new Error('not a git repo (no .git here)');
|
|
28
|
+
const hookPath = hookPathFor(root);
|
|
29
|
+
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
30
|
+
let content = '';
|
|
31
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch {}
|
|
32
|
+
if (content.includes(MARKER)) return { hookPath, already: true };
|
|
33
|
+
if (!content) content = '#!/bin/sh\n';
|
|
34
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
35
|
+
content += HOOK_BLOCK;
|
|
36
|
+
fs.writeFileSync(hookPath, content);
|
|
37
|
+
fs.chmodSync(hookPath, 0o755);
|
|
38
|
+
return { hookPath, already: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Remove only our marked block; leave the rest of the hook untouched.
|
|
42
|
+
function removeSignHook(root = process.cwd()) {
|
|
43
|
+
const hookPath = hookPathFor(root);
|
|
44
|
+
let content = '';
|
|
45
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch { return { hookPath, removed: false }; }
|
|
46
|
+
if (!content.includes(MARKER)) return { hookPath, removed: false };
|
|
47
|
+
const cleaned = content.replace(HOOK_BLOCK, '');
|
|
48
|
+
if (cleaned.trim() === '#!/bin/sh') {
|
|
49
|
+
fs.unlinkSync(hookPath);
|
|
50
|
+
} else {
|
|
51
|
+
fs.writeFileSync(hookPath, cleaned);
|
|
52
|
+
}
|
|
53
|
+
return { hookPath, removed: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function signStatus(root = process.cwd()) {
|
|
57
|
+
const hookPath = hookPathFor(root);
|
|
58
|
+
let content = '';
|
|
59
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch {}
|
|
60
|
+
return { hookPath, installed: content.includes(MARKER) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function signCommand(sub) {
|
|
64
|
+
const rel = (p) => path.relative(process.cwd(), p);
|
|
65
|
+
if (sub === 'off' || sub === 'remove') {
|
|
66
|
+
const { hookPath, removed } = removeSignHook();
|
|
67
|
+
console.log(removed
|
|
68
|
+
? `\n ✓ atris co-author removed: ${rel(hookPath)}\n`
|
|
69
|
+
: `\n nothing to remove — atris co-author is not installed\n`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
if (sub === 'status') {
|
|
73
|
+
const { hookPath, installed } = signStatus();
|
|
74
|
+
console.log(installed
|
|
75
|
+
? `\n ✓ atris co-author is on: ${rel(hookPath)}\n every commit gets: ${TRAILER}\n`
|
|
76
|
+
: `\n atris co-author is off — run 'atris sign' to turn it on\n`);
|
|
77
|
+
return installed ? 0 : 1;
|
|
78
|
+
}
|
|
79
|
+
if (sub && sub !== 'on' && sub !== 'install') {
|
|
80
|
+
console.log(`\n atris sign — co-author trailer on every commit\n\n atris sign install the prepare-commit-msg hook\n atris sign off remove it\n atris sign status check whether it's installed\n\n While installed, any commit in a repo with an atris/ folder gets:\n ${TRAILER}\n`);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
const { hookPath, already } = installSignHook();
|
|
84
|
+
console.log(already
|
|
85
|
+
? `\n already installed: ${rel(hookPath)}\n`
|
|
86
|
+
: `\n ✓ atris co-author installed: ${rel(hookPath)}\n every commit here now gets: ${TRAILER}\n`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { signCommand, installSignHook, removeSignHook, signStatus, TRAILER };
|