@thejoseki/clawform 0.0.1 → 2.2.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.
Files changed (55) hide show
  1. package/LICENSE +95 -4
  2. package/README.md +87 -10
  3. package/bin/clawform.js +151 -16
  4. package/package.json +47 -11
  5. package/src/aws/catalog.js +116 -0
  6. package/src/aws/changeset.js +231 -0
  7. package/src/aws/drift.js +59 -0
  8. package/src/aws/exports.js +213 -0
  9. package/src/aws/inventory.js +156 -0
  10. package/src/commands/activate.js +105 -0
  11. package/src/commands/ci-init.js +212 -0
  12. package/src/commands/cost-report.js +166 -0
  13. package/src/commands/deactivate.js +36 -0
  14. package/src/commands/deploy.js +413 -0
  15. package/src/commands/diff.js +39 -0
  16. package/src/commands/drift.js +35 -0
  17. package/src/commands/init-plugin.js +168 -0
  18. package/src/commands/init.js +360 -0
  19. package/src/commands/license-status.js +28 -0
  20. package/src/commands/rollback.js +126 -0
  21. package/src/commands/status.js +150 -0
  22. package/src/commands/sync.js +53 -0
  23. package/src/commands/validate.js +349 -0
  24. package/src/hooks/block-dangerous-eval.js +160 -0
  25. package/src/hooks/block-dangerous.js +62 -0
  26. package/src/hooks/cfn-lint-after-edit-eval.js +30 -0
  27. package/src/hooks/cfn-lint-after-edit.js +81 -0
  28. package/src/hooks/check-catalog-fresh-eval.js +63 -0
  29. package/src/hooks/check-catalog-fresh.js +33 -0
  30. package/src/hooks/session-context-eval.js +81 -0
  31. package/src/hooks/session-context.js +41 -0
  32. package/src/hooks/subagent-context-eval.js +44 -0
  33. package/src/hooks/subagent-context.js +48 -0
  34. package/src/lib/audit-synthesis.js +24 -0
  35. package/src/lib/aws-client.js +15 -0
  36. package/src/lib/cfn-yaml.js +92 -0
  37. package/src/lib/config.js +76 -0
  38. package/src/lib/constants.js +85 -0
  39. package/src/lib/defaults.js +16 -0
  40. package/src/lib/fingerprint.js +65 -0
  41. package/src/lib/install-workflow.js +28 -0
  42. package/src/lib/kit-source.js +81 -0
  43. package/src/lib/license-client.js +84 -0
  44. package/src/lib/license-config.js +162 -0
  45. package/src/lib/license-store.js +107 -0
  46. package/src/lib/license.js +146 -0
  47. package/src/lib/lint-overrides.js +226 -0
  48. package/src/lib/logger.js +17 -0
  49. package/src/lib/payload.js +74 -0
  50. package/src/lib/project-config.js +145 -0
  51. package/src/lib/providers/polar.js +215 -0
  52. package/src/lib/rename-compat.js +50 -0
  53. package/src/lib/session-state.js +91 -0
  54. package/src/lib/trial-claim.js +65 -0
  55. package/src/lib/watermark.js +65 -0
package/LICENSE CHANGED
@@ -1,9 +1,100 @@
1
+ Clawform Commercial License
1
2
  Copyright (c) 2026 Joseki. All rights reserved.
2
3
 
3
- This release contains no product it reserves the package name and prints a
4
- notice. You may install and run it. You may not redistribute it under this name.
4
+ This is a commercial software license, not an open-source license. By
5
+ installing, copying, or using Clawform ("the Software") you agree to these
6
+ terms. If you do not agree, do not install or use the Software.
5
7
 
