@thejoseki/clawform 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +141 -120
  2. package/bin/clawform.js +161 -151
  3. package/package.json +65 -65
  4. package/src/aws/catalog.js +116 -116
  5. package/src/aws/changeset.js +231 -231
  6. package/src/aws/drift.js +59 -59
  7. package/src/aws/exports.js +213 -213
  8. package/src/aws/inventory.js +156 -156
  9. package/src/commands/activate.js +34 -2
  10. package/src/commands/ci-init.js +393 -212
  11. package/src/commands/cost-report.js +163 -163
  12. package/src/commands/deactivate.js +9 -2
  13. package/src/commands/deploy.js +413 -413
  14. package/src/commands/diff.js +39 -39
  15. package/src/commands/drift.js +35 -35
  16. package/src/commands/init-plugin.js +6 -6
  17. package/src/commands/init.js +360 -360
  18. package/src/commands/rollback.js +126 -126
  19. package/src/commands/status.js +147 -147
  20. package/src/commands/sync.js +50 -50
  21. package/src/commands/update.js +24 -0
  22. package/src/commands/validate.js +349 -349
  23. package/src/hooks/block-dangerous.js +62 -62
  24. package/src/hooks/cfn-lint-after-edit-eval.js +30 -30
  25. package/src/hooks/cfn-lint-after-edit.js +81 -81
  26. package/src/hooks/check-catalog-fresh-eval.js +63 -63
  27. package/src/hooks/check-catalog-fresh.js +33 -33
  28. package/src/hooks/session-context-eval.js +81 -81
  29. package/src/hooks/session-context.js +41 -41
  30. package/src/hooks/subagent-context-eval.js +44 -44
  31. package/src/hooks/subagent-context.js +48 -48
  32. package/src/lib/audit-synthesis.js +24 -24
  33. package/src/lib/aws-client.js +15 -15
  34. package/src/lib/cfn-yaml.js +92 -92
  35. package/src/lib/config.js +76 -76
  36. package/src/lib/constants.js +85 -85
  37. package/src/lib/defaults.js +16 -16
  38. package/src/lib/install-workflow.js +28 -28
  39. package/src/lib/kit-source.js +43 -5
  40. package/src/lib/license-client.js +84 -84
  41. package/src/lib/license-config.js +162 -162
  42. package/src/lib/license-store.js +43 -8
  43. package/src/lib/license.js +15 -2
  44. package/src/lib/lint-overrides.js +226 -226
  45. package/src/lib/logger.js +16 -16
  46. package/src/lib/project-config.js +145 -145
  47. package/src/lib/providers/polar.js +215 -215
  48. package/src/lib/session-state.js +91 -91
