ganbatte-os 0.2.6 → 0.2.7
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.
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(__dirname, "../../..");
|
|
7
|
+
const STATE_DIR = path.join(ROOT, ".claude", ".hook-state");
|
|
8
|
+
|
|
9
|
+
function readStdin() {
|
|
10
|
+
try {
|
|
11
|
+
return fs.readFileSync(0, "utf8");
|
|
12
|
+
} catch {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parsePayload() {
|
|
18
|
+
const raw = readStdin().trim();
|
|
19
|
+
if (!raw) return {};
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getSessionId(payload) {
|
|
28
|
+
return (
|
|
29
|
+
payload.session_id ||
|
|
30
|
+
payload.sessionId ||
|
|
31
|
+
payload.conversation_id ||
|
|
32
|
+
payload.conversationId ||
|
|
33
|
+
"default"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureDir(dirPath) {
|
|
38
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function statePathFor(sessionId) {
|
|
42
|
+
return path.join(STATE_DIR, `${sessionId}.json`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readState(statePath, sessionId) {
|
|
46
|
+
if (!fs.existsSync(statePath)) {
|
|
47
|
+
return {
|
|
48
|
+
sessionId,
|
|
49
|
+
touchedFiles: [],
|
|
50
|
+
commands: [],
|
|
51
|
+
significantAction: false,
|
|
52
|
+
updatedAt: new Date().toISOString(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
58
|
+
} catch {
|
|
59
|
+
return {
|
|
60
|
+
sessionId,
|
|
61
|
+
touchedFiles: [],
|
|
62
|
+
commands: [],
|
|
63
|
+
significantAction: false,
|
|
64
|
+
updatedAt: new Date().toISOString(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function uniquePush(list, value) {
|
|
70
|
+
if (!value || list.includes(value)) return;
|
|
71
|
+
list.push(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizePath(candidate) {
|
|
75
|
+
if (typeof candidate !== "string" || !candidate.trim()) return null;
|
|
76
|
+
const trimmed = candidate.trim();
|
|
77
|
+
const absolute = path.isAbsolute(trimmed) ? trimmed : path.join(ROOT, trimmed);
|
|
78
|
+
const normalized = path.normalize(absolute);
|
|
79
|
+
if (!normalized.startsWith(ROOT)) return null;
|
|
80
|
+
return path.relative(ROOT, normalized).replace(/\\/g, "/");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function collectPaths(payload) {
|
|
84
|
+
const args = payload.args || payload.arguments || {};
|
|
85
|
+
const candidates = [
|
|
86
|
+
args.file_path,
|
|
87
|
+
args.path,
|
|
88
|
+
args.target_file,
|
|
89
|
+
args.new_file_path,
|
|
90
|
+
payload.file_path,
|
|
91
|
+
payload.path,
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const result = [];
|
|
95
|
+
for (const candidate of candidates) {
|
|
96
|
+
const normalized = normalizePath(candidate);
|
|
97
|
+
if (normalized) uniquePush(result, normalized);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function extractCommand(payload) {
|
|
103
|
+
const args = payload.args || payload.arguments || {};
|
|
104
|
+
return (
|
|
105
|
+
args.command ||
|
|
106
|
+
args.cmd ||
|
|
107
|
+
payload.command ||
|
|
108
|
+
payload.cmd ||
|
|
109
|
+
""
|
|
110
|
+
).trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function main() {
|
|
114
|
+
const payload = parsePayload();
|
|
115
|
+
const sessionId = getSessionId(payload);
|
|
116
|
+
const statePath = statePathFor(sessionId);
|
|
117
|
+
|
|
118
|
+
ensureDir(STATE_DIR);
|
|
119
|
+
const state = readState(statePath, sessionId);
|
|
120
|
+
|
|
121
|
+
for (const filePath of collectPaths(payload)) {
|
|
122
|
+
uniquePush(state.touchedFiles, filePath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const toolName = String(payload.tool || payload.tool_name || payload.matcher || "");
|
|
126
|
+
if (/Bash/i.test(toolName)) {
|
|
127
|
+
const command = extractCommand(payload);
|
|
128
|
+
if (command) uniquePush(state.commands, command);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (state.touchedFiles.length > 0 || state.commands.length > 0) {
|
|
132
|
+
state.significantAction = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
state.updatedAt = new Date().toISOString();
|
|
136
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
main();
|
|
141
|
+
} catch {
|
|
142
|
+
// observation hook: never block
|
|
143
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { execFileSync } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, "../../..");
|
|
8
|
+
const STATE_DIR = path.join(ROOT, ".claude", ".hook-state");
|
|
9
|
+
|
|
10
|
+
function readStdin() {
|
|
11
|
+
try {
|
|
12
|
+
return fs.readFileSync(0, "utf8");
|
|
13
|
+
} catch {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parsePayload() {
|
|
19
|
+
const raw = readStdin().trim();
|
|
20
|
+
if (!raw) return {};
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getSessionId(payload) {
|
|
29
|
+
return (
|
|
30
|
+
payload.session_id ||
|
|
31
|
+
payload.sessionId ||
|
|
32
|
+
payload.conversation_id ||
|
|
33
|
+
payload.conversationId ||
|
|
34
|
+
"default"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readState(sessionId) {
|
|
39
|
+
const statePath = path.join(STATE_DIR, `${sessionId}.json`);
|
|
40
|
+
if (!fs.existsSync(statePath)) {
|
|
41
|
+
return { statePath, state: null };
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return {
|
|
45
|
+
statePath,
|
|
46
|
+
state: JSON.parse(fs.readFileSync(statePath, "utf8")),
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
return { statePath, state: null };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function safeUnlink(filePath) {
|
|
54
|
+
try {
|
|
55
|
+
fs.unlinkSync(filePath);
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore cleanup issues
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runNpm(args, options = {}) {
|
|
62
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
63
|
+
return execFileSync(npmCmd, args, {
|
|
64
|
+
cwd: ROOT,
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
67
|
+
timeout: options.timeout || 120000,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getGitStatusMap(paths) {
|
|
72
|
+
const map = new Map();
|
|
73
|
+
if (!Array.isArray(paths) || paths.length === 0) return map;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const output = execFileSync("git", ["status", "--porcelain", "--", ...paths], {
|
|
77
|
+
cwd: ROOT,
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
80
|
+
timeout: 15000,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
for (const line of output.split(/\r?\n/)) {
|
|
84
|
+
if (!line.trim()) continue;
|
|
85
|
+
const status = line.slice(0, 2);
|
|
86
|
+
const filePath = line.slice(3).trim().replace(/\\/g, "/");
|
|
87
|
+
map.set(filePath, status);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// ignore git issues
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return map;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function anyPathMatches(paths, matcher) {
|
|
97
|
+
return paths.some((filePath) => matcher.test(filePath));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function anyCreatedOrRemoved(statusMap, matcher) {
|
|
101
|
+
for (const [filePath, status] of statusMap.entries()) {
|
|
102
|
+
if (!matcher.test(filePath)) continue;
|
|
103
|
+
if (status.includes("A") || status.includes("D") || status.includes("R") || status === "??") {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function main() {
|
|
111
|
+
const payload = parsePayload();
|
|
112
|
+
const sessionId = getSessionId(payload);
|
|
113
|
+
const { statePath, state } = readState(sessionId);
|
|
114
|
+
|
|
115
|
+
if (!state || !state.significantAction) {
|
|
116
|
+
safeUnlink(statePath);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const touchedFiles = Array.isArray(state.touchedFiles) ? state.touchedFiles : [];
|
|
121
|
+
const statusMap = getGitStatusMap(touchedFiles);
|
|
122
|
+
const summary = [];
|
|
123
|
+
const syncMatcher = /^(\.gos|\.claude|data|README\.md|CLAUDE\.md|AGENTS\.md|GEMINI\.md)/;
|
|
124
|
+
|
|
125
|
+
if (anyPathMatches(touchedFiles, syncMatcher)) {
|
|
126
|
+
try {
|
|
127
|
+
runNpm(["run", "sync:ides"], { timeout: 180000 });
|
|
128
|
+
summary.push("sync:ides OK");
|
|
129
|
+
|
|
130
|
+
if (anyCreatedOrRemoved(statusMap, /^(\.gos|\.claude)\//)) {
|
|
131
|
+
runNpm(["run", "doctor"], { timeout: 180000 });
|
|
132
|
+
summary.push("doctor OK");
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const message = error.stderr || error.stdout || error.message || "falha no sync";
|
|
136
|
+
summary.push(`sync:ides falhou (${String(message).split(/\r?\n/)[0]})`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
safeUnlink(statePath);
|
|
141
|
+
|
|
142
|
+
if (summary.length > 0) {
|
|
143
|
+
process.stdout.write(`${summary.join("; ")}\n`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
main();
|
|
149
|
+
} catch {
|
|
150
|
+
// observation hook: never block
|
|
151
|
+
}
|