@thejoseki/clawform 2.3.0 → 2.3.2
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 +141 -120
- package/bin/clawform.js +161 -151
- package/package.json +65 -65
- package/src/aws/catalog.js +116 -116
- package/src/aws/changeset.js +231 -231
- package/src/aws/drift.js +59 -59
- package/src/aws/exports.js +213 -213
- package/src/aws/inventory.js +156 -156
- package/src/commands/activate.js +34 -2
- package/src/commands/ci-init.js +393 -212
- package/src/commands/cost-report.js +163 -163
- package/src/commands/deactivate.js +9 -2
- package/src/commands/deploy.js +413 -413
- package/src/commands/diff.js +39 -39
- package/src/commands/drift.js +35 -35
- package/src/commands/init-plugin.js +6 -6
- package/src/commands/init.js +360 -360
- package/src/commands/rollback.js +126 -126
- package/src/commands/status.js +147 -147
- package/src/commands/sync.js +50 -50
- package/src/commands/update.js +24 -0
- package/src/commands/validate.js +349 -349
- package/src/hooks/block-dangerous.js +62 -62
- package/src/hooks/cfn-lint-after-edit-eval.js +30 -30
- package/src/hooks/cfn-lint-after-edit.js +81 -81
- package/src/hooks/check-catalog-fresh-eval.js +63 -63
- package/src/hooks/check-catalog-fresh.js +33 -33
- package/src/hooks/session-context-eval.js +81 -81
- package/src/hooks/session-context.js +41 -41
- package/src/hooks/subagent-context-eval.js +44 -44
- package/src/hooks/subagent-context.js +48 -48
- package/src/lib/audit-synthesis.js +24 -24
- package/src/lib/aws-client.js +15 -15
- package/src/lib/cfn-yaml.js +92 -92
- package/src/lib/config.js +76 -76
- package/src/lib/constants.js +85 -85
- package/src/lib/defaults.js +16 -16
- package/src/lib/install-workflow.js +28 -28
- package/src/lib/kit-source.js +43 -5
- package/src/lib/license-client.js +84 -84
- package/src/lib/license-config.js +162 -162
- package/src/lib/license-store.js +43 -8
- package/src/lib/license.js +15 -2
- package/src/lib/lint-overrides.js +226 -226
- package/src/lib/logger.js +16 -16
- package/src/lib/project-config.js +145 -145
- package/src/lib/providers/polar.js +215 -215
- package/src/lib/session-state.js +91 -91
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Clawform PreToolUse hook — CLI entry. Reads JSON event from stdin, resolves
|
|
4
|
-
* the consumer's project root from the EVENT's `cwd` field (Claude Code sets
|
|
5
|
-
* this on every hook event), and delegates to the pure `evaluate` in
|
|
6
|
-
* block-dangerous-eval.js. Exits 2 with stderr on block.
|
|
7
|
-
*
|
|
8
|
-
* Why `event.cwd` instead of `process.env.CLAUDE_PROJECT_DIR`: the Claude Code
|
|
9
|
-
* hook docs (anthropic.com/docs/claude-code/hooks) document `cwd` as part of
|
|
10
|
-
* the stdin event payload but make no guarantee about CLAUDE_PROJECT_DIR being
|
|
11
|
-
* set in the hook process env, particularly when the hook is registered via a
|
|
12
|
-
* plugin's `hooks.json` (where `${CLAUDE_PLUGIN_ROOT}` substitutes into the
|
|
13
|
-
* command path but the runtime env is undocumented). Relying on the env var
|
|
14
|
-
* here risks a non-Acme consumer silently inheriting THIS repo's dogfood config
|
|
15
|
-
* — exactly the security bug the composable refactor exists to prevent.
|
|
16
|
-
* Fallback: process.env.CLAUDE_PROJECT_DIR → process.cwd() (the latter is
|
|
17
|
-
* useful for dev-mode invocations from the repo root).
|
|
18
|
-
*
|
|
19
|
-
* Fail-LOUD on event-parse error → write to stderr + exit 0 so a buggy hook
|
|
20
|
-
* never silently disables the safety net (the alternative — fail closed —
|
|
21
|
-
* would deny every Bash call on any unrelated parse problem).
|
|
22
|
-
*
|
|
23
|
-
* Fail-LOUD on config-load error → write to stderr + continue with an empty
|
|
24
|
-
* legacy-prefix list. Built-in blockers (delete-stack, s3 rb --force) still
|
|
25
|
-
* fire; only the legacy-freeze gate is dropped, which is the correct outcome
|
|
26
|
-
* when the consumer's clawform.config.json is broken or missing.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { readFileSync } from 'node:fs';
|
|
30
|
-
import { evaluate } from './block-dangerous-eval.js';
|
|
31
|
-
import { loadProjectConfig } from '../lib/project-config.js';
|
|
32
|
-
|
|
33
|
-
let event;
|
|
34
|
-
try {
|
|
35
|
-
event = JSON.parse(readFileSync(0, 'utf8'));
|
|
36
|
-
} catch (err) {
|
|
37
|
-
process.stderr.write(`[clawform hook] event parse failed (allowing tool call): ${err.message}\n`);
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const projectDir = (typeof event?.cwd === 'string' && event.cwd) || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
42
|
-
|
|
43
|
-
let legacyPrefixes = [];
|
|
44
|
-
try {
|
|
45
|
-
const cfg = loadProjectConfig({ projectDir });
|
|
46
|
-
legacyPrefixes = cfg.legacy_prefixes || [];
|
|
47
|
-
} catch (err) {
|
|
48
|
-
process.stderr.write(`[clawform hook] consumer config load failed at ${projectDir} (legacy-freeze gate disabled, built-ins still fire): ${err.message}\n`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const { block, warn, reason } = evaluate(event, { legacyPrefixes });
|
|
52
|
-
if (block) {
|
|
53
|
-
process.stderr.write(`[clawform hook] ${reason}\n`);
|
|
54
|
-
process.exit(2);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Advisory: the command runs, but the operator is told what the guard could
|
|
58
|
-
// not see. Same posture as the catalog-freshness hook.
|
|
59
|
-
if (warn) {
|
|
60
|
-
process.stderr.write(`[clawform hook] ${reason}\n`);
|
|
61
|
-
process.exit(0);
|
|
62
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Clawform PreToolUse hook — CLI entry. Reads JSON event from stdin, resolves
|
|
4
|
+
* the consumer's project root from the EVENT's `cwd` field (Claude Code sets
|
|
5
|
+
* this on every hook event), and delegates to the pure `evaluate` in
|
|
6
|
+
* block-dangerous-eval.js. Exits 2 with stderr on block.
|
|
7
|
+
*
|
|
8
|
+
* Why `event.cwd` instead of `process.env.CLAUDE_PROJECT_DIR`: the Claude Code
|
|
9
|
+
* hook docs (anthropic.com/docs/claude-code/hooks) document `cwd` as part of
|
|
10
|
+
* the stdin event payload but make no guarantee about CLAUDE_PROJECT_DIR being
|
|
11
|
+
* set in the hook process env, particularly when the hook is registered via a
|
|
12
|
+
* plugin's `hooks.json` (where `${CLAUDE_PLUGIN_ROOT}` substitutes into the
|
|
13
|
+
* command path but the runtime env is undocumented). Relying on the env var
|
|
14
|
+
* here risks a non-Acme consumer silently inheriting THIS repo's dogfood config
|
|
15
|
+
* — exactly the security bug the composable refactor exists to prevent.
|
|
16
|
+
* Fallback: process.env.CLAUDE_PROJECT_DIR → process.cwd() (the latter is
|
|
17
|
+
* useful for dev-mode invocations from the repo root).
|
|
18
|
+
*
|
|
19
|
+
* Fail-LOUD on event-parse error → write to stderr + exit 0 so a buggy hook
|
|
20
|
+
* never silently disables the safety net (the alternative — fail closed —
|
|
21
|
+
* would deny every Bash call on any unrelated parse problem).
|
|
22
|
+
*
|
|
23
|
+
* Fail-LOUD on config-load error → write to stderr + continue with an empty
|
|
24
|
+
* legacy-prefix list. Built-in blockers (delete-stack, s3 rb --force) still
|
|
25
|
+
* fire; only the legacy-freeze gate is dropped, which is the correct outcome
|
|
26
|
+
* when the consumer's clawform.config.json is broken or missing.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
30
|
+
import { evaluate } from './block-dangerous-eval.js';
|
|
31
|
+
import { loadProjectConfig } from '../lib/project-config.js';
|
|
32
|
+
|
|
33
|
+
let event;
|
|
34
|
+
try {
|
|
35
|
+
event = JSON.parse(readFileSync(0, 'utf8'));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
process.stderr.write(`[clawform hook] event parse failed (allowing tool call): ${err.message}\n`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const projectDir = (typeof event?.cwd === 'string' && event.cwd) || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
42
|
+
|
|
43
|
+
let legacyPrefixes = [];
|
|
44
|
+
try {
|
|
45
|
+
const cfg = loadProjectConfig({ projectDir });
|
|
46
|
+
legacyPrefixes = cfg.legacy_prefixes || [];
|
|
47
|
+
} catch (err) {
|
|
48
|
+
process.stderr.write(`[clawform hook] consumer config load failed at ${projectDir} (legacy-freeze gate disabled, built-ins still fire): ${err.message}\n`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { block, warn, reason } = evaluate(event, { legacyPrefixes });
|
|
52
|
+
if (block) {
|
|
53
|
+
process.stderr.write(`[clawform hook] ${reason}\n`);
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Advisory: the command runs, but the operator is told what the guard could
|
|
58
|
+
// not see. Same posture as the catalog-freshness hook.
|
|
59
|
+
if (warn) {
|
|
60
|
+
process.stderr.write(`[clawform hook] ${reason}\n`);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
// Pure decision logic for the PostToolUse cfn-lint hook. Given a
|
|
2
|
-
// PostToolUse event, returns { lint, file?, reason? } so the CLI shim
|
|
3
|
-
// knows whether (and on which file) to spawn cfn-lint. Exported separately
|
|
4
|
-
// so unit tests don't need to spawn anything.
|
|
5
|
-
//
|
|
6
|
-
// Trigger rules (intentionally narrow — we don't want to lint every YAML
|
|
7
|
-
// the user touches):
|
|
8
|
-
// tool_name ∈ {Edit, Write, MultiEdit}
|
|
9
|
-
// tool_input.file_path is a string ending in .yaml or .yml
|
|
10
|
-
// the path contains a `cfn/` or `templates/` segment (path-separator agnostic)
|
|
11
|
-
//
|
|
12
|
-
// The path-segment check is what makes the hook safe to ship as a global
|
|
13
|
-
// PostToolUse matcher: editing some-other-product/serverless.yaml elsewhere
|
|
14
|
-
// won't trip cfn-lint just because it's a YAML file.
|
|
15
|
-
|
|
16
|
-
const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
17
|
-
const YAML_RE = /\.ya?ml$/i;
|
|
18
|
-
// `[/\\]` covers POSIX and Windows separators; `(^|...)` matches either an
|
|
19
|
-
// initial segment or a sub-segment. `(?=[/\\])` ensures it's a directory.
|
|
20
|
-
const CFN_DIR_RE = /(^|[/\\])(cfn|templates)(?=[/\\])/i;
|
|
21
|
-
|
|
22
|
-
export function shouldLint(event) {
|
|
23
|
-
const toolName = event?.tool_name;
|
|
24
|
-
if (!EDIT_TOOLS.has(toolName)) return { lint: false };
|
|
25
|
-
const file = event?.tool_input?.file_path;
|
|
26
|
-
if (typeof file !== 'string') return { lint: false };
|
|
27
|
-
if (!YAML_RE.test(file)) return { lint: false };
|
|
28
|
-
if (!CFN_DIR_RE.test(file)) return { lint: false };
|
|
29
|
-
return { lint: true, file };
|
|
30
|
-
}
|
|
1
|
+
// Pure decision logic for the PostToolUse cfn-lint hook. Given a
|
|
2
|
+
// PostToolUse event, returns { lint, file?, reason? } so the CLI shim
|
|
3
|
+
// knows whether (and on which file) to spawn cfn-lint. Exported separately
|
|
4
|
+
// so unit tests don't need to spawn anything.
|
|
5
|
+
//
|
|
6
|
+
// Trigger rules (intentionally narrow — we don't want to lint every YAML
|
|
7
|
+
// the user touches):
|
|
8
|
+
// tool_name ∈ {Edit, Write, MultiEdit}
|
|
9
|
+
// tool_input.file_path is a string ending in .yaml or .yml
|
|
10
|
+
// the path contains a `cfn/` or `templates/` segment (path-separator agnostic)
|
|
11
|
+
//
|
|
12
|
+
// The path-segment check is what makes the hook safe to ship as a global
|
|
13
|
+
// PostToolUse matcher: editing some-other-product/serverless.yaml elsewhere
|
|
14
|
+
// won't trip cfn-lint just because it's a YAML file.
|
|
15
|
+
|
|
16
|
+
const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
17
|
+
const YAML_RE = /\.ya?ml$/i;
|
|
18
|
+
// `[/\\]` covers POSIX and Windows separators; `(^|...)` matches either an
|
|
19
|
+
// initial segment or a sub-segment. `(?=[/\\])` ensures it's a directory.
|
|
20
|
+
const CFN_DIR_RE = /(^|[/\\])(cfn|templates)(?=[/\\])/i;
|
|
21
|
+
|
|
22
|
+
export function shouldLint(event) {
|
|
23
|
+
const toolName = event?.tool_name;
|
|
24
|
+
if (!EDIT_TOOLS.has(toolName)) return { lint: false };
|
|
25
|
+
const file = event?.tool_input?.file_path;
|
|
26
|
+
if (typeof file !== 'string') return { lint: false };
|
|
27
|
+
if (!YAML_RE.test(file)) return { lint: false };
|
|
28
|
+
if (!CFN_DIR_RE.test(file)) return { lint: false };
|
|
29
|
+
return { lint: true, file };
|
|
30
|
+
}
|
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Clawform PostToolUse hook — CLI entry. Spawns cfn-lint after Edit/Write/MultiEdit
|
|
4
|
-
* on YAML files under cfn/ or templates/. Surfaces ERRORS back to Claude (exit 2 +
|
|
5
|
-
* stderr) so it can self-correct; WARNINGS go to stderr at exit 0 (advisory). When
|
|
6
|
-
* cfn-lint isn't installed, exits 0 silently — the hook is opt-in, not a gate.
|
|
7
|
-
*
|
|
8
|
-
* The pure decision logic lives in cfn-lint-after-edit-eval.js (unit-tested
|
|
9
|
-
* separately).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readFileSync } from 'node:fs';
|
|
13
|
-
import { spawnSync } from 'node:child_process';
|
|
14
|
-
import { shouldLint } from './cfn-lint-after-edit-eval.js';
|
|
15
|
-
|
|
16
|
-
let event;
|
|
17
|
-
try {
|
|
18
|
-
event = JSON.parse(readFileSync(0, 'utf8'));
|
|
19
|
-
} catch (err) {
|
|
20
|
-
// Fail-LOUD on parse error (allow the tool result through, but visibly warn).
|
|
21
|
-
process.stderr.write(`[clawform cfn-lint hook] event parse failed (skipping lint): ${err.message}\n`);
|
|
22
|
-
process.exit(0);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const decision = shouldLint(event);
|
|
26
|
-
if (!decision.lint) process.exit(0);
|
|
27
|
-
|
|
28
|
-
const r = spawnSync('cfn-lint', ['--format', 'json', decision.file], { encoding: 'utf8' });
|
|
29
|
-
|
|
30
|
-
// cfn-lint not installed → silent skip. Hook stays opt-in for boxes without Python.
|
|
31
|
-
if (r.error && (r.error.code === 'ENOENT' || r.error.errno === -2)) {
|
|
32
|
-
process.exit(0);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const stdout = (r.stdout ?? '').trim();
|
|
36
|
-
if (!stdout) {
|
|
37
|
-
// No findings — clean lint pass. Exit 0 quietly.
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let findings;
|
|
42
|
-
try {
|
|
43
|
-
findings = JSON.parse(stdout);
|
|
44
|
-
} catch {
|
|
45
|
-
process.stderr.write(`[clawform cfn-lint hook] could not parse cfn-lint output for ${decision.file} (skipping)\n`);
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let errors = 0;
|
|
50
|
-
let warnings = 0;
|
|
51
|
-
const lines = [];
|
|
52
|
-
for (const f of findings) {
|
|
53
|
-
const id = f.Rule?.Id ?? '?';
|
|
54
|
-
const loc = f.Location?.Start ? `L${f.Location.Start.LineNumber}` : '';
|
|
55
|
-
const msg = f.Message ?? '';
|
|
56
|
-
if (f.Level === 'Error') {
|
|
57
|
-
errors++;
|
|
58
|
-
lines.push(` ✖ ${id} ${loc}: ${msg}`);
|
|
59
|
-
} else if (f.Level === 'Warning') {
|
|
60
|
-
warnings++;
|
|
61
|
-
lines.push(` ⚠ ${id} ${loc}: ${msg}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (errors > 0) {
|
|
66
|
-
// Errors get exit 2 + stderr so Claude sees them in the next turn and self-corrects.
|
|
67
|
-
process.stderr.write(
|
|
68
|
-
`[clawform cfn-lint hook] ${errors} error(s), ${warnings} warning(s) in ${decision.file}:\n` +
|
|
69
|
-
lines.join('\n') +
|
|
70
|
-
'\n',
|
|
71
|
-
);
|
|
72
|
-
process.exit(2);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (warnings > 0) {
|
|
76
|
-
// Warnings stay advisory — user-visible, not blocking.
|
|
77
|
-
process.stderr.write(
|
|
78
|
-
`[clawform cfn-lint hook] ${warnings} warning(s) in ${decision.file}:\n` + lines.join('\n') + '\n',
|
|
79
|
-
);
|
|
80
|
-
process.exit(0);
|
|
81
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Clawform PostToolUse hook — CLI entry. Spawns cfn-lint after Edit/Write/MultiEdit
|
|
4
|
+
* on YAML files under cfn/ or templates/. Surfaces ERRORS back to Claude (exit 2 +
|
|
5
|
+
* stderr) so it can self-correct; WARNINGS go to stderr at exit 0 (advisory). When
|
|
6
|
+
* cfn-lint isn't installed, exits 0 silently — the hook is opt-in, not a gate.
|
|
7
|
+
*
|
|
8
|
+
* The pure decision logic lives in cfn-lint-after-edit-eval.js (unit-tested
|
|
9
|
+
* separately).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { shouldLint } from './cfn-lint-after-edit-eval.js';
|
|
15
|
+
|
|
16
|
+
let event;
|
|
17
|
+
try {
|
|
18
|
+
event = JSON.parse(readFileSync(0, 'utf8'));
|
|
19
|
+
} catch (err) {
|
|
20
|
+
// Fail-LOUD on parse error (allow the tool result through, but visibly warn).
|
|
21
|
+
process.stderr.write(`[clawform cfn-lint hook] event parse failed (skipping lint): ${err.message}\n`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const decision = shouldLint(event);
|
|
26
|
+
if (!decision.lint) process.exit(0);
|
|
27
|
+
|
|
28
|
+
const r = spawnSync('cfn-lint', ['--format', 'json', decision.file], { encoding: 'utf8' });
|
|
29
|
+
|
|
30
|
+
// cfn-lint not installed → silent skip. Hook stays opt-in for boxes without Python.
|
|
31
|
+
if (r.error && (r.error.code === 'ENOENT' || r.error.errno === -2)) {
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const stdout = (r.stdout ?? '').trim();
|
|
36
|
+
if (!stdout) {
|
|
37
|
+
// No findings — clean lint pass. Exit 0 quietly.
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let findings;
|
|
42
|
+
try {
|
|
43
|
+
findings = JSON.parse(stdout);
|
|
44
|
+
} catch {
|
|
45
|
+
process.stderr.write(`[clawform cfn-lint hook] could not parse cfn-lint output for ${decision.file} (skipping)\n`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let errors = 0;
|
|
50
|
+
let warnings = 0;
|
|
51
|
+
const lines = [];
|
|
52
|
+
for (const f of findings) {
|
|
53
|
+
const id = f.Rule?.Id ?? '?';
|
|
54
|
+
const loc = f.Location?.Start ? `L${f.Location.Start.LineNumber}` : '';
|
|
55
|
+
const msg = f.Message ?? '';
|
|
56
|
+
if (f.Level === 'Error') {
|
|
57
|
+
errors++;
|
|
58
|
+
lines.push(` ✖ ${id} ${loc}: ${msg}`);
|
|
59
|
+
} else if (f.Level === 'Warning') {
|
|
60
|
+
warnings++;
|
|
61
|
+
lines.push(` ⚠ ${id} ${loc}: ${msg}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (errors > 0) {
|
|
66
|
+
// Errors get exit 2 + stderr so Claude sees them in the next turn and self-corrects.
|
|
67
|
+
process.stderr.write(
|
|
68
|
+
`[clawform cfn-lint hook] ${errors} error(s), ${warnings} warning(s) in ${decision.file}:\n` +
|
|
69
|
+
lines.join('\n') +
|
|
70
|
+
'\n',
|
|
71
|
+
);
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (warnings > 0) {
|
|
76
|
+
// Warnings stay advisory — user-visible, not blocking.
|
|
77
|
+
process.stderr.write(
|
|
78
|
+
`[clawform cfn-lint hook] ${warnings} warning(s) in ${decision.file}:\n` + lines.join('\n') + '\n',
|
|
79
|
+
);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
// Pure decision logic for the PreToolUse `clawform deploy` catalog-freshness hook.
|
|
2
|
-
// Returns { warn|block, reason? } so the CLI shim handles all I/O.
|
|
3
|
-
//
|
|
4
|
-
// Rule:
|
|
5
|
-
// - If the command is `clawform deploy ...`, the consumer is about to import live exports.
|
|
6
|
-
// Block (exit 2) if `.clawform/catalog/outputs-catalog.md` doesn't exist — the user has never
|
|
7
|
-
// run `clawform sync`, so every Fn::ImportValue is unverifiable and changeset failure
|
|
8
|
-
// at deploy time is the predictable next step.
|
|
9
|
-
// - If the catalog file exists but is stale (>STALE_HOURS), warn (advisory) — the export
|
|
10
|
-
// set probably changed underneath. User decides.
|
|
11
|
-
// - Otherwise let it pass.
|
|
12
|
-
//
|
|
13
|
-
// Stays pure: takes (event, {cwd, now, stat}) and returns a verdict. The shim
|
|
14
|
-
// supplies real `cwd`, `Date.now()`, and `node:fs.statSync` — tests substitute fakes.
|
|
15
|
-
|
|
16
|
-
import { CATALOG_DIR_REL } from '../lib/constants.js';
|
|
17
|
-
|
|
18
|
-
const DEPLOY_RE = /(^|\s|;|&&|\|\|)clawform\s+deploy\b/;
|
|
19
|
-
// Derived, not restated: the gate must read exactly where `sync` writes, or it
|
|
20
|
-
// blocks every deploy with "run sync" even right after a successful sync.
|
|
21
|
-
export const CATALOG_REL = `${CATALOG_DIR_REL}/outputs-catalog.md`;
|
|
22
|
-
export const STALE_HOURS = 24;
|
|
23
|
-
|
|
24
|
-
// Resolve catalog path relative to a working directory. The path module's
|
|
25
|
-
// join is platform-aware; we accept either separator on input. Exported so
|
|
26
|
-
// other hooks (session-context-eval.js) can reuse the same resolution
|
|
27
|
-
// instead of re-deriving it (DRY — session-context reports the same
|
|
28
|
-
// freshness signal this hook gates on).
|
|
29
|
-
export function resolveCatalogPath(cwd) {
|
|
30
|
-
return cwd.endsWith('/') || cwd.endsWith('\\') ? cwd + CATALOG_REL : cwd + '/' + CATALOG_REL;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function checkCatalog(event, { cwd, now, statSync }) {
|
|
34
|
-
if (event?.tool_name !== 'Bash') return { ok: true };
|
|
35
|
-
const cmd = event?.tool_input?.command;
|
|
36
|
-
if (typeof cmd !== 'string' || !DEPLOY_RE.test(cmd)) return { ok: true };
|
|
37
|
-
|
|
38
|
-
const path = resolveCatalogPath(cwd);
|
|
39
|
-
|
|
40
|
-
let stat;
|
|
41
|
-
try {
|
|
42
|
-
stat = statSync(path);
|
|
43
|
-
} catch {
|
|
44
|
-
return {
|
|
45
|
-
block: true,
|
|
46
|
-
reason:
|
|
47
|
-
`Catalog ${CATALOG_REL} is missing — run \`clawform sync\` before deploying so Fn::ImportValue references can be verified against live exports.`,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const ageMs = now - new Date(stat.mtime).getTime();
|
|
52
|
-
const ageHours = ageMs / 3_600_000;
|
|
53
|
-
if (ageHours > STALE_HOURS) {
|
|
54
|
-
return {
|
|
55
|
-
warn: true,
|
|
56
|
-
reason:
|
|
57
|
-
`Catalog ${CATALOG_REL} is ${Math.round(ageHours)}h old (>${STALE_HOURS}h). ` +
|
|
58
|
-
'Consider `clawform sync` before deploy if any upstream stack changed recently.',
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { ok: true };
|
|
63
|
-
}
|
|
1
|
+
// Pure decision logic for the PreToolUse `clawform deploy` catalog-freshness hook.
|
|
2
|
+
// Returns { warn|block, reason? } so the CLI shim handles all I/O.
|
|
3
|
+
//
|
|
4
|
+
// Rule:
|
|
5
|
+
// - If the command is `clawform deploy ...`, the consumer is about to import live exports.
|
|
6
|
+
// Block (exit 2) if `.clawform/catalog/outputs-catalog.md` doesn't exist — the user has never
|
|
7
|
+
// run `clawform sync`, so every Fn::ImportValue is unverifiable and changeset failure
|
|
8
|
+
// at deploy time is the predictable next step.
|
|
9
|
+
// - If the catalog file exists but is stale (>STALE_HOURS), warn (advisory) — the export
|
|
10
|
+
// set probably changed underneath. User decides.
|
|
11
|
+
// - Otherwise let it pass.
|
|
12
|
+
//
|
|
13
|
+
// Stays pure: takes (event, {cwd, now, stat}) and returns a verdict. The shim
|
|
14
|
+
// supplies real `cwd`, `Date.now()`, and `node:fs.statSync` — tests substitute fakes.
|
|
15
|
+
|
|
16
|
+
import { CATALOG_DIR_REL } from '../lib/constants.js';
|
|
17
|
+
|
|
18
|
+
const DEPLOY_RE = /(^|\s|;|&&|\|\|)clawform\s+deploy\b/;
|
|
19
|
+
// Derived, not restated: the gate must read exactly where `sync` writes, or it
|
|
20
|
+
// blocks every deploy with "run sync" even right after a successful sync.
|
|
21
|
+
export const CATALOG_REL = `${CATALOG_DIR_REL}/outputs-catalog.md`;
|
|
22
|
+
export const STALE_HOURS = 24;
|
|
23
|
+
|
|
24
|
+
// Resolve catalog path relative to a working directory. The path module's
|
|
25
|
+
// join is platform-aware; we accept either separator on input. Exported so
|
|
26
|
+
// other hooks (session-context-eval.js) can reuse the same resolution
|
|
27
|
+
// instead of re-deriving it (DRY — session-context reports the same
|
|
28
|
+
// freshness signal this hook gates on).
|
|
29
|
+
export function resolveCatalogPath(cwd) {
|
|
30
|
+
return cwd.endsWith('/') || cwd.endsWith('\\') ? cwd + CATALOG_REL : cwd + '/' + CATALOG_REL;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function checkCatalog(event, { cwd, now, statSync }) {
|
|
34
|
+
if (event?.tool_name !== 'Bash') return { ok: true };
|
|
35
|
+
const cmd = event?.tool_input?.command;
|
|
36
|
+
if (typeof cmd !== 'string' || !DEPLOY_RE.test(cmd)) return { ok: true };
|
|
37
|
+
|
|
38
|
+
const path = resolveCatalogPath(cwd);
|
|
39
|
+
|
|
40
|
+
let stat;
|
|
41
|
+
try {
|
|
42
|
+
stat = statSync(path);
|
|
43
|
+
} catch {
|
|
44
|
+
return {
|
|
45
|
+
block: true,
|
|
46
|
+
reason:
|
|
47
|
+
`Catalog ${CATALOG_REL} is missing — run \`clawform sync\` before deploying so Fn::ImportValue references can be verified against live exports.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ageMs = now - new Date(stat.mtime).getTime();
|
|
52
|
+
const ageHours = ageMs / 3_600_000;
|
|
53
|
+
if (ageHours > STALE_HOURS) {
|
|
54
|
+
return {
|
|
55
|
+
warn: true,
|
|
56
|
+
reason:
|
|
57
|
+
`Catalog ${CATALOG_REL} is ${Math.round(ageHours)}h old (>${STALE_HOURS}h). ` +
|
|
58
|
+
'Consider `clawform sync` before deploy if any upstream stack changed recently.',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { ok: true };
|
|
63
|
+
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Clawform PreToolUse hook — catalog-freshness guard for `clawform deploy`.
|
|
4
|
-
* Reads JSON event from stdin, delegates to pure `checkCatalog`, and:
|
|
5
|
-
* - exit 2 + stderr → catalog missing (blocks deploy until clawform sync runs)
|
|
6
|
-
* - exit 0 + stderr → catalog stale (>24h) — advisory only
|
|
7
|
-
* - exit 0 silent → no deploy command, or catalog fresh
|
|
8
|
-
*
|
|
9
|
-
* Fail-LOUD on parse error so a buggy hook never silently disables the guard.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
13
|
-
import { checkCatalog } from './check-catalog-fresh-eval.js';
|
|
14
|
-
|
|
15
|
-
let event;
|
|
16
|
-
try {
|
|
17
|
-
event = JSON.parse(readFileSync(0, 'utf8'));
|
|
18
|
-
} catch (err) {
|
|
19
|
-
process.stderr.write(`[clawform catalog hook] event parse failed (allowing tool call): ${err.message}\n`);
|
|
20
|
-
process.exit(0);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const verdict = checkCatalog(event, { cwd: process.cwd(), now: Date.now(), statSync });
|
|
24
|
-
|
|
25
|
-
if (verdict.block) {
|
|
26
|
-
process.stderr.write(`[clawform catalog hook] ${verdict.reason}\n`);
|
|
27
|
-
process.exit(2);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (verdict.warn) {
|
|
31
|
-
process.stderr.write(`[clawform catalog hook] ${verdict.reason}\n`);
|
|
32
|
-
process.exit(0);
|
|
33
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Clawform PreToolUse hook — catalog-freshness guard for `clawform deploy`.
|
|
4
|
+
* Reads JSON event from stdin, delegates to pure `checkCatalog`, and:
|
|
5
|
+
* - exit 2 + stderr → catalog missing (blocks deploy until clawform sync runs)
|
|
6
|
+
* - exit 0 + stderr → catalog stale (>24h) — advisory only
|
|
7
|
+
* - exit 0 silent → no deploy command, or catalog fresh
|
|
8
|
+
*
|
|
9
|
+
* Fail-LOUD on parse error so a buggy hook never silently disables the guard.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
13
|
+
import { checkCatalog } from './check-catalog-fresh-eval.js';
|
|
14
|
+
|
|
15
|
+
let event;
|
|
16
|
+
try {
|
|
17
|
+
event = JSON.parse(readFileSync(0, 'utf8'));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
process.stderr.write(`[clawform catalog hook] event parse failed (allowing tool call): ${err.message}\n`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const verdict = checkCatalog(event, { cwd: process.cwd(), now: Date.now(), statSync });
|
|
24
|
+
|
|
25
|
+
if (verdict.block) {
|
|
26
|
+
process.stderr.write(`[clawform catalog hook] ${verdict.reason}\n`);
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (verdict.warn) {
|
|
31
|
+
process.stderr.write(`[clawform catalog hook] ${verdict.reason}\n`);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|