6
- Clawform itself is licensed commercially, per seat. Those terms ship with the
7
- product and are not granted by this package.
8
+ 1. GRANT (PER-SEAT)
9
+
10
+ Subject to payment and to these terms, Joseki grants you a
11
+ non-exclusive, non-transferable, revocable license to install and use the
12
+ Software on a per-seat basis. One seat authorizes use by one individual
13
+ developer. A team must hold one seat for each developer who uses the
14
+ Software. Seats are counted per person, not per machine; a single developer
15
+ may install the Software on more than one machine they personally use.
16
+
17
+ 2. RESTRICTIONS
18
+
19
+ You may not:
20
+ (a) resell, sublicense, rent, lease, or otherwise redistribute the
21
+ Software or any part of it, whether modified or unmodified;
22
+ (b) share your copy, license key, or access with anyone who does not hold
23
+ their own seat;
24
+ (c) remove or alter this license, copyright notices, or attribution;
25
+ (d) represent the Software, or anything derived from it, as your own
26
+ product for distribution.
27
+
28
+ Templates the Software generates for your own infrastructure are yours to
29
+ use; this restriction governs the Software itself, not the CloudFormation
30
+ output you produce with it.
31
+
32
+ WATERMARKING. Your installed copy is watermarked with your licence identity:
33
+ the plugin content carries your masked account address and licence key
34
+ suffix, and a `.licensed-to` file records the full details of your seat.
35
+ This lets a redistributed copy be traced back to the seat it came from. The
36
+ watermark identifies the licence, and is not used to track your use of the
37
+ Software.
38
+
39
+ DEVICE FINGERPRINT. Activating any licence computes a fingerprint of the
40
+ machine (a hash of its hostname, platform, architecture, and machine
41
+ identifier) and uses it as the activation's label, which is how activation
42
+ slots are counted. For trial licences only, the fingerprint is additionally
43
+ recorded in a device register operated by Joseki, so that one machine can
44
+ use one trial. The fingerprint identifies the machine, not you, and is not
45
+ used to track your use of the Software.
46
+
47
+ 3. NO WARRANTY
48
+
49
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
50
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
51
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. Joseki does not
52
+ warrant that the Software's templates, guardrails, or recommendations are
53
+ correct, safe, secure, complete, or suitable for any particular purpose. You
54
+ are solely responsible for reviewing every change before it is deployed to
55
+ AWS.
56
+
57
+ 4. LIMITATION OF LIABILITY
58
+
59
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, JOSEKI SHALL NOT BE LIABLE FOR
60
+ ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR FOR
61
+ ANY LOSS OF PROFITS, DATA, OR GOODWILL, ARISING OUT OF OR RELATED TO THE
62
+ SOFTWARE.
63
+
64
+ FOR CLARITY, THIS EXCLUSION INCLUDES ANY AWS OR OTHER CLOUD INFRASTRUCTURE
65
+ CHARGES, COSTS, OR BILLS incurred through use of the Software — including
66
+ charges resulting from a guardrail that failed to catch a mistake, a
67
+ template that provisioned more than intended, or any resource created,
68
+ modified, or left running as a result of the Software. You are responsible
69
+ for your own cloud spend.
70
+
71
+ JOSEKI'S TOTAL AGGREGATE LIABILITY UNDER THIS LICENSE SHALL NOT EXCEED
72
+ THE AMOUNT YOU ACTUALLY PAID FOR THE SEAT GIVING RISE TO THE CLAIM.
73
+
74
+ 5. NO AFFILIATION
75
+
76
+ Clawform is an independent product. It is not affiliated with, endorsed by,
77
+ sponsored by, or otherwise associated with Amazon Web Services, Inc. or its
78
+ affiliates. "AWS", "Amazon Web Services", and "CloudFormation" are trademarks
79
+ of Amazon.com, Inc. or its affiliates, used here only to describe
80
+ interoperability.
81
+
82
+ 6. THIRD-PARTY COMPONENTS
83
+
84
+ The Software depends on third-party open-source packages, each licensed under
85
+ its own terms. Nothing in this license alters your rights or obligations
86
+ under those third-party licenses.
87
+
88
+ 7. TERMINATION
89
+
90
+ This license terminates automatically if you breach any term. On termination
91
+ you must stop using and destroy all copies of the Software. Sections 3, 4,
92
+ and 5 survive termination.
93
+
94
+ 8. GENERAL
95
+
96
+ If any provision of this license is held unenforceable, the remaining
97
+ provisions stay in effect. This license is the entire agreement between you
98
+ and Joseki regarding the Software.
8
99
 
9
100
  Contact: support@thejoseki.com
package/README.md CHANGED
@@ -1,18 +1,95 @@
1
- # clawform
1
+ # Clawform
2
2
 
3
- **Not released yet.** This package reserves the name.
3
+ **Safe CloudFormation workflows for Claude Code.**
4
4
 
