@thejoseki/clawform 2.3.1 → 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 +137 -105
  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 +163 -150
  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,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
+ }
@@ -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
+ }
@@ -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
+ }