package/README.md CHANGED
@@ -1,120 +1,141 @@
1
- # Clawform
2
-
3
- **Safe CloudFormation workflows for Claude Code.**
4
-
5
- ![The Clawform hook vetoing an aws cloudformation delete-stack, then the safe changeset path](https://clawform.thejoseki.com/assets/clawform-demo.gif)
6
-
7
- Claude writes CloudFormation fast — and just as confidently when it's wrong.
8
- Clawform is the layer between the agent and your AWS account.
9
-
10
- It gives Claude the conventions to read before it writes, so templates come out
11
- named to your standard, tagged, and with retention set on every stateful
12
- resource. Every AWS change then goes through the `clawform` CLI: create a
13
- changeset, show you the diff, execute only after you confirm. Destructive
14
- operations and the stacks you've marked frozen are refused.
15
-
16
- CloudFormation only — by design. Not Terraform, not CDK, not Pulumi.
17
-
18
- **Try it free for 14 days** — the full kit, email-only checkout, no card:
19
- <https://clawform.thejoseki.com>. When the trial lapses, `deploy` and
20
- `rollback` keep working; an expired licence never strands a stack.
21
-
22
- ## Quickstart (10 minutes)
23
-
24
- ```bash
25
- # 1. Install the CLI (adds `clawform` to your PATH)
26
- npm i -g @thejoseki/clawform
27
-
28
- # 2. Activate your key — free 14-day trial or purchase, both from the site
29
- clawform activate <your-license-key>
30
-
31
- # 3. Install the Claude Code plugin — the CLI unpacks it from your activated copy
32
- clawform init plugin # --global (default) or --project
33
- ```
34
-
35
- Activation claims one of your seat's device slots; free one with
36
- `clawform deactivate` when you retire a machine. **In CI, do not activate** —
37
- set `CLAWFORM_LICENSE_KEY` in the environment instead, which validates per-run
38
- without taking a slot.
39
-
40
- Then, **in your own project**:
41
-
42
- ```bash
43
- # 4. Create your config — customer, region, accounts, legacy prefixes
44
- clawform init # interactive wizard, writes clawform.config.json
45
-
46
- # 5. Point Clawform at your AWS profile (per-user, gitignored)
47
- # .claude/settings.local.json → env block → AWS_PROFILE
48
-
49
- # 6. Cache your account's stacks and exports (run before your first deploy,
50
- # and after every successful one)
51
- clawform sync
52
-
53
- # 7. Open Claude Code and scaffold your first stack
54
- claude
55
- > /clawform:scaffold-project # propose architecture + deploy order
56
- > /clawform:new-stack networking d
57
- > /clawform:validate cfn/d-acme-networking.yaml
58
- > /clawform:deploy cfn/d-acme-networking.yaml d
59
- ```
60
-
61
- That's a real stack, deployed through a changeset you confirmed.
62
-
63
- ### Don't know which services you need?
64
-
65
- `/clawform:scaffold-project` detects that case and asks a different set of questions —
66
- about consequences rather than services. *"If this were down for two hours on a Sunday
67
- night, would anyone lose money?"* rather than *"Fargate or Lambda?"* Every question has an
68
- **"I'm not sure"** option that states the default it applies and what that default costs.
69
-
70
- Your monthly spending ceiling is the first question, and it eliminates candidate
71
- architectures **before** any design work so you never spend half an hour agreeing to
72
- something you cannot pay for. Nothing is priced from memory: figures come from the AWS
73
- Pricing API, and if one cannot be fetched you are told so instead of being given a guess.
74
-
75
- The interview runs in at most three rounds and its answers are written to
76
- `docs/scaffold/*.json`, so an interrupted session resumes where it stopped rather than
77
- starting over. **Those files are committed** that is what makes the design reviewable in
78
- a pull request and `requirements.json` contains your spending ceiling and a
79
- plain-language description of what you are building. On a public repository, decide
80
- whether you want that before you push.
81
-
82
- ## Configuration
83
-
84
- Clawform reads two files. Neither hardcodes anything about your organization.
85
-
86
- | File | Scope | Committed? | Holds |
87
- |---|---|---|---|
88
- | `clawform.config.json` | per-project | yes | customer, region, accounts, `legacy_prefixes` |
89
- | `.claude/settings.local.json` | per-user | no (gitignored) | `AWS_PROFILE`, owner/cost-center overrides |
90
-
91
- `clawform init` writes the first one. For CI or scripted bootstrap, set
92
- `CLAWFORM_INIT_CUSTOMER=… CLAWFORM_INIT_REGION=… clawform init`.
93
-
94
- A fresh project with an empty `legacy_prefixes` has no legacy-freeze gate — that
95
- is correct. Clawform never imposes another project's freeze list on you.
96
-
97
- ## Requirements
98
-
99
- - Node.js 20 LTS
100
- - AWS CLI v2
101
- - Claude Code CLI
102
- - *(recommended)* `cfn-lint` — `pip install cfn-lint`
103
- - *(optional)* `cfn-guard` 3.x for Clawform's custom rules
104
-
105
- ## Links
106
-
107
- - **[clawform.thejoseki.com](https://clawform.thejoseki.com)** — the product
108
- page: what Clawform does, per-seat pricing, and how to get a license.
109
- - **[thejoseki.com](https://thejoseki.com)** Joseki, the vendor behind
110
- Clawform. Known-good moves for agentic work.
111
-
112
- ## License
113
-
114
- Commercial, per-seat. One seat per developer; no resale or redistribution.
115
- Provided **as is**, with no warranty and no liability for AWS charges you are
116
- responsible for reviewing every change before it deploys and for your own cloud
117
- spend. Full terms in the `LICENSE` file.
118
-
119
- Clawform is not affiliated with Amazon Web Services, Inc. "AWS" and
120
- "CloudFormation" are trademarks of Amazon.com, Inc. or its affiliates.
1
+ # Clawform
2
+
3
+ **Safe CloudFormation workflows for Claude Code.**
4
+
5
+ ![The Clawform hook vetoing an aws cloudformation delete-stack, then the safe changeset path](https://clawform.thejoseki.com/assets/clawform-demo.gif)
6
+
7
+ Claude writes CloudFormation fast — and just as confidently when it's wrong.
8
+ Clawform is the layer between the agent and your AWS account.
9
+
10
+ It gives Claude the conventions to read before it writes, so templates come out
11
+ named to your standard, tagged, and with retention set on every stateful
12
+ resource. Every AWS change then goes through the `clawform` CLI: create a
13
+ changeset, show you the diff, execute only after you confirm. Destructive
14
+ operations and the stacks you've marked frozen are refused.
15
+
16
+ CloudFormation only — by design. Not Terraform, not CDK, not Pulumi.
17
+
18
+ **Try it free for 14 days** — the full kit, email-only checkout, no card:
19
+ <https://clawform.thejoseki.com/?utm_source=npm>. When the trial lapses, `deploy` and
20
+ `rollback` keep working; an expired licence never strands a stack.
21
+
22
+ ## Quickstart (10 minutes)
23
+
24
+ ```bash
25
+ # 1. Install the CLI (adds `clawform` to your PATH)
26
+ npm i -g @thejoseki/clawform
27
+
28
+ # 2. Activate your key — free 14-day trial or purchase, both from the site
29
+ clawform activate <your-license-key>
30
+
31
+ # 3. Install the Claude Code plugin — the CLI unpacks it from your activated copy
32
+ clawform init plugin # --global (default) or --project
33
+ ```
34
+
35
+ Activation claims one of your seat's device slots; free one with
36
+ `clawform deactivate` when you retire a machine (re-activating the same machine
37
+ re-adopts its existing slot it does not take a second). **In CI, do not
38
+ activate** set `CLAWFORM_LICENSE_KEY` in the environment instead, which
39
+ validates per-run without taking a slot.
40
+
41
+ Upgrading later? After `npm i -g @thejoseki/clawform@latest`, run
42
+ `clawform update` to pull the newest skills (it force-refreshes the plugin kit),
43
+ then restart Claude Code.
44
+
45
+ Then, **in your own project**:
46
+
47
+ ```bash
48
+ # 4. Create your config — customer, region, accounts, legacy prefixes
49
+ clawform init # interactive wizard, writes clawform.config.json
50
+
51
+ # 5. Point Clawform at your AWS profile (per-user, gitignored)
52
+ # .claude/settings.local.json → env block → AWS_PROFILE
53
+
54
+ # 6. Cache your account's stacks and exports (run before your first deploy,
55
+ # and after every successful one)
56
+ clawform sync
57
+
58
+ # 7. Open Claude Code and scaffold your first stack
59
+ claude
60
+ > /clawform:scaffold-project # propose architecture + deploy order
61
+ > /clawform:new-stack networking d
62
+ > /clawform:validate cfn/d-acme-networking.yaml
63
+ > /clawform:deploy cfn/d-acme-networking.yaml d
64
+ ```
65
+
66
+ That's a real stack, deployed through a changeset you confirmed.
67
+
68
+ ### Don't know which services you need?
69
+
70
+ `/clawform:scaffold-project` detects that case and asks a different set of questions
71
+ about consequences rather than services. *"If this were down for two hours on a Sunday
72
+ night, would anyone lose money?"* rather than *"Fargate or Lambda?"* Every question has an
73
+ **"I'm not sure"** option that states the default it applies and what that default costs.
74
+
75
+ Your monthly spending ceiling is the first question, and it eliminates candidate
76
+ architectures **before** any design work — so you never spend half an hour agreeing to
77
+ something you cannot pay for. Nothing is priced from memory: figures come from the AWS
78
+ Pricing API, and if one cannot be fetched you are told so instead of being given a guess.
79
+
80
+ The interview runs in at most three rounds and its answers are written to
81
+ `docs/scaffold/*.json`, so an interrupted session resumes where it stopped rather than
82
+ starting over. **Those files are committed** — that is what makes the design reviewable in
83
+ a pull request — and `requirements.json` contains your spending ceiling and a
84
+ plain-language description of what you are building. On a public repository, decide
85
+ whether you want that before you push.
86
+
87
+ ## Configuration
88
+
89
+ Clawform reads two files. Neither hardcodes anything about your organization.
90
+
91
+ | File | Scope | Committed? | Holds |
92
+ |---|---|---|---|
93
+ | `clawform.config.json` | per-project | yes | customer, region, accounts, `legacy_prefixes` |
94
+ | `.claude/settings.local.json` | per-user | no (gitignored) | `AWS_PROFILE`, owner/cost-center overrides |
95
+
96
+ `clawform init` writes the first one. For CI or scripted bootstrap, set
97
+ `CLAWFORM_INIT_CUSTOMER=… CLAWFORM_INIT_REGION=… clawform init`.
98
+
99
+ A fresh project with an empty `legacy_prefixes` has no legacy-freeze gate that
100
+ is correct. Clawform never imposes another project's freeze list on you.
101
+
102
+ ## CI/CD
103
+
104
+ `clawform ci init` generates a GitHub Actions workflow or a GitLab CI config that
105
+ lints your templates and, on the protected branch, hands a source bundle to the
106
+ `cicd-pipeline` archetype's CodePipeline (validate → manual approval → execute).
107
+
108
+ - `--provider gitlab | github` your Git host (SaaS or self-managed).
109
+ - `--auth keys | oidc` — `keys` (default) stores IAM access keys as CI secrets;
110
+ `--auth oidc` is keyless the job assumes an IAM role through the host's OIDC
111
+ issuer (GitHub or GitLab SaaS) and uses short-lived credentials, so there are
112
+ no AWS keys to store. OIDC needs a `--role-arn` and a one-time AWS-side trust
113
+ policy you own; a self-managed GitLab uses `--auth keys`.
114
+
115
+ Full trust-policy setup and the keys→OIDC cutover are in the runbook
116
+ (`docs/runbook.md`, bundled with the plugin).
117
+
118
+ ## Requirements
119
+
120
+ - Node.js 20 LTS
121
+ - AWS CLI v2
122
+ - Claude Code CLI
123
+ - *(recommended)* `cfn-lint` — `pip install cfn-lint`
124
+ - *(optional)* `cfn-guard` 3.x for Clawform's custom rules
125
+
126
+ ## Links
127
+
128
+ - **[clawform.thejoseki.com](https://clawform.thejoseki.com)** — the product
129
+ page: what Clawform does, per-seat pricing, and how to get a license.
130
+ - **[thejoseki.com](https://thejoseki.com)** — Joseki, the vendor behind
131
+ Clawform. Known-good moves for agentic work.
132
+
133
+ ## License
134
+
135
+ Commercial, per-seat. One seat per developer; no resale or redistribution.
136
+ Provided **as is**, with no warranty and no liability for AWS charges — you are
137
+ responsible for reviewing every change before it deploys and for your own cloud
138
+ spend. Full terms in the `LICENSE` file.
139
+
140
+ Clawform is not affiliated with Amazon Web Services, Inc. "AWS" and
141
+ "CloudFormation" are trademarks of Amazon.com, Inc. or its affiliates.
package/bin/clawform.js CHANGED
@@ -1,151 +1,161 @@
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);
1
+ #!/usr/bin/env node
2
+ import { Command, Option } 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
+ .option('--refresh', 'Re-fetch the plugin kit from the service, ignoring the local cache (gets the newest skills)')
54
+ .action(async (opts) => lazy('init-plugin')({ scope: opts.project ? 'project' : 'global', refresh: Boolean(opts.refresh) }));
55
+
56
+ program
57
+ .command('update')
58
+ .description('Refresh the installed plugin to the newest skills/commands (force re-fetches the kit) and remind you to update the CLI')
59
+ .option('--project', 'Refresh the project-scoped install (./.claude) instead of global')
60
+ .action(async (opts) => lazy('update')({ scope: opts.project ? 'project' : 'global' }));
61
+
62
+ program
63
+ .command('sync')
64
+ .description('Refresh outputs-catalog.md and stacks-inventory.md from live AWS')
65
+ .option('--profile <name>', 'AWS profile (overrides .claude/settings.local.json env block)')
66
+ .option('--region <name>', 'AWS region (overrides .claude/settings.local.json env block)')
67
+ .action(async (opts) => lazy('sync')(opts));
68
+
69
+ program
70
+ .command('validate <file>')
71
+ .description('Run cfn-lint, cfn-guard (rules.guard + lint-overrides), cfn-nag, and CloudFormation ValidateTemplate on a YAML template')
72
+ .option('--skip-aws', 'Skip the live ValidateTemplate call (CI / fresh box without AWS creds)')
73
+ .action(async (file, opts) => lazy('validate')({ file, ...opts }));
74
+
75
+ program
76
+ .command('deploy <file> <env>')
77
+ .description('Deploy a template to <env> via CloudFormation changeset (create → confirm → execute)')
78
+ .requiredOption('--stack-name <name>', 'CFN stack name (e.g., d-acme-clawtest-fanout)')
79
+ .requiredOption('--project <name>', 'Project shortname for Project tag (e.g., clawtest)')
80
+ .option('--params <path>', 'JSON file: [{ParameterKey, ParameterValue}, ...]')
81
+ .option('--capabilities <list>', 'Comma-separated: CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND')
82
+ .option('--auto-approve', 'Skip confirmation (refused on Replace or data-resource removal)')
83
+ .action(async (file, env, opts) => lazy('deploy')({ file, env, ...opts }));
84
+
85
+ program
86
+ .command('diff <stack>')
87
+ .description('Diff local template vs live stack')
88
+ .option('--file <path>', 'Local template file (defaults to cfn/<stack>.yaml)')
89
+ .action(async (stack, opts) => lazy('diff')({ stack, ...opts }));
90
+
91
+ program
92
+ .command('rollback <stack>')
93
+ .description('Recover a stack stuck in CREATE_FAILED / UPDATE_ROLLBACK_FAILED')
94
+ .action(async (stack, opts) => lazy('rollback')({ stack, ...opts }));
95
+
96
+ program
97
+ .command('drift <stack>')
98
+ .description('Detect configuration drift on a deployed stack')
99
+ .action(async (stack, opts) => lazy('drift')({ stack, ...opts }));
100
+
101
+ program
102
+ .command('status')
103
+ .description('Day-2 health board: per-project stack status, last deploy, last-known drift, monitoring gap')
104
+ .option('--project <name>', 'Project shortname to filter to (default: all)')
105
+ .option('--env <env>', 'Env to filter to (d | stg | p)')
106
+ .option('--profile <name>', 'AWS profile (overrides .claude/settings.local.json env block)')
107
+ .option('--region <name>', 'AWS region (overrides .claude/settings.local.json env block)')
108
+ .action(async (opts) => lazy('status')(opts));
109
+
110
+ program
111
+ .command('activate <key>')
112
+ .description('Activate a license key on this machine (one activation slot; CI should use CLAWFORM_LICENSE_KEY instead)')
113
+ .action(async (key) => lazy('activate')({ key }));
114
+
115
+ program
116
+ .command('license')
117
+ .description('Show license status on this machine. --check exits 1 when unlicensed (for scripts/skills)')
118
+ .option('--check', 'Silent success when active; exit 1 when not')
119
+ .action(async (opts) => lazy('license-status')(opts));
120
+
121
+ program
122
+ .command('deactivate')
123
+ .description('Release this machine\'s activation slot and clear the local license')
124
+ .action(async () => lazy('deactivate')({}));
125
+
126
+ program
127
+ .command('cost-report')
128
+ .description('Month-to-date AWS spend grouped by Project / Env / CostCenter tag (or service) via Cost Explorer')
129
+ .option('--month <YYYY-MM>', 'Month to report (defaults to the current month, month-to-date)')
130
+ .option('--group-by <key>', 'project | env | costcenter | service', 'project')
131
+ .option('--profile <name>', 'AWS profile (overrides .claude/settings.local.json env block)')
132
+ .action(async (opts) => lazy('cost-report')(opts));
133
+
134
+ const ci = program
135
+ .command('ci')
136
+ .description('CI/CD scaffolding: bundle the repo to the pipeline SourceBucket on the protected branch (GitHub or GitLab, SaaS or self-managed)');
137
+
138
+ ci
139
+ .command('init')
140
+ .description('Generate CI config (GitHub Actions or GitLab CI): cfn-lint on every MR/PR, then upload source.zip to the pipeline bucket on the protected branch. Creds via stored IAM keys or keyless OIDC (--auth)')
141
+ .addOption(new Option('--provider <name>', 'Git host').choices(['gitlab', 'github']).default('gitlab'))
142
+ .addOption(new Option('--auth <mode>', 'keys (stored IAM keys, default) | oidc (keyless, github.com/gitlab.com)').choices(['keys', 'oidc']).default('keys'))
143
+ .option('--role-arn <arn>', 'IAM role to assume via OIDC (required with --auth oidc)')
144
+ .option('--region <name>', 'AWS region for the upload (--auth oidc; defaults to clawform.config.json region)')
145
+ .option('--source-bucket <name>', 'Pipeline SourceBucket name (the cicd-pipeline stack\'s SourceBucketName output)')
146
+ .option('--cfn-dir <dir>', 'Directory holding cfn templates', 'cfn')
147
+ .option('--branch <name>', 'Protected branch that triggers the deploy upload', 'main')
148
+ .option('--force', 'Overwrite an existing CI config')
149
+ .action(async (opts) => lazy('ci-init')({ provider: opts.provider, auth: opts.auth, roleArn: opts.roleArn, region: opts.region, sourceBucket: opts.sourceBucket, cfnDir: opts.cfnDir, branch: opts.branch, force: opts.force }));
150
+
151
+ async function runSafe(fn) {
152
+ try {
153
+ await fn();
154
+ } catch (err) {
155
+ logger.error(err.message || String(err));
156
+ if (process.env.CLAWFORM_DEBUG) console.error(err.stack);
157
+ process.exit(1);
158
+ }
159
+ }
160
+
161
+ program.parseAsync(process.argv);