@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.
Files changed (48) hide show
  1. package/README.md +141 -120
  2. package/bin/clawform.js +161 -151
  3. package/package.json +65 -65
  4. package/src/aws/catalog.js +116 -116
  5. package/src/aws/changeset.js +231 -231
  6. package/src/aws/drift.js +59 -59
  7. package/src/aws/exports.js +213 -213
  8. package/src/aws/inventory.js +156 -156
  9. package/src/commands/activate.js +34 -2
  10. package/src/commands/ci-init.js +393 -212
  11. package/src/commands/cost-report.js +163 -163
  12. package/src/commands/deactivate.js +9 -2
  13. package/src/commands/deploy.js +413 -413
  14. package/src/commands/diff.js +39 -39
  15. package/src/commands/drift.js +35 -35
  16. package/src/commands/init-plugin.js +6 -6
  17. package/src/commands/init.js +360 -360
  18. package/src/commands/rollback.js +126 -126
  19. package/src/commands/status.js +147 -147
  20. package/src/commands/sync.js +50 -50
  21. package/src/commands/update.js +24 -0
  22. package/src/commands/validate.js +349 -349
  23. package/src/hooks/block-dangerous.js +62 -62
  24. package/src/hooks/cfn-lint-after-edit-eval.js +30 -30
  25. package/src/hooks/cfn-lint-after-edit.js +81 -81
  26. package/src/hooks/check-catalog-fresh-eval.js +63 -63
  27. package/src/hooks/check-catalog-fresh.js +33 -33
  28. package/src/hooks/session-context-eval.js +81 -81
  29. package/src/hooks/session-context.js +41 -41
  30. package/src/hooks/subagent-context-eval.js +44 -44
  31. package/src/hooks/subagent-context.js +48 -48
  32. package/src/lib/audit-synthesis.js +24 -24
  33. package/src/lib/aws-client.js +15 -15
  34. package/src/lib/cfn-yaml.js +92 -92
  35. package/src/lib/config.js +76 -76
  36. package/src/lib/constants.js +85 -85
  37. package/src/lib/defaults.js +16 -16
  38. package/src/lib/install-workflow.js +28 -28
  39. package/src/lib/kit-source.js +43 -5
  40. package/src/lib/license-client.js +84 -84
  41. package/src/lib/license-config.js +162 -162
  42. package/src/lib/license-store.js +43 -8
  43. package/src/lib/license.js +15 -2
  44. package/src/lib/lint-overrides.js +226 -226
  45. package/src/lib/logger.js +16 -16
  46. package/src/lib/project-config.js +145 -145
  47. package/src/lib/providers/polar.js +215 -215
  48. 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
+ }