@thejoseki/clawform 2.3.1 → 2.4.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 +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 +137 -105
- 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 +163 -150
- 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
package/src/lib/cfn-yaml.js
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
// Shared CloudFormation-YAML helpers.
|
|
2
|
-
//
|
|
3
|
-
// CloudFormation templates use short-tag intrinsics (`!Ref`, `!Sub`, `!GetAtt`,
|
|
4
|
-
// …) that stock js-yaml rejects. This module owns the one CFN-aware schema and
|
|
5
|
-
// the small extractors built on it, so every caller parses templates the same
|
|
6
|
-
// way instead of re-declaring tag tables.
|
|
7
|
-
//
|
|
8
|
-
// The schema is construct-only: `!Sub "x"` becomes `{ 'Fn::Sub': 'x' }`. We only
|
|
9
|
-
// ever READ the parsed shape (we never dump back through it here), so the long
|
|
10
|
-
// form is convenient and lossless for our purposes.
|
|
11
|
-
//
|
|
12
|
-
// Used by:
|
|
13
|
-
// - src/lib/lint-overrides.js (inject / detect cfn-guard suppressions)
|
|
14
|
-
// - src/aws/exports.js (export-lock + frozen-export pre-checks)
|
|
15
|
-
|
|
16
|
-
import yaml from 'js-yaml';
|
|
17
|
-
|
|
18
|
-
const CFN_TAGS = {
|
|
19
|
-
'!Ref': 'Ref', '!Sub': 'Fn::Sub', '!GetAtt': 'Fn::GetAtt',
|
|
20
|
-
'!ImportValue': 'Fn::ImportValue', '!Join': 'Fn::Join',
|
|
21
|
-
'!Select': 'Fn::Select', '!Split': 'Fn::Split',
|
|
22
|
-
'!FindInMap': 'Fn::FindInMap', '!If': 'Fn::If',
|
|
23
|
-
'!Equals': 'Fn::Equals', '!And': 'Fn::And', '!Or': 'Fn::Or',
|
|
24
|
-
'!Not': 'Fn::Not', '!Base64': 'Fn::Base64', '!Cidr': 'Fn::Cidr',
|
|
25
|
-
'!GetAZs': 'Fn::GetAZs', '!Condition': 'Condition',
|
|
26
|
-
'!Transform': 'Fn::Transform',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const CFN_SCHEMA = yaml.DEFAULT_SCHEMA.extend(
|
|
30
|
-
Object.entries(CFN_TAGS).flatMap(([tag, long]) =>
|
|
31
|
-
['scalar', 'sequence', 'mapping'].map(
|
|
32
|
-
(kind) => new yaml.Type(tag, { kind, construct: (data) => ({ [long]: data }) }),
|
|
33
|
-
),
|
|
34
|
-
),
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
// Best-effort parse of a CloudFormation template body. Returns the parsed doc,
|
|
38
|
-
// or null on any parse error (unknown short tag, malformed YAML). Callers treat
|
|
39
|
-
// null as "couldn't inspect" and fail open — the real linters / changeset still
|
|
40
|
-
// run against the original file and surface the parse problem with their own
|
|
41
|
-
// diagnostics. Mirrors the contract in lint-overrides.js.
|
|
42
|
-
export function loadCfnTemplate(body) {
|
|
43
|
-
try {
|
|
44
|
-
const doc = yaml.load(body, { schema: CFN_SCHEMA });
|
|
45
|
-
return doc && typeof doc === 'object' ? doc : null;
|
|
46
|
-
} catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Set of Output logical keys (OutputKey) that still declare an `Export.Name`.
|
|
52
|
-
//
|
|
53
|
-
// Why keys, not export-name values: an Output's logical key is a plain YAML map
|
|
54
|
-
// key — exact string, intrinsic-free. The export *value* (`Export.Name`) is
|
|
55
|
-
// frequently an intrinsic (`!Sub "${EnvName}-..."`) we can't resolve offline.
|
|
56
|
-
// Live stack Outputs report both the OutputKey AND the resolved ExportName, so
|
|
57
|
-
// matching live→template by OutputKey lets us detect an export removal with zero
|
|
58
|
-
// false positives — see findRemovedExports() in src/aws/exports.js.
|
|
59
|
-
export function extractExportingOutputKeys(doc) {
|
|
60
|
-
const keys = new Set();
|
|
61
|
-
const outputs = doc && typeof doc === 'object' ? doc.Outputs : null;
|
|
62
|
-
if (!outputs || typeof outputs !== 'object') return keys;
|
|
63
|
-
for (const [key, out] of Object.entries(outputs)) {
|
|
64
|
-
if (!out || typeof out !== 'object') continue;
|
|
65
|
-
const exp = out.Export;
|
|
66
|
-
// Export.Name truthy → this output still exports. The Name may be a string
|
|
67
|
-
// or an intrinsic object (e.g. { 'Fn::Sub': '...' }); presence is all we need.
|
|
68
|
-
if (exp && typeof exp === 'object' && exp.Name != null && exp.Name !== '') {
|
|
69
|
-
keys.add(key);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return keys;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Set of Export.Name values that are plain string literals (intrinsics like
|
|
76
|
-
// `!Sub "${EnvName}-…"` can't be resolved offline and are skipped). Two users:
|
|
77
|
-
// - export-lock pre-check: a live export whose name still appears as a literal
|
|
78
|
-
// Export.Name in the new template was merely moved to a different OutputKey,
|
|
79
|
-
// not removed — skipping it kills the false positive on OutputKey renames.
|
|
80
|
-
// - frozen-export introduction gate: a literal Export.Name matching a legacy
|
|
81
|
-
// prefix is refused before it ever goes live.
|
|
82
|
-
export function extractLiteralExportNames(doc) {
|
|
83
|
-
const names = new Set();
|
|
84
|
-
const outputs = doc && typeof doc === 'object' ? doc.Outputs : null;
|
|
85
|
-
if (!outputs || typeof outputs !== 'object') return names;
|
|
86
|
-
for (const out of Object.values(outputs)) {
|
|
87
|
-
if (!out || typeof out !== 'object') continue;
|
|
88
|
-
const name = out.Export?.Name;
|
|
89
|
-
if (typeof name === 'string' && name !== '') names.add(name);
|
|
90
|
-
}
|
|
91
|
-
return names;
|
|
92
|
-
}
|
|
1
|
+
// Shared CloudFormation-YAML helpers.
|
|
2
|
+
//
|
|
3
|
+
// CloudFormation templates use short-tag intrinsics (`!Ref`, `!Sub`, `!GetAtt`,
|
|
4
|
+
// …) that stock js-yaml rejects. This module owns the one CFN-aware schema and
|
|
5
|
+
// the small extractors built on it, so every caller parses templates the same
|
|
6
|
+
// way instead of re-declaring tag tables.
|
|
7
|
+
//
|
|
8
|
+
// The schema is construct-only: `!Sub "x"` becomes `{ 'Fn::Sub': 'x' }`. We only
|
|
9
|
+
// ever READ the parsed shape (we never dump back through it here), so the long
|
|
10
|
+
// form is convenient and lossless for our purposes.
|
|
11
|
+
//
|
|
12
|
+
// Used by:
|
|
13
|
+
// - src/lib/lint-overrides.js (inject / detect cfn-guard suppressions)
|
|
14
|
+
// - src/aws/exports.js (export-lock + frozen-export pre-checks)
|
|
15
|
+
|
|
16
|
+
import yaml from 'js-yaml';
|
|
17
|
+
|
|
18
|
+
const CFN_TAGS = {
|
|
19
|
+
'!Ref': 'Ref', '!Sub': 'Fn::Sub', '!GetAtt': 'Fn::GetAtt',
|
|
20
|
+
'!ImportValue': 'Fn::ImportValue', '!Join': 'Fn::Join',
|
|
21
|
+
'!Select': 'Fn::Select', '!Split': 'Fn::Split',
|
|
22
|
+
'!FindInMap': 'Fn::FindInMap', '!If': 'Fn::If',
|
|
23
|
+
'!Equals': 'Fn::Equals', '!And': 'Fn::And', '!Or': 'Fn::Or',
|
|
24
|
+
'!Not': 'Fn::Not', '!Base64': 'Fn::Base64', '!Cidr': 'Fn::Cidr',
|
|
25
|
+
'!GetAZs': 'Fn::GetAZs', '!Condition': 'Condition',
|
|
26
|
+
'!Transform': 'Fn::Transform',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const CFN_SCHEMA = yaml.DEFAULT_SCHEMA.extend(
|
|
30
|
+
Object.entries(CFN_TAGS).flatMap(([tag, long]) =>
|
|
31
|
+
['scalar', 'sequence', 'mapping'].map(
|
|
32
|
+
(kind) => new yaml.Type(tag, { kind, construct: (data) => ({ [long]: data }) }),
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Best-effort parse of a CloudFormation template body. Returns the parsed doc,
|
|
38
|
+
// or null on any parse error (unknown short tag, malformed YAML). Callers treat
|
|
39
|
+
// null as "couldn't inspect" and fail open — the real linters / changeset still
|
|
40
|
+
// run against the original file and surface the parse problem with their own
|
|
41
|
+
// diagnostics. Mirrors the contract in lint-overrides.js.
|
|
42
|
+
export function loadCfnTemplate(body) {
|
|
43
|
+
try {
|
|
44
|
+
const doc = yaml.load(body, { schema: CFN_SCHEMA });
|
|
45
|
+
return doc && typeof doc === 'object' ? doc : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Set of Output logical keys (OutputKey) that still declare an `Export.Name`.
|
|
52
|
+
//
|
|
53
|
+
// Why keys, not export-name values: an Output's logical key is a plain YAML map
|
|
54
|
+
// key — exact string, intrinsic-free. The export *value* (`Export.Name`) is
|
|
55
|
+
// frequently an intrinsic (`!Sub "${EnvName}-..."`) we can't resolve offline.
|
|
56
|
+
// Live stack Outputs report both the OutputKey AND the resolved ExportName, so
|
|
57
|
+
// matching live→template by OutputKey lets us detect an export removal with zero
|
|
58
|
+
// false positives — see findRemovedExports() in src/aws/exports.js.
|
|
59
|
+
export function extractExportingOutputKeys(doc) {
|
|
60
|
+
const keys = new Set();
|
|
61
|
+
const outputs = doc && typeof doc === 'object' ? doc.Outputs : null;
|
|
62
|
+
if (!outputs || typeof outputs !== 'object') return keys;
|
|
63
|
+
for (const [key, out] of Object.entries(outputs)) {
|
|
64
|
+
if (!out || typeof out !== 'object') continue;
|
|
65
|
+
const exp = out.Export;
|
|
66
|
+
// Export.Name truthy → this output still exports. The Name may be a string
|
|
67
|
+
// or an intrinsic object (e.g. { 'Fn::Sub': '...' }); presence is all we need.
|
|
68
|
+
if (exp && typeof exp === 'object' && exp.Name != null && exp.Name !== '') {
|
|
69
|
+
keys.add(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return keys;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Set of Export.Name values that are plain string literals (intrinsics like
|
|
76
|
+
// `!Sub "${EnvName}-…"` can't be resolved offline and are skipped). Two users:
|
|
77
|
+
// - export-lock pre-check: a live export whose name still appears as a literal
|
|
78
|
+
// Export.Name in the new template was merely moved to a different OutputKey,
|
|
79
|
+
// not removed — skipping it kills the false positive on OutputKey renames.
|
|
80
|
+
// - frozen-export introduction gate: a literal Export.Name matching a legacy
|
|
81
|
+
// prefix is refused before it ever goes live.
|
|
82
|
+
export function extractLiteralExportNames(doc) {
|
|
83
|
+
const names = new Set();
|
|
84
|
+
const outputs = doc && typeof doc === 'object' ? doc.Outputs : null;
|
|
85
|
+
if (!outputs || typeof outputs !== 'object') return names;
|
|
86
|
+
for (const out of Object.values(outputs)) {
|
|
87
|
+
if (!out || typeof out !== 'object') continue;
|
|
88
|
+
const name = out.Export?.Name;
|
|
89
|
+
if (typeof name === 'string' && name !== '') names.add(name);
|
|
90
|
+
}
|
|
91
|
+
return names;
|
|
92
|
+
}
|
package/src/lib/config.js
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
const ENV_KEYS = ['AWS_PROFILE', 'AWS_REGION', 'ACCOUNT_DEV', 'ACCOUNT_STG', 'ACCOUNT_PROD', 'MCP_AWS_SERVER', 'OWNER', 'COST_CENTER'];
|
|
2
|
-
const REQUIRED = ['AWS_PROFILE', 'AWS_REGION'];
|
|
3
|
-
|
|
4
|
-
export function loadConfig(cliOverrides = {}) {
|
|
5
|
-
const fromEnv = {};
|
|
6
|
-
for (const key of ENV_KEYS) {
|
|
7
|
-
if (process.env[key]) fromEnv[key] = process.env[key];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const merged = { ...fromEnv, ...cliOverrides };
|
|
11
|
-
|
|
12
|
-
const missing = REQUIRED.filter((k) => !merged[k]);
|
|
13
|
-
if (missing.length) {
|
|
14
|
-
throw new Error(
|
|
15
|
-
`Missing required config: ${missing.join(', ')}. ` +
|
|
16
|
-
`Set via .claude/settings.local.json "env" block (copy .claude/settings.local.json.example) ` +
|
|
17
|
-
`or shell env vars (e.g. AWS_PROFILE=acme-admin AWS_REGION=ap-northeast-1).`
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return merged;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Resolve the per-stack tag values. Owner / CostCenter come from a two-source
|
|
25
|
-
// chain: shell env var (per-user, gitignored settings.local.json) wins over
|
|
26
|
-
// clawform.config.json tags_defaults (per-project, committed). Either source
|
|
27
|
-
// alone is sufficient; both unset is a hard error pointing at both options.
|
|
28
|
-
//
|
|
29
|
-
// Why env wins: a developer running locally with their own handle should not
|
|
30
|
-
// have to edit the committed project config. tags_defaults is the team default
|
|
31
|
-
// for CI / CodePipeline / any context without per-user env.
|
|
32
|
-
//
|
|
33
|
-
// Args:
|
|
34
|
-
// projectConfig — result of loadProjectConfig(). Reads .customer and
|
|
35
|
-
// .tags_defaults. Pure data, no I/O.
|
|
36
|
-
// envName — the env string ('d' | 'stg' | 'p') passed to deploy().
|
|
37
|
-
// Used as the Environment value only.
|
|
38
|
-
//
|
|
39
|
-
// Returns: { Owner, CostCenter, Environment, ManagedBy, Customer }.
|
|
40
|
-
// Callers map this into the stack-level Tags array; the key spelling there
|
|
41
|
-
// (Env vs Environment) is the caller's choice — rules/tagging.md pins it to "Env".
|
|
42
|
-
export function resolveStackTags(projectConfig, envName) {
|
|
43
|
-
const defaults = (projectConfig && projectConfig.tags_defaults) || {};
|
|
44
|
-
const envOwner = (process.env.OWNER || '').trim();
|
|
45
|
-
const envCost = (process.env.COST_CENTER || '').trim();
|
|
46
|
-
const cfgOwner = typeof defaults.Owner === 'string' ? defaults.Owner.trim() : '';
|
|
47
|
-
const cfgCost = typeof defaults.CostCenter === 'string' ? defaults.CostCenter.trim() : '';
|
|
48
|
-
|
|
49
|
-
const Owner = envOwner || cfgOwner;
|
|
50
|
-
if (!Owner) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
'Owner tag is unset. Set one of:\n' +
|
|
53
|
-
' 1. OWNER env var (e.g. .claude/settings.local.json "env" block) — per-user override, wins.\n' +
|
|
54
|
-
' 2. tags_defaults.Owner in clawform.config.json — per-project team default.\n' +
|
|
55
|
-
'See docs/config-schema.md and rules/tagging.md.'
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const CostCenter = envCost || cfgCost;
|
|
60
|
-
if (!CostCenter) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
'CostCenter tag is unset. Set one of:\n' +
|
|
63
|
-
' 1. COST_CENTER env var (e.g. .claude/settings.local.json "env" block) — per-user override, wins.\n' +
|
|
64
|
-
' 2. tags_defaults.CostCenter in clawform.config.json — per-project team default.\n' +
|
|
65
|
-
'See docs/config-schema.md and rules/tagging.md.'
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
Owner,
|
|
71
|
-
CostCenter,
|
|
72
|
-
Environment: envName,
|
|
73
|
-
ManagedBy: 'CloudFormation',
|
|
74
|
-
Customer: (projectConfig && projectConfig.customer) || '',
|
|
75
|
-
};
|
|
76
|
-
}
|
|
1
|
+
const ENV_KEYS = ['AWS_PROFILE', 'AWS_REGION', 'ACCOUNT_DEV', 'ACCOUNT_STG', 'ACCOUNT_PROD', 'MCP_AWS_SERVER', 'OWNER', 'COST_CENTER'];
|
|
2
|
+
const REQUIRED = ['AWS_PROFILE', 'AWS_REGION'];
|
|
3
|
+
|
|
4
|
+
export function loadConfig(cliOverrides = {}) {
|
|
5
|
+
const fromEnv = {};
|
|
6
|
+
for (const key of ENV_KEYS) {
|
|
7
|
+
if (process.env[key]) fromEnv[key] = process.env[key];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const merged = { ...fromEnv, ...cliOverrides };
|
|
11
|
+
|
|
12
|
+
const missing = REQUIRED.filter((k) => !merged[k]);
|
|
13
|
+
if (missing.length) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Missing required config: ${missing.join(', ')}. ` +
|
|
16
|
+
`Set via .claude/settings.local.json "env" block (copy .claude/settings.local.json.example) ` +
|
|
17
|
+
`or shell env vars (e.g. AWS_PROFILE=acme-admin AWS_REGION=ap-northeast-1).`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return merged;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Resolve the per-stack tag values. Owner / CostCenter come from a two-source
|
|
25
|
+
// chain: shell env var (per-user, gitignored settings.local.json) wins over
|
|
26
|
+
// clawform.config.json tags_defaults (per-project, committed). Either source
|
|
27
|
+
// alone is sufficient; both unset is a hard error pointing at both options.
|
|
28
|
+
//
|
|
29
|
+
// Why env wins: a developer running locally with their own handle should not
|
|
30
|
+
// have to edit the committed project config. tags_defaults is the team default
|
|
31
|
+
// for CI / CodePipeline / any context without per-user env.
|
|
32
|
+
//
|
|
33
|
+
// Args:
|
|
34
|
+
// projectConfig — result of loadProjectConfig(). Reads .customer and
|
|
35
|
+
// .tags_defaults. Pure data, no I/O.
|
|
36
|
+
// envName — the env string ('d' | 'stg' | 'p') passed to deploy().
|
|
37
|
+
// Used as the Environment value only.
|
|
38
|
+
//
|
|
39
|
+
// Returns: { Owner, CostCenter, Environment, ManagedBy, Customer }.
|
|
40
|
+
// Callers map this into the stack-level Tags array; the key spelling there
|
|
41
|
+
// (Env vs Environment) is the caller's choice — rules/tagging.md pins it to "Env".
|
|
42
|
+
export function resolveStackTags(projectConfig, envName) {
|
|
43
|
+
const defaults = (projectConfig && projectConfig.tags_defaults) || {};
|
|
44
|
+
const envOwner = (process.env.OWNER || '').trim();
|
|
45
|
+
const envCost = (process.env.COST_CENTER || '').trim();
|
|
46
|
+
const cfgOwner = typeof defaults.Owner === 'string' ? defaults.Owner.trim() : '';
|
|
47
|
+
const cfgCost = typeof defaults.CostCenter === 'string' ? defaults.CostCenter.trim() : '';
|
|
48
|
+
|
|
49
|
+
const Owner = envOwner || cfgOwner;
|
|
50
|
+
if (!Owner) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'Owner tag is unset. Set one of:\n' +
|
|
53
|
+
' 1. OWNER env var (e.g. .claude/settings.local.json "env" block) — per-user override, wins.\n' +
|
|
54
|
+
' 2. tags_defaults.Owner in clawform.config.json — per-project team default.\n' +
|
|
55
|
+
'See docs/config-schema.md and rules/tagging.md.'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const CostCenter = envCost || cfgCost;
|
|
60
|
+
if (!CostCenter) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'CostCenter tag is unset. Set one of:\n' +
|
|
63
|
+
' 1. COST_CENTER env var (e.g. .claude/settings.local.json "env" block) — per-user override, wins.\n' +
|
|
64
|
+
' 2. tags_defaults.CostCenter in clawform.config.json — per-project team default.\n' +
|
|
65
|
+
'See docs/config-schema.md and rules/tagging.md.'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
Owner,
|
|
71
|
+
CostCenter,
|
|
72
|
+
Environment: envName,
|
|
73
|
+
ManagedBy: 'CloudFormation',
|
|
74
|
+
Customer: (projectConfig && projectConfig.customer) || '',
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/lib/constants.js
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
export const ENVS = ['d', 'stg', 'p'];
|
|
2
|
-
|
|
3
|
-
// The customer token is the widest variable slot in every generated name, and
|
|
4
|
-
// the tightest consumer is the 32-char cap AWS puts on load-balancer and
|
|
5
|
-
// target-group names. Eight characters is what that budget leaves once the
|
|
6
|
-
// longest env, the fixed infix, and a usable project shortname are accounted
|
|
7
|
-
// for — see the arithmetic in templates/alb.yaml. The archetypes'
|
|
8
|
-
// AllowedPattern and the init wizard both have to agree with this number, or
|
|
9
|
-
// the wizard writes a config that fails at deploy time.
|
|
10
|
-
export const CUSTOMER_MAX_LEN = 8;
|
|
11
|
-
export const CUSTOMER_RE = new RegExp(`^[a-z0-9]{1,${CUSTOMER_MAX_LEN}}$`);
|
|
12
|
-
|
|
13
|
-
// Catalog + inventory are generated from the consumer's own AWS account, so
|
|
14
|
-
// they belong in the consumer's project — not in the plugin checkout, where
|
|
15
|
-
// they would be shared across every project, wiped by `npm update`, and out of
|
|
16
|
-
// reach of the deploy-gate hook (which resolves them relative to the project).
|
|
17
|
-
// Dotted and single-rooted so a buyer gitignores one entry.
|
|
18
|
-
export const CATALOG_DIR_REL = '.clawform/catalog';
|
|
19
|
-
|
|
20
|
-
export const VISIBILITY_PREFIXES = ['pri', 'pub'];
|
|
21
|
-
|
|
22
|
-
// When adding: update rules/cfn-standards.md service-shortname table in the same change.
|
|
23
|
-
// Consumers can extend this list via `service_shortnames_extra` in clawform.config.json
|
|
24
|
-
// without forking the plugin.
|
|
25
|
-
export const SERVICE_SHORTNAMES = [
|
|
26
|
-
'vpc', 'snet', 'sg', 'rt', 'vpce', 'igw', 'nat', 'eip',
|
|
27
|
-
'alb', 'nlb', 'tg', 'lstn',
|
|
28
|
-
'ecsc', 'ecss', 'ecr',
|
|
29
|
-
'rds', 'aur', 'ddb', 'ecache',
|
|
30
|
-
's3', 'lmbd',
|
|
31
|
-
'sns', 'sqs', 'glue',
|
|
32
|
-
'cf', 'r53', 'acm', 'cgnp', 'wafa',
|
|
33
|
-
'sm', 'cwlg', 'cwa', 'bgt',
|
|
34
|
-
'iamr', 'kms', 'ec2',
|
|
35
|
-
'ccmt', 'cbld', 'cpl', 'cdpl',
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
// The legacy-prefix list moved out of the plugin and into the consumer's
|
|
39
|
-
// clawform.config.json (`legacy_prefixes`). See src/lib/project-config.js
|
|
40
|
-
// and docs/config-schema.md. A fresh consumer with no config has no legacy
|
|
41
|
-
// stacks, so the built-in default is an empty list — we don't impose Acme's
|
|
42
|
-
// LEGACY/OLDAPP freeze on someone else's account.
|
|
43
|
-
|
|
44
|
-
// Anchored to line-start to defeat prompt-injection via "FORCE LEGACY" inside narrative content.
|
|
45
|
-
export const LEGACY_OVERRIDE_PHRASE = /^\s*#\s*FORCE\s+LEGACY\b/m;
|
|
46
|
-
|
|
47
|
-
// Stateful CFN resource types per CLAUDE.md hard rule #4. Replace on any of these
|
|
48
|
-
// requires the data-resource gate in src/commands/deploy.js (user must retype stack name).
|
|
49
|
-
//
|
|
50
|
-
// IMPORTANT: list only types that actually hold data. The RDS/EFS/ElastiCache namespaces
|
|
51
|
-
// each contain config types (SubnetGroup, ParameterGroup, MountTarget, OptionGroup, ...)
|
|
52
|
-
// that hold no data — including them via a bare-prefix match would over-fire the safety
|
|
53
|
-
// gate on harmless config Replaces and train operators to ignore the warning. Add new
|
|
54
|
-
// stateful types here explicitly; don't reintroduce DATA_RESOURCE_PREFIXES.
|
|
55
|
-
export const DATA_RESOURCE_EXACT = [
|
|
56
|
-
// Cognito — a user pool holds every user account; a Replace repoints the
|
|
57
|
-
// export at a brand-new EMPTY pool. Treated as data per hard rule #4.
|
|
58
|
-
'AWS::Cognito::UserPool',
|
|
59
|
-
// S3 / DynamoDB / Logs / Secrets / KMS
|
|
60
|
-
'AWS::S3::Bucket',
|
|
61
|
-
'AWS::DynamoDB::Table',
|
|
62
|
-
'AWS::DynamoDB::GlobalTable',
|
|
63
|
-
'AWS::Logs::LogGroup',
|
|
64
|
-
'AWS::SecretsManager::Secret',
|
|
65
|
-
'AWS::KMS::Key',
|
|
66
|
-
// RDS — stateful only (DBInstance/Cluster + snapshots; NOT *Group, *Subscription, DBProxy)
|
|
67
|
-
'AWS::RDS::DBInstance',
|
|
68
|
-
'AWS::RDS::DBCluster',
|
|
69
|
-
'AWS::RDS::GlobalCluster',
|
|
70
|
-
'AWS::RDS::DBClusterSnapshot',
|
|
71
|
-
'AWS::RDS::DBSnapshot',
|
|
72
|
-
// EFS — stateful only (FileSystem; NOT MountTarget, AccessPoint, FileSystemPolicy)
|
|
73
|
-
'AWS::EFS::FileSystem',
|
|
74
|
-
// ElastiCache — "when persistent" per CLAUDE.md hard rule #4
|
|
75
|
-
// (NOT SubnetGroup, ParameterGroup, SecurityGroup, User, UserGroup)
|
|
76
|
-
'AWS::ElastiCache::CacheCluster',
|
|
77
|
-
'AWS::ElastiCache::ReplicationGroup',
|
|
78
|
-
'AWS::ElastiCache::GlobalReplicationGroup',
|
|
79
|
-
'AWS::ElastiCache::ServerlessCache',
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
export function isDataResource(resourceType) {
|
|
83
|
-
if (!resourceType) return false;
|
|
84
|
-
return DATA_RESOURCE_EXACT.includes(resourceType);
|
|
85
|
-
}
|
|
1
|
+
export const ENVS = ['d', 'stg', 'p'];
|
|
2
|
+
|
|
3
|
+
// The customer token is the widest variable slot in every generated name, and
|
|
4
|
+
// the tightest consumer is the 32-char cap AWS puts on load-balancer and
|
|
5
|
+
// target-group names. Eight characters is what that budget leaves once the
|
|
6
|
+
// longest env, the fixed infix, and a usable project shortname are accounted
|
|
7
|
+
// for — see the arithmetic in templates/alb.yaml. The archetypes'
|
|
8
|
+
// AllowedPattern and the init wizard both have to agree with this number, or
|
|
9
|
+
// the wizard writes a config that fails at deploy time.
|
|
10
|
+
export const CUSTOMER_MAX_LEN = 8;
|
|
11
|
+
export const CUSTOMER_RE = new RegExp(`^[a-z0-9]{1,${CUSTOMER_MAX_LEN}}$`);
|
|
12
|
+
|
|
13
|
+
// Catalog + inventory are generated from the consumer's own AWS account, so
|
|
14
|
+
// they belong in the consumer's project — not in the plugin checkout, where
|
|
15
|
+
// they would be shared across every project, wiped by `npm update`, and out of
|
|
16
|
+
// reach of the deploy-gate hook (which resolves them relative to the project).
|
|
17
|
+
// Dotted and single-rooted so a buyer gitignores one entry.
|
|
18
|
+
export const CATALOG_DIR_REL = '.clawform/catalog';
|
|
19
|
+
|
|
20
|
+
export const VISIBILITY_PREFIXES = ['pri', 'pub'];
|
|
21
|
+
|
|
22
|
+
// When adding: update rules/cfn-standards.md service-shortname table in the same change.
|
|
23
|
+
// Consumers can extend this list via `service_shortnames_extra` in clawform.config.json
|
|
24
|
+
// without forking the plugin.
|
|
25
|
+
export const SERVICE_SHORTNAMES = [
|
|
26
|
+
'vpc', 'snet', 'sg', 'rt', 'vpce', 'igw', 'nat', 'eip',
|
|
27
|
+
'alb', 'nlb', 'tg', 'lstn',
|
|
28
|
+
'ecsc', 'ecss', 'ecr',
|
|
29
|
+
'rds', 'aur', 'ddb', 'ecache',
|
|
30
|
+
's3', 'lmbd',
|
|
31
|
+
'sns', 'sqs', 'glue',
|
|
32
|
+
'cf', 'r53', 'acm', 'cgnp', 'wafa',
|
|
33
|
+
'sm', 'cwlg', 'cwa', 'bgt',
|
|
34
|
+
'iamr', 'kms', 'ec2',
|
|
35
|
+
'ccmt', 'cbld', 'cpl', 'cdpl',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// The legacy-prefix list moved out of the plugin and into the consumer's
|
|
39
|
+
// clawform.config.json (`legacy_prefixes`). See src/lib/project-config.js
|
|
40
|
+
// and docs/config-schema.md. A fresh consumer with no config has no legacy
|
|
41
|
+
// stacks, so the built-in default is an empty list — we don't impose Acme's
|
|
42
|
+
// LEGACY/OLDAPP freeze on someone else's account.
|
|
43
|
+
|
|
44
|
+
// Anchored to line-start to defeat prompt-injection via "FORCE LEGACY" inside narrative content.
|
|
45
|
+
export const LEGACY_OVERRIDE_PHRASE = /^\s*#\s*FORCE\s+LEGACY\b/m;
|
|
46
|
+
|
|
47
|
+
// Stateful CFN resource types per CLAUDE.md hard rule #4. Replace on any of these
|
|
48
|
+
// requires the data-resource gate in src/commands/deploy.js (user must retype stack name).
|
|
49
|
+
//
|
|
50
|
+
// IMPORTANT: list only types that actually hold data. The RDS/EFS/ElastiCache namespaces
|
|
51
|
+
// each contain config types (SubnetGroup, ParameterGroup, MountTarget, OptionGroup, ...)
|
|
52
|
+
// that hold no data — including them via a bare-prefix match would over-fire the safety
|
|
53
|
+
// gate on harmless config Replaces and train operators to ignore the warning. Add new
|
|
54
|
+
// stateful types here explicitly; don't reintroduce DATA_RESOURCE_PREFIXES.
|
|
55
|
+
export const DATA_RESOURCE_EXACT = [
|
|
56
|
+
// Cognito — a user pool holds every user account; a Replace repoints the
|
|
57
|
+
// export at a brand-new EMPTY pool. Treated as data per hard rule #4.
|
|
58
|
+
'AWS::Cognito::UserPool',
|
|
59
|
+
// S3 / DynamoDB / Logs / Secrets / KMS
|
|
60
|
+
'AWS::S3::Bucket',
|
|
61
|
+
'AWS::DynamoDB::Table',
|
|
62
|
+
'AWS::DynamoDB::GlobalTable',
|
|
63
|
+
'AWS::Logs::LogGroup',
|
|
64
|
+
'AWS::SecretsManager::Secret',
|
|
65
|
+
'AWS::KMS::Key',
|
|
66
|
+
// RDS — stateful only (DBInstance/Cluster + snapshots; NOT *Group, *Subscription, DBProxy)
|
|
67
|
+
'AWS::RDS::DBInstance',
|
|
68
|
+
'AWS::RDS::DBCluster',
|
|
69
|
+
'AWS::RDS::GlobalCluster',
|
|
70
|
+
'AWS::RDS::DBClusterSnapshot',
|
|
71
|
+
'AWS::RDS::DBSnapshot',
|
|
72
|
+
// EFS — stateful only (FileSystem; NOT MountTarget, AccessPoint, FileSystemPolicy)
|
|
73
|
+
'AWS::EFS::FileSystem',
|
|
74
|
+
// ElastiCache — "when persistent" per CLAUDE.md hard rule #4
|
|
75
|
+
// (NOT SubnetGroup, ParameterGroup, SecurityGroup, User, UserGroup)
|
|
76
|
+
'AWS::ElastiCache::CacheCluster',
|
|
77
|
+
'AWS::ElastiCache::ReplicationGroup',
|
|
78
|
+
'AWS::ElastiCache::GlobalReplicationGroup',
|
|
79
|
+
'AWS::ElastiCache::ServerlessCache',
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
export function isDataResource(resourceType) {
|
|
83
|
+
if (!resourceType) return false;
|
|
84
|
+
return DATA_RESOURCE_EXACT.includes(resourceType);
|
|
85
|
+
}
|
package/src/lib/defaults.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// Built-in defaults for Clawform's consumer config. Merged FIRST (lowest precedence)
|
|
2
|
-
// by src/lib/project-config.js. A fresh consumer (no clawform.config.json) sees these
|
|
3
|
-
// values; the dogfood clawform.config.json in this repo overlays the Acme-specific bits.
|
|
4
|
-
|
|
5
|
-
export const DEFAULT_ENVS = ['d', 'stg', 'p'];
|
|
6
|
-
|
|
7
|
-
export const DEFAULT_CONFIG = {
|
|
8
|
-
customer: '',
|
|
9
|
-
envs: DEFAULT_ENVS,
|
|
10
|
-
region: '',
|
|
11
|
-
accounts: {},
|
|
12
|
-
// Empty by default — we never impose Acme's legacy list on someone else's account.
|
|
13
|
-
legacy_prefixes: [],
|
|
14
|
-
service_shortnames_extra: [],
|
|
15
|
-
tags_defaults: {},
|
|
16
|
-
};
|
|
1
|
+
// Built-in defaults for Clawform's consumer config. Merged FIRST (lowest precedence)
|
|
2
|
+
// by src/lib/project-config.js. A fresh consumer (no clawform.config.json) sees these
|
|
3
|
+
// values; the dogfood clawform.config.json in this repo overlays the Acme-specific bits.
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_ENVS = ['d', 'stg', 'p'];
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_CONFIG = {
|
|
8
|
+
customer: '',
|
|
9
|
+
envs: DEFAULT_ENVS,
|
|
10
|
+
region: '',
|
|
11
|
+
accounts: {},
|
|
12
|
+
// Empty by default — we never impose Acme's legacy list on someone else's account.
|
|
13
|
+
legacy_prefixes: [],
|
|
14
|
+
service_shortnames_extra: [],
|
|
15
|
+
tags_defaults: {},
|
|
16
|
+
};
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
// Claude Code has no "workflows" plugin component type — saved workflow
|
|
5
|
-
// scripts are discovered only from a project's own .claude/workflows/ (or
|
|
6
|
-
// ~/.claude/workflows/), never from a plugin's shipped files. This copies
|
|
7
|
-
// the plugin-shipped workflows/audit-all-stacks.js into the CONSUMER
|
|
8
|
-
// project's .claude/workflows/ so `/audit-all-stacks` (or `/workflows`)
|
|
9
|
-
// actually finds it there. Idempotent: never overwrites an existing copy,
|
|
10
|
-
// so a consumer's local customization survives repeated `clawform sync` runs.
|
|
11
|
-
export function ensureWorkflowInstalled({
|
|
12
|
-
repoRoot,
|
|
13
|
-
projectDir,
|
|
14
|
-
exists = existsSync,
|
|
15
|
-
mkdir = mkdirSync,
|
|
16
|
-
copyFile = copyFileSync,
|
|
17
|
-
} = {}) {
|
|
18
|
-
const src = join(repoRoot, 'workflows', 'audit-all-stacks.js');
|
|
19
|
-
if (!exists(src)) return { installed: false, reason: 'source-missing' };
|
|
20
|
-
|
|
21
|
-
const destDir = join(projectDir, '.claude', 'workflows');
|
|
22
|
-
const dest = join(destDir, 'audit-all-stacks.js');
|
|
23
|
-
if (exists(dest)) return { installed: false, reason: 'already-present', dest };
|
|
24
|
-
|
|
25
|
-
mkdir(destDir, { recursive: true });
|
|
26
|
-
copyFile(src, dest);
|
|
27
|
-
return { installed: true, dest };
|
|
28
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Claude Code has no "workflows" plugin component type — saved workflow
|
|
5
|
+
// scripts are discovered only from a project's own .claude/workflows/ (or
|
|
6
|
+
// ~/.claude/workflows/), never from a plugin's shipped files. This copies
|
|
7
|
+
// the plugin-shipped workflows/audit-all-stacks.js into the CONSUMER
|
|
8
|
+
// project's .claude/workflows/ so `/audit-all-stacks` (or `/workflows`)
|
|
9
|
+
// actually finds it there. Idempotent: never overwrites an existing copy,
|
|
10
|
+
// so a consumer's local customization survives repeated `clawform sync` runs.
|
|
11
|
+
export function ensureWorkflowInstalled({
|
|
12
|
+
repoRoot,
|
|
13
|
+
projectDir,
|
|
14
|
+
exists = existsSync,
|
|
15
|
+
mkdir = mkdirSync,
|
|
16
|
+
copyFile = copyFileSync,
|
|
17
|
+
} = {}) {
|
|
18
|
+
const src = join(repoRoot, 'workflows', 'audit-all-stacks.js');
|
|
19
|
+
if (!exists(src)) return { installed: false, reason: 'source-missing' };
|
|
20
|
+
|
|
21
|
+
const destDir = join(projectDir, '.claude', 'workflows');
|
|
22
|
+
const dest = join(destDir, 'audit-all-stacks.js');
|
|
23
|
+
if (exists(dest)) return { installed: false, reason: 'already-present', dest };
|
|
24
|
+
|
|
25
|
+
mkdir(destDir, { recursive: true });
|
|
26
|
+
copyFile(src, dest);
|
|
27
|
+
return { installed: true, dest };
|
|
28
|
+
}
|