5
- Clawform will be a commercial tool for running AWS CloudFormation safely from
6
- Claude Code rules the agent reads before it writes, a CLI that does every AWS
7
- call through a changeset you confirm, and a hook that vetoes dangerous commands
8
- before they run.
5
+ Claude writes CloudFormation fast and just as confidently when it's wrong.
6
+ Clawform is the layer between the agent and your AWS account.
9
7
 
10
- Installing this version gives you a stub that prints a notice. It does not talk
11
- to AWS, read credentials, or write anything to disk.
8
+ It gives Claude the conventions to read before it writes, so templates come out
9
+ named to your standard, tagged, and with retention set on every stateful
10
+ resource. Every AWS change then goes through the `clawform` CLI: create a
11
+ changeset, show you the diff, execute only after you confirm. Destructive
12
+ operations and the stacks you've marked frozen are refused.
12
13
 
13
- Availability and pricing: **support@thejoseki.com**
14
+ CloudFormation only by design. Not Terraform, not CDK, not Pulumi.
14
15
 
15
- ---
16
+ ## Quickstart (10 minutes)
17
+
18
+ ```bash
19
+ # 1. Install the CLI (adds `clawform` to your PATH)
20
+ npm i -g @thejoseki/clawform
21
+
22
+ # 2. Activate your license (the key is in your purchase email)
23
+ clawform activate <your-license-key>
24
+
25
+ # 3. Install the Claude Code plugin — the CLI unpacks it from your activated copy
26
+ clawform init plugin # --global (default) or --project
27
+ ```
28
+
29
+ Activation claims one of your seat's device slots; free one with
30
+ `clawform deactivate` when you retire a machine. **In CI, do not activate** —
31
+ set `CLAWFORM_LICENSE_KEY` in the environment instead, which validates per-run
32
+ without taking a slot.
33
+
34
+ Then, **in your own project**:
35
+
36
+ ```bash
37
+ # 4. Create your config — customer, region, accounts, legacy prefixes
38
+ clawform init # interactive wizard, writes clawform.config.json
39
+
40
+ # 5. Point Clawform at your AWS profile (per-user, gitignored)
41
+ # .claude/settings.local.json → env block → AWS_PROFILE
42
+
43
+ # 6. Cache your account's stacks and exports (run before your first deploy,
44
+ # and after every successful one)
45
+ clawform sync
46
+
47
+ # 7. Open Claude Code and scaffold your first stack
48
+ claude
49
+ > /clawform:scaffold-project # propose architecture + deploy order
50
+ > /clawform:new-stack networking d
51
+ > /clawform:validate cfn/d-acme-networking.yaml
52
+ > /clawform:deploy cfn/d-acme-networking.yaml d
53
+ ```
54
+
55
+ That's a real stack, deployed through a changeset you confirmed.
56
+
57
+ ## Configuration
58
+
59
+ Clawform reads two files. Neither hardcodes anything about your organization.
60
+
61
+ | File | Scope | Committed? | Holds |
62
+ |---|---|---|---|
63
+ | `clawform.config.json` | per-project | yes | customer, region, accounts, `legacy_prefixes` |
64
+ | `.claude/settings.local.json` | per-user | no (gitignored) | `AWS_PROFILE`, owner/cost-center overrides |
65
+
66
+ `clawform init` writes the first one. For CI or scripted bootstrap, set
67
+ `CLAWFORM_INIT_CUSTOMER=… CLAWFORM_INIT_REGION=… clawform init`.
68
+
69
+ A fresh project with an empty `legacy_prefixes` has no legacy-freeze gate — that
70
+ is correct. Clawform never imposes another project's freeze list on you.
71
+
72
+ ## Requirements
73
+
74
+ - Node.js ≥ 20 LTS
75
+ - AWS CLI v2
76
+ - Claude Code CLI
77
+ - *(recommended)* `cfn-lint` — `pip install cfn-lint`
78
+ - *(optional)* `cfn-guard` 3.x for Clawform's custom rules
79
+
80
+ ## Links
81
+
82
+ - **[clawform.thejoseki.com](https://clawform.thejoseki.com)** — the product
83
+ page: what Clawform does, per-seat pricing, and how to get a license.
84
+ - **[thejoseki.com](https://thejoseki.com)** — Joseki, the vendor behind
85
+ Clawform. Known-good moves for agentic work.
86
+
87
+ ## License
88
+
89
+ Commercial, per-seat. One seat per developer; no resale or redistribution.
90
+ Provided **as is**, with no warranty and no liability for AWS charges — you are
91
+ responsible for reviewing every change before it deploys and for your own cloud
92
+ spend. Full terms in the `LICENSE` file.
16
93
 
17
94
  Clawform is not affiliated with Amazon Web Services, Inc. "AWS" and
18
95
  "CloudFormation" are trademarks of Amazon.com, Inc. or its affiliates.
package/bin/clawform.js CHANGED
@@ -1,16 +1,151 @@
1
- #!/usr/bin/env node
2
-
3
- // Placeholder release. This package reserves the name; the tool itself is not
4
- // published yet. Every command prints the same notice rather than failing
5
- // silently or pretending to work — someone who installed this expected a CLI.
6
-
7
- const NOTICE = `
8
- Clawform is not released yet.
9
-
10
- This package reserves the name. Installing it does not give you the tool.
11
- Nothing here talks to AWS, reads your credentials, or writes to your disk.
12
-
13
- Ask about availability: support@thejoseki.com
14
- `;
15
-
16
- process.stdout.write(NOTICE);
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { readLicense } from '../src/lib/license-store.js';
4
+ import { versionWatermark } from '../src/lib/watermark.js';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { logger } from '../src/lib/logger.js';
8
+ import pkg from '../package.json' with { type: 'json' };
9
+
10
+ // Inject .claude/settings.local.json env block into process.env before any
11
+ // command runs. Claude Code applies this env to its own tool invocations,
12
+ // but when the CLI is spawned via Bash (or run directly) it must self-load.
13
+ loadSettingsEnv(process.cwd());
14
+
15
+ function loadSettingsEnv(cwd) {
16
+ const path = join(cwd, '.claude', 'settings.local.json');
17
+ if (!existsSync(path)) return;
18
+ try {
19
+ const settings = JSON.parse(readFileSync(path, 'utf8'));
20
+ for (const [k, v] of Object.entries(settings.env ?? {})) {
21
+ if (!process.env[k] && typeof v === 'string') process.env[k] = v;
22
+ }
23
+ } catch {
24
+ // Silent — loadConfig() will surface missing creds with an actionable message.
25
+ }
26
+ }
27
+
28
+ const program = new Command();
29
+
30
+ program
31
+ .name('clawform')
32
+ .description('Clawform — safe CloudFormation workflows for Claude Code')
33
+ // Version carries the ownership watermark (holder + key suffix) so a shared
34
+ // install names its source. readLicense fails open, so this never throws.
35
+ .version(pkg.version + versionWatermark(readLicense()), '-v, --version');
36
+
37
+ const lazy = (mod) => async (...args) => {
38
+ const { default: handler } = await import(`../src/commands/${mod}.js`);
39
+ return runSafe(() => handler(...args));
40
+ };
41
+
42
+ const init = program
43
+ .command('init')
44
+ .description('Scaffold a new AWS project (greenfield wizard). Non-interactive when CLAWFORM_INIT_* env vars are set.')
45
+ .option('--force', 'Overwrite an existing clawform.config.json')
46
+ .action(async (opts) => lazy('init')(opts));
47
+
48
+ init
49
+ .command('plugin')
50
+ .description('Install the Clawform Claude Code plugin (decrypts the bundled content; requires an active license once enforcement is on)')
51
+ .option('--global', 'Install for all projects (~/.claude) — default')
52
+ .option('--project', 'Install for the current project only (./.claude)')
53
+ .action(async (opts) => lazy('init-plugin')({ scope: opts.project ? 'project' : 'global' }));
54
+
55
+ program
56
+ .command('sync')
57
+ .description('Refresh outputs-catalog.md and stacks-inventory.md from live AWS')
58
+ .option('--profile <name>', 'AWS profile (overrides .claude/settings.local.json env block)')
59
+ .option('--region <name>', 'AWS region (overrides .claude/settings.local.json env block)')
60
+ .action(async (opts) => lazy('sync')(opts));
61
+
62
+ program
63
+ .command('validate <file>')
64
+ .description('Run cfn-lint, cfn-guard (rules.guard + lint-overrides), cfn-nag, and CloudFormation ValidateTemplate on a YAML template')
65
+ .option('--skip-aws', 'Skip the live ValidateTemplate call (CI / fresh box without AWS creds)')
66
+ .action(async (file, opts) => lazy('validate')({ file, ...opts }));
67
+
68
+ program
69
+ .command('deploy <file> <env>')
70
+ .description('Deploy a template to <env> via CloudFormation changeset (create → confirm → execute)')
71
+ .requiredOption('--stack-name <name>', 'CFN stack name (e.g., d-acme-clawtest-fanout)')
72
+ .requiredOption('--project <name>', 'Project shortname for Project tag (e.g., clawtest)')
73
+ .option('--params <path>', 'JSON file: [{ParameterKey, ParameterValue}, ...]')
74
+ .option('--capabilities <list>', 'Comma-separated: CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND')
75
+ .option('--auto-approve', 'Skip confirmation (refused on Replace or data-resource removal)')
76
+ .action(async (file, env, opts) => lazy('deploy')({ file, env, ...opts }));
77
+
78
+ program
79
+ .command('diff <stack>')
80
+ .description('Diff local template vs live stack')
81
+ .option('--file <path>', 'Local template file (defaults to cfn/<stack>.yaml)')
82
+ .action(async (stack, opts) => lazy('diff')({ stack, ...opts }));
83
+
84
+ program
85
+ .command('rollback <stack>')
86
+ .description('Recover a stack stuck in CREATE_FAILED / UPDATE_ROLLBACK_FAILED')
87
+ .action(async (stack, opts) => lazy('rollback')({ stack, ...opts }));
88
+
89
+ program
90
+ .command('drift <stack>')
91
+ .description('Detect configuration drift on a deployed stack')
92
+ .action(async (stack, opts) => lazy('drift')({ stack, ...opts }));
93
+
94
+ program
95
+ .command('status')
96
+ .description('Day-2 health board: per-project stack status, last deploy, last-known drift, monitoring gap')
97
+ .option('--project <name>', 'Project shortname to filter to (default: all)')
98
+ .option('--env <env>', 'Env to filter to (d | stg | p)')
99
+ .option('--profile <name>', 'AWS profile (overrides .claude/settings.local.json env block)')
100
+ .option('--region <name>', 'AWS region (overrides .claude/settings.local.json env block)')
101
+ .action(async (opts) => lazy('status')(opts));
102
+
103
+ program
104
+ .command('activate <key>')
105
+ .description('Activate a license key on this machine (one activation slot; CI should use CLAWFORM_LICENSE_KEY instead)')
106
+ .action(async (key) => lazy('activate')({ key }));
107
+
108
+ program
109
+ .command('license')
110
+ .description('Show license status on this machine. --check exits 1 when unlicensed (for scripts/skills)')
111
+ .option('--check', 'Silent success when active; exit 1 when not')
112
+ .action(async (opts) => lazy('license-status')(opts));
113
+
114
+ program
115
+ .command('deactivate')
116
+ .description('Release this machine\'s activation slot and clear the local license')
117
+ .action(async () => lazy('deactivate')({}));
118
+
119
+ program
120
+ .command('cost-report')
121
+ .description('Month-to-date AWS spend grouped by Project / Env / CostCenter tag (or service) via Cost Explorer')
122
+ .option('--month <YYYY-MM>', 'Month to report (defaults to the current month, month-to-date)')
123
+ .option('--group-by <key>', 'project | env | costcenter | service', 'project')
124
+ .option('--profile <name>', 'AWS profile (overrides .claude/settings.local.json env block)')
125
+ .action(async (opts) => lazy('cost-report')(opts));
126
+
127
+ const ci = program
128
+ .command('ci')
129
+ .description('CI/CD scaffolding for the GitLab On-Prem → CodePipeline model');
130
+
131
+ ci
132
+ .command('init')
133
+ .description('Generate CI config: cfn-lint on every MR/PR, upload source.zip to the pipeline bucket on the protected branch')
134
+ .option('--provider <name>', 'gitlab | github', 'gitlab')
135
+ .option('--source-bucket <name>', 'Pipeline SourceBucket name (the cicd-pipeline stack\'s SourceBucketName output)')
136
+ .option('--cfn-dir <dir>', 'Directory holding cfn templates', 'cfn')
137
+ .option('--branch <name>', 'Protected branch that triggers the deploy upload', 'main')
138
+ .option('--force', 'Overwrite an existing CI config')
139
+ .action(async (opts) => lazy('ci-init')({ provider: opts.provider, sourceBucket: opts.sourceBucket, cfnDir: opts.cfnDir, branch: opts.branch, force: opts.force }));
140
+
141
+ async function runSafe(fn) {
142
+ try {
143
+ await fn();
144
+ } catch (err) {
145
+ logger.error(err.message || String(err));
146
+ if (process.env.CLAWFORM_DEBUG) console.error(err.stack);
147
+ process.exit(1);
148
+ }
149
+ }
150
+
151
+ program.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,27 +1,63 @@
1
1
  {
2
2
  "name": "@thejoseki/clawform",
3
- "version": "0.0.1",
3
+ "version": "2.2.0",
4
+ "description": "Clawform — safe CloudFormation workflows for Claude Code",
5
+ "homepage": "https://clawform.thejoseki.com",
6
+ "type": "module",
7
+ "license": "SEE LICENSE IN LICENSE",
8
+ "author": "Joseki <support@thejoseki.com>",
4
9
  "publishConfig": {
5
10
  "access": "public"
6
11
  },
7
- "description": "Clawform — safe CloudFormation workflows for Claude Code. Name reserved; the tool is not released yet.",
8
- "license": "SEE LICENSE IN LICENSE",
9
- "author": "Joseki <support@thejoseki.com>",
10
- "type": "module",
11
12
  "bin": {
12
13
  "clawform": "bin/clawform.js"
13
14
  },
14
- "files": [
15
- "bin/",
16
- "LICENSE"
17
- ],
18
15
  "engines": {
19
16
  "node": ">=20.10"
20
17
  },
18
+ "scripts": {
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "test:eval": "vitest run tests/eval",
22
+ "lint": "node scripts/lint-content.js",
23
+ "package": "node scripts/package-plugin.js && node scripts/pack-payload.js",
24
+ "kit:build": "node scripts/package-plugin.js && node scripts/build-kit-worker.js",
25
+ "kit:deploy": "npm run kit:build && npm --prefix services/kit-worker run deploy",
26
+ "prepublishOnly": "node scripts/check-release-mode.js && npm run package",
27
+ "clean-room": "node scripts/clean-room.js",
28
+ "e2e": "node scripts/e2e-licensed-install.js",
29
+ "e2e:trial": "node scripts/e2e-trial.js"
30
+ },
31
+ "dependencies": {
32
+ "@aws-sdk/client-cloudformation": "^3.700.0",
33
+ "@aws-sdk/client-cost-explorer": "^3.1066.0",
34
+ "@aws-sdk/client-sts": "^3.700.0",
35
+ "chalk": "^5.3.0",
36
+ "commander": "^12.1.0",
37
+ "diff": "^7.0.0",
38
+ "execa": "^9.5.0",
39
+ "inquirer": "^12.0.0",
40
+ "js-yaml": "^4.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "vitest": "^2.1.9"
44
+ },
45
+ "files": [
46
+ "bin/",
47
+ "src/"
48
+ ],
21
49
  "keywords": [
50
+ "claude-code",
51
+ "claude",
22
52
  "cloudformation",
23
53
  "aws",
24
- "claude-code",
25
- "infrastructure-as-code"
54
+ "iac",
55
+ "infrastructure-as-code",
56
+ "aws-cdk-alternative",
57
+ "changeset",
58
+ "cfn-lint",
59
+ "devops",
60
+ "guardrails",
61
+ "ai-agent"
26
62
  ]
27
63
  }
@@ -0,0 +1,116 @@
1
+ import { paginateListExports } from '@aws-sdk/client-cloudformation';
2
+ import { ENVS, VISIBILITY_PREFIXES, SERVICE_SHORTNAMES } from '../lib/constants.js';
3
+
4
+ const ENV_PREFIX_RE = new RegExp(`^(${ENVS.join('|')})-`);
5
+ const VISIBILITY = new Set(VISIBILITY_PREFIXES);
6
+ const SERVICES = new Set(SERVICE_SHORTNAMES);
7
+
8
+ export function parseExport(name) {
9
+ const m = ENV_PREFIX_RE.exec(name);
10
+ if (!m) return { env: null, project: null };
11
+ const tokens = name.split('-');
12
+ if (tokens.length < 3) return { env: null, project: null };
13
+
14
+ let projectIdx;
15
+ if (VISIBILITY.has(tokens[2])) projectIdx = 4; // <env>-<cust>-<pri|pub>-<svc>-<project>
16
+ else if (SERVICES.has(tokens[2])) projectIdx = 3; // <env>-<cust>-<svc>-<project>
17
+ else projectIdx = 2; // <env>-<cust>-<project>-<feature>
18
+
19
+ const project = tokens[projectIdx];
20
+ if (!project) return { env: null, project: null };
21
+ return { env: m[1], project };
22
+ }
23
+
24
+ export async function fetchExports(client) {
25
+ const exports = [];
26
+ for await (const page of paginateListExports({ client }, {})) {
27
+ for (const ex of page.Exports ?? []) {
28
+ exports.push({
29
+ name: ex.Name,
30
+ value: ex.Value,
31
+ exportingStackId: ex.ExportingStackId,
32
+ });
33
+ }
34
+ }
35
+ return exports;
36
+ }
37
+
38
+ export function groupExports(exports) {
39
+ const byProject = new Map();
40
+ const uncategorized = [];
41
+ for (const ex of exports) {
42
+ const { env, project } = parseExport(ex.name);
43
+ if (!env || !project) {
44
+ uncategorized.push(ex);
45
+ continue;
46
+ }
47
+ if (!byProject.has(project)) byProject.set(project, new Map());
48
+ const byEnv = byProject.get(project);
49
+ if (!byEnv.has(env)) byEnv.set(env, []);
50
+ byEnv.get(env).push(ex);
51
+ }
52
+ return { byProject, uncategorized };
53
+ }
54
+
55
+ function stackNameOf(exportingStackId) {
56
+ if (!exportingStackId) return '(unknown)';
57
+ const parts = exportingStackId.split('/');
58
+ return parts[1] ?? '(unknown)';
59
+ }
60
+
61
+ function truncate(s, n) {
62
+ if (!s) return '';
63
+ return s.length > n ? s.slice(0, n - 1) + '…' : s;
64
+ }
65
+
66
+ function escapePipe(s) {
67
+ return String(s ?? '').replace(/\|/g, '\\|');
68
+ }
69
+
70
+ function renderTable(items) {
71
+ const lines = [];
72
+ lines.push('| Export | Value preview | Source stack |');
73
+ lines.push('|---|---|---|');
74
+ const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
75
+ for (const it of sorted) {
76
+ lines.push(
77
+ `| \`${escapePipe(it.name)}\` | \`${escapePipe(truncate(it.value, 40))}\` | \`${escapePipe(stackNameOf(it.exportingStackId))}\` |`,
78
+ );
79
+ }
80
+ return lines;
81
+ }
82
+
83
+ export function renderCatalog({ byProject, uncategorized }, meta) {
84
+ const lines = [
85
+ '# CloudFormation Outputs Catalog',
86
+ '',
87
+ `> Generated by \`clawform sync\` at ${meta.generatedAt}`,
88
+ `> Account: ${meta.account} · Region: ${meta.region} · ${meta.total} exports`,
89
+ '',
90
+ ];
91
+
92
+ const projects = [...byProject.keys()].sort();
93
+ for (const project of projects) {
94
+ lines.push(`## Project: ${project}`, '');
95
+ const envs = [...byProject.get(project).keys()].sort();
96
+ for (const env of envs) {
97
+ lines.push(`### Env: ${env}`, '');
98
+ lines.push(...renderTable(byProject.get(project).get(env)));
99
+ lines.push('');
100
+ }
101
+ }
102
+
103
+ if (uncategorized.length > 0) {
104
+ lines.push(
105
+ `## Uncategorized (non-conforming naming, ${uncategorized.length})`,
106
+ '',
107
+ '> Exports that don\'t match `<env>-<customer>-[<pri|pub>-]<service>-<project>-...`.',
108
+ '> Usually legacy stacks (OLDNET-*, OLDAPP-*) or pre-convention exports (e.g. `d-vpc-acme-billing-id`).',
109
+ '',
110
+ );
111
+ lines.push(...renderTable(uncategorized));
112
+ lines.push('');
113
+ }
114
+
115
+ return lines.join('\n');
116
+ }