@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,145 +1,145 @@
|
|
|
1
|
-
// Consumer-config loader for Clawform. Reads <projectDir>/clawform.config.json,
|
|
2
|
-
// merges it onto the built-in defaults, returns a frozen object. Pure I/O — no
|
|
3
|
-
// validation of "is customer set"; callers that need it (naming.js) check at
|
|
4
|
-
// the point they need a value, with a focused error message.
|
|
5
|
-
//
|
|
6
|
-
// Merge order (low → high):
|
|
7
|
-
// 1. DEFAULT_CONFIG (src/lib/defaults.js)
|
|
8
|
-
// 2. <projectDir>/clawform.config.json (consumer-committed, may be missing)
|
|
9
|
-
// 3. caller-supplied overrides (CLI flags)
|
|
10
|
-
//
|
|
11
|
-
// Missing file → silently skipped (a fresh consumer is a valid state).
|
|
12
|
-
// Malformed JSON → throws with the file path and the parse error.
|
|
13
|
-
// Wrong shape (e.g. → throws — we'd rather fail loud than ship Acme's defaults under
|
|
14
|
-
// legacy_prefixes: a wrong key.
|
|
15
|
-
// "OLDNET")
|
|
16
|
-
|
|
17
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
18
|
-
import { join, basename } from 'node:path';
|
|
19
|
-
import { DEFAULT_CONFIG } from './defaults.js';
|
|
20
|
-
import {
|
|
21
|
-
CONFIG_FILENAMES,
|
|
22
|
-
isDeprecatedConfigFilename,
|
|
23
|
-
warnDeprecatedConfigFilename,
|
|
24
|
-
} from './rename-compat.js';
|
|
25
|
-
|
|
26
|
-
const ARRAY_KEYS = new Set(['envs', 'legacy_prefixes', 'service_shortnames_extra']);
|
|
27
|
-
const OBJECT_KEYS = new Set(['accounts', 'tags_defaults']);
|
|
28
|
-
const STRING_KEYS = new Set(['customer', 'region']);
|
|
29
|
-
|
|
30
|
-
// First match wins, so a project mid-rename that still has both files gets the
|
|
31
|
-
// new one. Silent about the old name here — the warning belongs on the read,
|
|
32
|
-
// not on every existence probe.
|
|
33
|
-
export function findConfigPath(projectDir) {
|
|
34
|
-
if (!projectDir) return null;
|
|
35
|
-
for (const name of CONFIG_FILENAMES) {
|
|
36
|
-
const p = join(projectDir, name);
|
|
37
|
-
if (existsSync(p)) return p;
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function readProjectConfig(projectDir) {
|
|
43
|
-
const p = findConfigPath(projectDir);
|
|
44
|
-
if (!p) return {};
|
|
45
|
-
if (isDeprecatedConfigFilename(basename(p))) warnDeprecatedConfigFilename(p);
|
|
46
|
-
let raw;
|
|
47
|
-
try {
|
|
48
|
-
raw = readFileSync(p, 'utf8');
|
|
49
|
-
} catch (err) {
|
|
50
|
-
throw new Error(`Failed to read ${p}: ${err.message}`);
|
|
51
|
-
}
|
|
52
|
-
let parsed;
|
|
53
|
-
try {
|
|
54
|
-
parsed = JSON.parse(raw);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
throw new Error(`Malformed JSON in ${p}: ${err.message}`);
|
|
57
|
-
}
|
|
58
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
59
|
-
throw new Error(`Expected JSON object in ${p}, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);
|
|
60
|
-
}
|
|
61
|
-
return validateShape(parsed, p);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function validateShape(cfg, sourcePath) {
|
|
65
|
-
const out = {};
|
|
66
|
-
for (const [k, v] of Object.entries(cfg)) {
|
|
67
|
-
if (k.startsWith('$')) continue;
|
|
68
|
-
if (STRING_KEYS.has(k)) {
|
|
69
|
-
if (typeof v !== 'string') throw new Error(`${sourcePath}: "${k}" must be a string`);
|
|
70
|
-
out[k] = v;
|
|
71
|
-
} else if (ARRAY_KEYS.has(k)) {
|
|
72
|
-
if (!Array.isArray(v) || !v.every((x) => typeof x === 'string')) {
|
|
73
|
-
throw new Error(`${sourcePath}: "${k}" must be an array of strings`);
|
|
74
|
-
}
|
|
75
|
-
out[k] = [...v];
|
|
76
|
-
} else if (OBJECT_KEYS.has(k)) {
|
|
77
|
-
if (v === null || typeof v !== 'object' || Array.isArray(v)) {
|
|
78
|
-
throw new Error(`${sourcePath}: "${k}" must be an object`);
|
|
79
|
-
}
|
|
80
|
-
out[k] = { ...v };
|
|
81
|
-
} else {
|
|
82
|
-
// Unknown key — keep verbatim so the consumer can add metadata, but don't
|
|
83
|
-
// act on it. This trades strictness for forward-compatibility.
|
|
84
|
-
out[k] = v;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return out;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function loadProjectConfig({ projectDir, overrides = {} } = {}) {
|
|
91
|
-
const fromFile = readProjectConfig(projectDir);
|
|
92
|
-
const merged = mergeShallow(DEFAULT_CONFIG, fromFile, overrides);
|
|
93
|
-
return Object.freeze(merged);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function mergeShallow(...sources) {
|
|
97
|
-
const out = {
|
|
98
|
-
...DEFAULT_CONFIG,
|
|
99
|
-
accounts: { ...DEFAULT_CONFIG.accounts },
|
|
100
|
-
tags_defaults: { ...DEFAULT_CONFIG.tags_defaults },
|
|
101
|
-
envs: [...DEFAULT_CONFIG.envs],
|
|
102
|
-
legacy_prefixes: [...DEFAULT_CONFIG.legacy_prefixes],
|
|
103
|
-
service_shortnames_extra: [...DEFAULT_CONFIG.service_shortnames_extra],
|
|
104
|
-
};
|
|
105
|
-
for (const src of sources) {
|
|
106
|
-
if (!src) continue;
|
|
107
|
-
for (const [k, v] of Object.entries(src)) {
|
|
108
|
-
if (OBJECT_KEYS.has(k) && v && typeof v === 'object' && !Array.isArray(v)) {
|
|
109
|
-
out[k] = { ...out[k], ...v };
|
|
110
|
-
} else if (ARRAY_KEYS.has(k) && Array.isArray(v)) {
|
|
111
|
-
out[k] = [...v];
|
|
112
|
-
} else {
|
|
113
|
-
out[k] = v;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function resolveProjectDir(explicit) {
|
|
121
|
-
return explicit || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Build the LEGACY_PREFIX_RE used by the hook from a runtime list. Returns null
|
|
125
|
-
// when the list is empty — the hook treats null as "no legacy-freeze gate".
|
|
126
|
-
//
|
|
127
|
-
// Unanchored (\b...) on purpose: the hook applies it to a whole COMMAND LINE,
|
|
128
|
-
// where the stack name appears mid-string. Do NOT reuse this against a bare
|
|
129
|
-
// export/stack name — `\b` treats every hyphen as a boundary, so a legacy token
|
|
130
|
-
// appearing mid-name (`p-acme-…-OLDAPP-arn`) would false-positive. Use the anchored
|
|
131
|
-
// builder below for whole-name checks.
|
|
132
|
-
export function buildLegacyPrefixRegex(prefixes) {
|
|
133
|
-
if (!Array.isArray(prefixes) || prefixes.length === 0) return null;
|
|
134
|
-
const escaped = prefixes.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
135
|
-
return new RegExp(`\\b(${escaped.join('|')})-[A-Za-z0-9-]+`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Anchored variant for matching a WHOLE name (export name, stack name) rather
|
|
139
|
-
// than a substring of a command line: the legacy prefix must be the name's
|
|
140
|
-
// leading segment. `OLDNET-VPC` matches; `p-acme-pri-lmd-payroll-OLDAPP-arn` does not.
|
|
141
|
-
export function buildAnchoredLegacyPrefixRegex(prefixes) {
|
|
142
|
-
if (!Array.isArray(prefixes) || prefixes.length === 0) return null;
|
|
143
|
-
const escaped = prefixes.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
144
|
-
return new RegExp(`^(${escaped.join('|')})-`);
|
|
145
|
-
}
|
|
1
|
+
// Consumer-config loader for Clawform. Reads <projectDir>/clawform.config.json,
|
|
2
|
+
// merges it onto the built-in defaults, returns a frozen object. Pure I/O — no
|
|
3
|
+
// validation of "is customer set"; callers that need it (naming.js) check at
|
|
4
|
+
// the point they need a value, with a focused error message.
|
|
5
|
+
//
|
|
6
|
+
// Merge order (low → high):
|
|
7
|
+
// 1. DEFAULT_CONFIG (src/lib/defaults.js)
|
|
8
|
+
// 2. <projectDir>/clawform.config.json (consumer-committed, may be missing)
|
|
9
|
+
// 3. caller-supplied overrides (CLI flags)
|
|
10
|
+
//
|
|
11
|
+
// Missing file → silently skipped (a fresh consumer is a valid state).
|
|
12
|
+
// Malformed JSON → throws with the file path and the parse error.
|
|
13
|
+
// Wrong shape (e.g. → throws — we'd rather fail loud than ship Acme's defaults under
|
|
14
|
+
// legacy_prefixes: a wrong key.
|
|
15
|
+
// "OLDNET")
|
|
16
|
+
|
|
17
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
18
|
+
import { join, basename } from 'node:path';
|
|
19
|
+
import { DEFAULT_CONFIG } from './defaults.js';
|
|
20
|
+
import {
|
|
21
|
+
CONFIG_FILENAMES,
|
|
22
|
+
isDeprecatedConfigFilename,
|
|
23
|
+
warnDeprecatedConfigFilename,
|
|
24
|
+
} from './rename-compat.js';
|
|
25
|
+
|
|
26
|
+
const ARRAY_KEYS = new Set(['envs', 'legacy_prefixes', 'service_shortnames_extra']);
|
|
27
|
+
const OBJECT_KEYS = new Set(['accounts', 'tags_defaults']);
|
|
28
|
+
const STRING_KEYS = new Set(['customer', 'region']);
|
|
29
|
+
|
|
30
|
+
// First match wins, so a project mid-rename that still has both files gets the
|
|
31
|
+
// new one. Silent about the old name here — the warning belongs on the read,
|
|
32
|
+
// not on every existence probe.
|
|
33
|
+
export function findConfigPath(projectDir) {
|
|
34
|
+
if (!projectDir) return null;
|
|
35
|
+
for (const name of CONFIG_FILENAMES) {
|
|
36
|
+
const p = join(projectDir, name);
|
|
37
|
+
if (existsSync(p)) return p;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readProjectConfig(projectDir) {
|
|
43
|
+
const p = findConfigPath(projectDir);
|
|
44
|
+
if (!p) return {};
|
|
45
|
+
if (isDeprecatedConfigFilename(basename(p))) warnDeprecatedConfigFilename(p);
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = readFileSync(p, 'utf8');
|
|
49
|
+
} catch (err) {
|
|
50
|
+
throw new Error(`Failed to read ${p}: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(raw);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
throw new Error(`Malformed JSON in ${p}: ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
59
|
+
throw new Error(`Expected JSON object in ${p}, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);
|
|
60
|
+
}
|
|
61
|
+
return validateShape(parsed, p);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateShape(cfg, sourcePath) {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
67
|
+
if (k.startsWith('$')) continue;
|
|
68
|
+
if (STRING_KEYS.has(k)) {
|
|
69
|
+
if (typeof v !== 'string') throw new Error(`${sourcePath}: "${k}" must be a string`);
|
|
70
|
+
out[k] = v;
|
|
71
|
+
} else if (ARRAY_KEYS.has(k)) {
|
|
72
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === 'string')) {
|
|
73
|
+
throw new Error(`${sourcePath}: "${k}" must be an array of strings`);
|
|
74
|
+
}
|
|
75
|
+
out[k] = [...v];
|
|
76
|
+
} else if (OBJECT_KEYS.has(k)) {
|
|
77
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) {
|
|
78
|
+
throw new Error(`${sourcePath}: "${k}" must be an object`);
|
|
79
|
+
}
|
|
80
|
+
out[k] = { ...v };
|
|
81
|
+
} else {
|
|
82
|
+
// Unknown key — keep verbatim so the consumer can add metadata, but don't
|
|
83
|
+
// act on it. This trades strictness for forward-compatibility.
|
|
84
|
+
out[k] = v;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function loadProjectConfig({ projectDir, overrides = {} } = {}) {
|
|
91
|
+
const fromFile = readProjectConfig(projectDir);
|
|
92
|
+
const merged = mergeShallow(DEFAULT_CONFIG, fromFile, overrides);
|
|
93
|
+
return Object.freeze(merged);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mergeShallow(...sources) {
|
|
97
|
+
const out = {
|
|
98
|
+
...DEFAULT_CONFIG,
|
|
99
|
+
accounts: { ...DEFAULT_CONFIG.accounts },
|
|
100
|
+
tags_defaults: { ...DEFAULT_CONFIG.tags_defaults },
|
|
101
|
+
envs: [...DEFAULT_CONFIG.envs],
|
|
102
|
+
legacy_prefixes: [...DEFAULT_CONFIG.legacy_prefixes],
|
|
103
|
+
service_shortnames_extra: [...DEFAULT_CONFIG.service_shortnames_extra],
|
|
104
|
+
};
|
|
105
|
+
for (const src of sources) {
|
|
106
|
+
if (!src) continue;
|
|
107
|
+
for (const [k, v] of Object.entries(src)) {
|
|
108
|
+
if (OBJECT_KEYS.has(k) && v && typeof v === 'object' && !Array.isArray(v)) {
|
|
109
|
+
out[k] = { ...out[k], ...v };
|
|
110
|
+
} else if (ARRAY_KEYS.has(k) && Array.isArray(v)) {
|
|
111
|
+
out[k] = [...v];
|
|
112
|
+
} else {
|
|
113
|
+
out[k] = v;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function resolveProjectDir(explicit) {
|
|
121
|
+
return explicit || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Build the LEGACY_PREFIX_RE used by the hook from a runtime list. Returns null
|
|
125
|
+
// when the list is empty — the hook treats null as "no legacy-freeze gate".
|
|
126
|
+
//
|
|
127
|
+
// Unanchored (\b...) on purpose: the hook applies it to a whole COMMAND LINE,
|
|
128
|
+
// where the stack name appears mid-string. Do NOT reuse this against a bare
|
|
129
|
+
// export/stack name — `\b` treats every hyphen as a boundary, so a legacy token
|
|
130
|
+
// appearing mid-name (`p-acme-…-OLDAPP-arn`) would false-positive. Use the anchored
|
|
131
|
+
// builder below for whole-name checks.
|
|
132
|
+
export function buildLegacyPrefixRegex(prefixes) {
|
|
133
|
+
if (!Array.isArray(prefixes) || prefixes.length === 0) return null;
|
|
134
|
+
const escaped = prefixes.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
135
|
+
return new RegExp(`\\b(${escaped.join('|')})-[A-Za-z0-9-]+`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Anchored variant for matching a WHOLE name (export name, stack name) rather
|
|
139
|
+
// than a substring of a command line: the legacy prefix must be the name's
|
|
140
|
+
// leading segment. `OLDNET-VPC` matches; `p-acme-pri-lmd-payroll-OLDAPP-arn` does not.
|
|
141
|
+
export function buildAnchoredLegacyPrefixRegex(prefixes) {
|
|
142
|
+
if (!Array.isArray(prefixes) || prefixes.length === 0) return null;
|
|
143
|
+
const escaped = prefixes.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
144
|
+
return new RegExp(`^(${escaped.join('|')})-`);
|
|
145
|
+
}
|