@massu/core 1.2.1 → 1.4.0-soak.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/README.md +40 -0
- package/commands/README.md +137 -0
- package/commands/massu-deploy.python-docker.md +170 -0
- package/commands/massu-deploy.python-fly.md +189 -0
- package/commands/massu-deploy.python-launchd.md +144 -0
- package/commands/massu-deploy.python-systemd.md +163 -0
- package/commands/massu-deploy.python.md +200 -0
- package/commands/massu-scaffold-page.md +172 -59
- package/commands/massu-scaffold-page.swift.md +121 -0
- package/commands/massu-scaffold-router.python-django.md +153 -0
- package/commands/massu-scaffold-router.python-fastapi.md +145 -0
- package/commands/massu-scaffold-router.python.md +143 -0
- package/dist/cli.js +10170 -4138
- package/dist/hooks/auto-learning-pipeline.js +44 -6
- package/dist/hooks/classify-failure.js +44 -6
- package/dist/hooks/cost-tracker.js +44 -6
- package/dist/hooks/fix-detector.js +44 -6
- package/dist/hooks/incident-pipeline.js +44 -6
- package/dist/hooks/post-edit-context.js +44 -6
- package/dist/hooks/post-tool-use.js +44 -6
- package/dist/hooks/pre-compact.js +44 -6
- package/dist/hooks/pre-delete-check.js +44 -6
- package/dist/hooks/quality-event.js +44 -6
- package/dist/hooks/rule-enforcement-pipeline.js +44 -6
- package/dist/hooks/session-end.js +44 -6
- package/dist/hooks/session-start.js +4789 -410
- package/dist/hooks/user-prompt.js +44 -6
- package/package.json +10 -4
- package/src/cli.ts +28 -2
- package/src/commands/config-refresh.ts +88 -20
- package/src/commands/init.ts +130 -23
- package/src/commands/install-commands.ts +482 -42
- package/src/commands/refresh-log.ts +37 -0
- package/src/commands/show-template.ts +65 -0
- package/src/commands/template-engine.ts +262 -0
- package/src/commands/watch.ts +430 -0
- package/src/config.ts +69 -3
- package/src/detect/adapters/nextjs-trpc.ts +166 -0
- package/src/detect/adapters/parse-guard.ts +133 -0
- package/src/detect/adapters/python-django.ts +208 -0
- package/src/detect/adapters/python-fastapi.ts +223 -0
- package/src/detect/adapters/query-helpers.ts +170 -0
- package/src/detect/adapters/runner.ts +252 -0
- package/src/detect/adapters/swift-swiftui.ts +171 -0
- package/src/detect/adapters/tree-sitter-loader.ts +348 -0
- package/src/detect/adapters/types.ts +174 -0
- package/src/detect/codebase-introspector.ts +190 -0
- package/src/detect/index.ts +28 -2
- package/src/detect/regex-fallback.ts +449 -0
- package/src/hooks/session-start.ts +94 -3
- package/src/lib/gitToplevel.ts +22 -0
- package/src/lib/installLock.ts +179 -0
- package/src/lib/pidLiveness.ts +67 -0
- package/src/lsp/auto-detect.ts +89 -0
- package/src/lsp/client.ts +590 -0
- package/src/lsp/enrich.ts +127 -0
- package/src/lsp/types.ts +221 -0
- package/src/watch/daemon.ts +385 -0
- package/src/watch/lockfile-detector.ts +65 -0
- package/src/watch/paths.ts +279 -0
- package/src/watch/state.ts +178 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `massu show-template <name>` — print the resolved variant of a template.
|
|
6
|
+
*
|
|
7
|
+
* Used by the local-edit-protection messaging to support a workflow like:
|
|
8
|
+
*
|
|
9
|
+
* diff .claude/commands/massu-scaffold-router.md \
|
|
10
|
+
* <(npx massu show-template massu-scaffold-router)
|
|
11
|
+
*
|
|
12
|
+
* The resolved variant honors `pickVariant` against the consumer's current
|
|
13
|
+
* `massu.config.yaml`. Exits 0 on success, 1 on unknown template.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync } from 'fs';
|
|
17
|
+
import { resolve } from 'path';
|
|
18
|
+
import { getConfig } from '../config.ts';
|
|
19
|
+
import { pickVariant, resolveAssetDir } from './install-commands.ts';
|
|
20
|
+
|
|
21
|
+
/** Strip an optional trailing `.md` extension. */
|
|
22
|
+
function normalizeBaseName(input: string): string {
|
|
23
|
+
return input.endsWith('.md') ? input.slice(0, -'.md'.length) : input;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runShowTemplate(args: string[]): Promise<void> {
|
|
27
|
+
const rawName = args[0];
|
|
28
|
+
if (!rawName) {
|
|
29
|
+
process.stderr.write('massu: show-template requires a template name\n');
|
|
30
|
+
process.stderr.write(' usage: massu show-template <name>\n');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const baseName = normalizeBaseName(rawName);
|
|
36
|
+
const sourceDir = resolveAssetDir('commands');
|
|
37
|
+
if (!sourceDir) {
|
|
38
|
+
process.stderr.write('massu: could not locate the bundled commands directory\n');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const framework = getConfig().framework;
|
|
44
|
+
const choice = pickVariant(baseName, sourceDir, framework);
|
|
45
|
+
|
|
46
|
+
if (choice.kind === 'miss') {
|
|
47
|
+
process.stderr.write(`massu: no template named "${baseName}" found\n`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const suffix = choice.kind === 'hit' ? choice.suffix : '';
|
|
53
|
+
const file = suffix === ''
|
|
54
|
+
? resolve(sourceDir, `${baseName}.md`)
|
|
55
|
+
: resolve(sourceDir, `${baseName}${suffix}.md`);
|
|
56
|
+
|
|
57
|
+
if (!existsSync(file)) {
|
|
58
|
+
// Defensive: pickVariant said hit but the file isn't there.
|
|
59
|
+
process.stderr.write(`massu: resolved template "${file}" no longer exists\n`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
process.stdout.write(readFileSync(file, 'utf-8'));
|
|
65
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Massu codebase-aware templating engine — string substitution only.
|
|
6
|
+
*
|
|
7
|
+
* Spec: docs/internal/2026-04-26-codebase-aware-templates-spec.md
|
|
8
|
+
*
|
|
9
|
+
* Grammar (the entire surface):
|
|
10
|
+
* {{path.to.var}} Look up + render
|
|
11
|
+
* {{path.to.var | default("fallback")}} Look up; use literal on miss
|
|
12
|
+
* \{{ Literal `{{` (escape)
|
|
13
|
+
*
|
|
14
|
+
* Hard rules (do not weaken in maintenance):
|
|
15
|
+
* - NO `eval`, `Function`, `new Function`, `vm`, `child_process`, `exec`, `spawn`.
|
|
16
|
+
* - Variable lookup uses `Object.hasOwn` ONLY — no prototype walk.
|
|
17
|
+
* - Output is NEVER re-rendered (a value containing `{{x}}` stays literal).
|
|
18
|
+
* - No HTML escaping; output is markdown.
|
|
19
|
+
* - Single linear pass; no recursion, no fixed-point loop.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Thrown when a variable is missing and no `| default("...")` was given. */
|
|
23
|
+
export class MissingVariableError extends Error {
|
|
24
|
+
readonly path: string;
|
|
25
|
+
constructor(path: string) {
|
|
26
|
+
super(`Template variable not found: "${path}"`);
|
|
27
|
+
this.name = 'MissingVariableError';
|
|
28
|
+
this.path = path;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Thrown on an unbalanced or malformed template token. */
|
|
33
|
+
export class TemplateParseError extends Error {
|
|
34
|
+
readonly position: number;
|
|
35
|
+
constructor(message: string, position: number) {
|
|
36
|
+
super(`Template parse error at position ${position}: ${message}`);
|
|
37
|
+
this.name = 'TemplateParseError';
|
|
38
|
+
this.position = position;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render `template` against the given variables object.
|
|
44
|
+
*
|
|
45
|
+
* Throws:
|
|
46
|
+
* - `MissingVariableError` if a token references a path that doesn't exist
|
|
47
|
+
* and no `default("...")` is provided.
|
|
48
|
+
* - `TemplateParseError` on unbalanced `{{` (no closing `}}`) or malformed
|
|
49
|
+
* `default(...)` syntax.
|
|
50
|
+
*/
|
|
51
|
+
export function renderTemplate(template: string, vars: Record<string, unknown>): string {
|
|
52
|
+
const out: string[] = [];
|
|
53
|
+
const len = template.length;
|
|
54
|
+
let i = 0;
|
|
55
|
+
|
|
56
|
+
while (i < len) {
|
|
57
|
+
const ch = template[i];
|
|
58
|
+
|
|
59
|
+
// Escape sequence: \{{ → literal {{
|
|
60
|
+
if (ch === '\\' && i + 2 < len && template[i + 1] === '{' && template[i + 2] === '{') {
|
|
61
|
+
out.push('{{');
|
|
62
|
+
i += 3;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Token open: {{...}}
|
|
67
|
+
if (ch === '{' && i + 1 < len && template[i + 1] === '{') {
|
|
68
|
+
const tokenStart = i;
|
|
69
|
+
const closeIdx = findTokenClose(template, i + 2);
|
|
70
|
+
if (closeIdx === -1) {
|
|
71
|
+
throw new TemplateParseError('unclosed `{{` (no matching `}}`)', tokenStart);
|
|
72
|
+
}
|
|
73
|
+
const inner = template.slice(i + 2, closeIdx);
|
|
74
|
+
const rendered = renderToken(inner, vars, tokenStart);
|
|
75
|
+
out.push(rendered);
|
|
76
|
+
i = closeIdx + 2;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
out.push(ch);
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return out.join('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Find the closing `}}` for a token that starts at index `start` (which points
|
|
89
|
+
* to the first character INSIDE the `{{`). Skips `}}` that occur inside a
|
|
90
|
+
* double-quoted string literal (so `default("a }} b")` works).
|
|
91
|
+
*
|
|
92
|
+
* Returns the index of the first `}` of the closing `}}`, or -1 if not found.
|
|
93
|
+
*/
|
|
94
|
+
function findTokenClose(template: string, start: number): number {
|
|
95
|
+
const len = template.length;
|
|
96
|
+
let i = start;
|
|
97
|
+
let inString = false;
|
|
98
|
+
|
|
99
|
+
while (i < len) {
|
|
100
|
+
const ch = template[i];
|
|
101
|
+
|
|
102
|
+
if (inString) {
|
|
103
|
+
if (ch === '\\' && i + 1 < len) {
|
|
104
|
+
// Skip the next char (escape sequence inside default string literal).
|
|
105
|
+
i += 2;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (ch === '"') {
|
|
109
|
+
inString = false;
|
|
110
|
+
i++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
i++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (ch === '"') {
|
|
118
|
+
inString = true;
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (ch === '}' && i + 1 < len && template[i + 1] === '}') {
|
|
124
|
+
return i;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return -1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Render a single token (text BETWEEN `{{` and `}}`).
|
|
135
|
+
* Format: `path.to.var` OR `path.to.var | default("fallback")`.
|
|
136
|
+
*/
|
|
137
|
+
function renderToken(
|
|
138
|
+
inner: string,
|
|
139
|
+
vars: Record<string, unknown>,
|
|
140
|
+
tokenStart: number,
|
|
141
|
+
): string {
|
|
142
|
+
const trimmed = inner.trim();
|
|
143
|
+
if (trimmed === '') {
|
|
144
|
+
throw new TemplateParseError('empty token `{{}}`', tokenStart);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Split on the first unquoted `|`. Anything inside a string literal is preserved.
|
|
148
|
+
const pipeIdx = findUnquotedPipe(trimmed);
|
|
149
|
+
|
|
150
|
+
let path: string;
|
|
151
|
+
let defaultValue: string | null = null;
|
|
152
|
+
|
|
153
|
+
if (pipeIdx === -1) {
|
|
154
|
+
path = trimmed;
|
|
155
|
+
} else {
|
|
156
|
+
path = trimmed.slice(0, pipeIdx).trim();
|
|
157
|
+
const filterPart = trimmed.slice(pipeIdx + 1).trim();
|
|
158
|
+
defaultValue = parseDefaultFilter(filterPart, tokenStart);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!isValidPath(path)) {
|
|
162
|
+
throw new TemplateParseError(
|
|
163
|
+
`invalid variable path: "${path}" (allowed: dot-separated identifiers)`,
|
|
164
|
+
tokenStart,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const looked = lookup(vars, path);
|
|
169
|
+
if (looked === undefined) {
|
|
170
|
+
if (defaultValue !== null) return defaultValue;
|
|
171
|
+
throw new MissingVariableError(path);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return stringify(looked);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Find the first `|` outside a double-quoted string literal. Returns -1 if none.
|
|
179
|
+
* Backslash escapes are honored inside the string.
|
|
180
|
+
*/
|
|
181
|
+
function findUnquotedPipe(s: string): number {
|
|
182
|
+
let inString = false;
|
|
183
|
+
for (let i = 0; i < s.length; i++) {
|
|
184
|
+
const c = s[i];
|
|
185
|
+
if (inString) {
|
|
186
|
+
if (c === '\\' && i + 1 < s.length) {
|
|
187
|
+
i++;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (c === '"') inString = false;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (c === '"') {
|
|
194
|
+
inString = true;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (c === '|') return i;
|
|
198
|
+
}
|
|
199
|
+
return -1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Parse a `default("...")` filter, returning the literal string.
|
|
204
|
+
* Throws `TemplateParseError` on any deviation from the grammar.
|
|
205
|
+
*/
|
|
206
|
+
function parseDefaultFilter(filter: string, tokenStart: number): string {
|
|
207
|
+
// Must be exactly: `default(<string-literal>)`
|
|
208
|
+
const m = /^default\s*\(\s*"((?:\\.|[^"\\])*)"\s*\)\s*$/.exec(filter);
|
|
209
|
+
if (!m) {
|
|
210
|
+
throw new TemplateParseError(
|
|
211
|
+
`malformed filter: expected default("...")`,
|
|
212
|
+
tokenStart,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
// Decode \" → ", \\ → \. Other backslashes are preserved verbatim per spec.
|
|
216
|
+
const raw = m[1];
|
|
217
|
+
return raw.replace(/\\(["\\])/g, '$1');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validate a dot-walk path: must be one or more segments, each a valid identifier.
|
|
222
|
+
* Identifiers: ASCII letters, digits, underscore, hyphen; must not start with a digit.
|
|
223
|
+
* Hyphens are allowed because some YAML keys (e.g., `web-source`) use them.
|
|
224
|
+
*/
|
|
225
|
+
function isValidPath(path: string): boolean {
|
|
226
|
+
if (path.length === 0) return false;
|
|
227
|
+
const segments = path.split('.');
|
|
228
|
+
for (const seg of segments) {
|
|
229
|
+
if (seg.length === 0) return false;
|
|
230
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(seg)) return false;
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Walk a dot-path through `obj` using own-property lookups only. NEVER traverses
|
|
237
|
+
* the prototype chain. Returns `undefined` if any segment is missing or if the
|
|
238
|
+
* value at any non-leaf segment is not an object.
|
|
239
|
+
*/
|
|
240
|
+
function lookup(obj: Record<string, unknown>, path: string): unknown {
|
|
241
|
+
const segments = path.split('.');
|
|
242
|
+
let current: unknown = obj;
|
|
243
|
+
for (const seg of segments) {
|
|
244
|
+
if (current === null || current === undefined) return undefined;
|
|
245
|
+
if (typeof current !== 'object') return undefined;
|
|
246
|
+
if (Array.isArray(current)) return undefined;
|
|
247
|
+
if (!Object.hasOwn(current as object, seg)) return undefined;
|
|
248
|
+
current = (current as Record<string, unknown>)[seg];
|
|
249
|
+
}
|
|
250
|
+
return current;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Stringify a looked-up value. `undefined` is impossible at this point (caller
|
|
255
|
+
* already checked). `null` becomes the literal `"null"`. Everything else uses
|
|
256
|
+
* `String(value)`.
|
|
257
|
+
*/
|
|
258
|
+
function stringify(value: unknown): string {
|
|
259
|
+
if (value === null) return 'null';
|
|
260
|
+
if (typeof value === 'string') return value;
|
|
261
|
+
return String(value);
|
|
262
|
+
}
|