toga-ai 1.0.922 → 1.0.923
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/.claude/settings.json
CHANGED
|
@@ -90,6 +90,16 @@
|
|
|
90
90
|
"timeout": 3000
|
|
91
91
|
}
|
|
92
92
|
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"matcher": "Write|Edit",
|
|
96
|
+
"hooks": [
|
|
97
|
+
{
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/platform-reuse-reminder.js\"",
|
|
100
|
+
"timeout": 3000
|
|
101
|
+
}
|
|
102
|
+
]
|
|
93
103
|
}
|
|
94
104
|
],
|
|
95
105
|
"PostToolUse": [
|
package/package.json
CHANGED
|
@@ -29,7 +29,8 @@ not a script. When a principle and a real constraint collide, reason it out and
|
|
|
29
29
|
- **Data-drive the UI.** Labels, colors, options, config come from Surface/settings and
|
|
30
30
|
`labelRecordFieldId` — not hardcoded. Model anything data-drivable through those layers.
|
|
31
31
|
- **Reuse before build.** A fundamental shared component (the table first) belongs in `blox`,
|
|
32
|
-
themed per app — not copied.
|
|
32
|
+
themed per app — not copied. Nudged by `scripts/hooks/platform-reuse-reminder.js` on every
|
|
33
|
+
new `src/` file in a platform app or blox (silence: `TOGA_REUSE_REMINDER_DISABLED=1`).
|
|
33
34
|
- **Standard before custom.** Discuss standard-vs-custom before adding any custom field.
|
|
34
35
|
|
|
35
36
|
## Reversibility gate
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* platform-reuse-reminder.js — PRE-CREATE nudge to reuse before building in the 2.0
|
|
6
|
+
* platform apps.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: the platform has ~10 apps sharing one component library
|
|
9
|
+
* (toga25-blox). New pages keep getting fresh components, hooks and styles that already
|
|
10
|
+
* exist in blox or in another app. The "reuse before build" rule
|
|
11
|
+
* (rules/common/architecture-principles.md) is passive; this puts it in context at the
|
|
12
|
+
* moment a new file is about to be created.
|
|
13
|
+
*
|
|
14
|
+
* How it works: registered on PreToolUse for Write|Edit. It fires only when ALL hold:
|
|
15
|
+
* - the target file does NOT exist yet (a new file),
|
|
16
|
+
* - it ends .tsx / .ts / .css (not .test.ts / .d.ts),
|
|
17
|
+
* - it sits under the `src/` of a platform app — a folder whose vite.config.ts calls
|
|
18
|
+
* defineTogaApp — or under the `src/` of the toga25-blox repo itself.
|
|
19
|
+
* Then it adds a short reminder through the PreToolUse additionalContext channel.
|
|
20
|
+
* Once per file path per session (state file in the OS temp dir, keyed on session id).
|
|
21
|
+
*
|
|
22
|
+
* It never blocks. It always exits 0 and fails open on any error.
|
|
23
|
+
*
|
|
24
|
+
* Escape hatch: set TOGA_REUSE_REMINDER_DISABLED=1 to silence the reminder.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
// Single-source platform consolidation rule (lives in toga25-blox).
|
|
32
|
+
const CONSOLIDATION_DOC = 'toga25-blox/docs/consolidation.md';
|
|
33
|
+
const BLOX_INVENTORY = '../toga25-blox/INVENTORY.md';
|
|
34
|
+
const APP_REUSE_DOC = 'docs/architecture/reusable-components.md';
|
|
35
|
+
const BLOX_PACKAGE_NAME = '@toga/blox';
|
|
36
|
+
const BLOX_DIR_NAME = 'toga25-blox';
|
|
37
|
+
const MAX_WALK_UP_LEVELS = 15;
|
|
38
|
+
const MAX_SEEN_PATHS = 500;
|
|
39
|
+
|
|
40
|
+
function readPayload() {
|
|
41
|
+
let raw = '';
|
|
42
|
+
try { raw = fs.readFileSync(0, 'utf8'); } catch (e) { /* no stdin */ }
|
|
43
|
+
let data = {};
|
|
44
|
+
if (raw && raw.trim()) {
|
|
45
|
+
try { data = JSON.parse(raw); } catch (e) { data = {}; }
|
|
46
|
+
}
|
|
47
|
+
if (!data.tool_name && process.env.CLAUDE_TOOL_NAME) data.tool_name = process.env.CLAUDE_TOOL_NAME;
|
|
48
|
+
if (!data.tool_input && process.env.CLAUDE_TOOL_INPUT) {
|
|
49
|
+
try { data.tool_input = JSON.parse(process.env.CLAUDE_TOOL_INPUT); } catch (e) { /* ignore */ }
|
|
50
|
+
}
|
|
51
|
+
if (!data.session_id) data.session_id = process.env.CLAUDE_SESSION_ID || process.env.CLAUDE_CODE_SESSION_ID;
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isWatchedExtension(filePath) {
|
|
56
|
+
const p = filePath.toLowerCase();
|
|
57
|
+
if (p.endsWith('.d.ts') || p.endsWith('.test.ts') || p.endsWith('.test.tsx')) return false;
|
|
58
|
+
return /\.(tsx|ts|css)$/.test(p);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isBloxRoot(dir) {
|
|
62
|
+
if (path.basename(dir).toLowerCase() !== BLOX_DIR_NAME) {
|
|
63
|
+
try {
|
|
64
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
65
|
+
return pkg && pkg.name === BLOX_PACKAGE_NAME;
|
|
66
|
+
} catch (e) { return false; }
|
|
67
|
+
}
|
|
68
|
+
return fs.existsSync(path.join(dir, 'package.json'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPlatformAppRoot(dir) {
|
|
72
|
+
try {
|
|
73
|
+
return fs.readFileSync(path.join(dir, 'vite.config.ts'), 'utf8').includes('defineTogaApp');
|
|
74
|
+
} catch (e) { return false; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Walk up from the file to the nearest project root. Returns { root, isBlox } when the
|
|
78
|
+
* nearest root is a platform app or blox, else null. The file must be under root/src/. */
|
|
79
|
+
function findPlatformRoot(filePath) {
|
|
80
|
+
let dir = path.dirname(filePath);
|
|
81
|
+
for (let i = 0; i < MAX_WALK_UP_LEVELS; i++) {
|
|
82
|
+
const app = isPlatformAppRoot(dir);
|
|
83
|
+
const blox = !app && isBloxRoot(dir);
|
|
84
|
+
if (app || blox) {
|
|
85
|
+
const rel = path.relative(dir, filePath).replace(/\\/g, '/');
|
|
86
|
+
return rel.startsWith('src/') ? { root: dir, isBlox: blox } : null;
|
|
87
|
+
}
|
|
88
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return null; // some other project
|
|
89
|
+
const parent = path.dirname(dir);
|
|
90
|
+
if (parent === dir) return null;
|
|
91
|
+
dir = parent;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sessionKey(data) {
|
|
97
|
+
const sid = (data && data.session_id) || String(process.ppid || process.pid);
|
|
98
|
+
return String(sid).replace(/[^A-Za-z0-9_-]/g, '').slice(0, 64) || 'x';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* True the first time this path is seen this session; records it. */
|
|
102
|
+
function firstTimeThisSession(data, normPath) {
|
|
103
|
+
const sf = path.join(os.tmpdir(), 'toga-reuse-reminder-' + sessionKey(data) + '.json');
|
|
104
|
+
let seen = [];
|
|
105
|
+
try { seen = JSON.parse(fs.readFileSync(sf, 'utf8')) || []; } catch (e) { seen = []; }
|
|
106
|
+
if (!Array.isArray(seen)) seen = [];
|
|
107
|
+
if (seen.includes(normPath)) return false;
|
|
108
|
+
seen.push(normPath);
|
|
109
|
+
try { fs.writeFileSync(sf, JSON.stringify(seen.slice(-MAX_SEEN_PATHS))); } catch (e) { /* non-fatal */ }
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function reminder(isBlox) {
|
|
114
|
+
const where = isBlox
|
|
115
|
+
? 'New file in toga25-blox. Before creating it: check INVENTORY.md in this repo'
|
|
116
|
+
: 'New file in a platform app. Before creating it: check `' + BLOX_INVENTORY + '` and this app\'s `' +
|
|
117
|
+
APP_REUSE_DOC + '`';
|
|
118
|
+
return [
|
|
119
|
+
'<system-reminder>',
|
|
120
|
+
where + ' — reuse or extend an existing piece first.',
|
|
121
|
+
'If this duplicates something in another page or app, consolidate instead',
|
|
122
|
+
'(rule: `' + CONSOLIDATION_DOC + '`).',
|
|
123
|
+
'</system-reminder>',
|
|
124
|
+
].join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function main() {
|
|
128
|
+
if (process.env.TOGA_REUSE_REMINDER_DISABLED === '1') return;
|
|
129
|
+
|
|
130
|
+
const data = readPayload();
|
|
131
|
+
if (!/^(Write|Edit)$/.test(data.tool_name || '')) return;
|
|
132
|
+
|
|
133
|
+
const input = data.tool_input || {};
|
|
134
|
+
const filePath = typeof input.file_path === 'string' ? input.file_path : '';
|
|
135
|
+
if (!filePath || !isWatchedExtension(filePath)) return;
|
|
136
|
+
|
|
137
|
+
const abs = path.resolve(data.cwd || process.cwd(), filePath);
|
|
138
|
+
if (fs.existsSync(abs)) return; // only new files
|
|
139
|
+
|
|
140
|
+
const hit = findPlatformRoot(abs);
|
|
141
|
+
if (!hit) return;
|
|
142
|
+
|
|
143
|
+
if (!firstTimeThisSession(data, abs.replace(/\\/g, '/').toLowerCase())) return;
|
|
144
|
+
|
|
145
|
+
console.log(JSON.stringify({
|
|
146
|
+
hookSpecificOutput: {
|
|
147
|
+
hookEventName: 'PreToolUse',
|
|
148
|
+
additionalContext: reminder(hit.isBlox),
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try { main(); } catch (e) { /* fail-open: never block or brick an edit */ }
|
|
154
|
+
process.exitCode = 0; // always 0; exitCode (not exit()) lets stdout flush on Windows pipes
|