@thejoseki/clawform 0.0.1 → 2.2.1
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/LICENSE +95 -4
- package/README.md +93 -10
- package/bin/clawform.js +151 -16
- package/package.json +47 -11
- package/src/aws/catalog.js +116 -0
- package/src/aws/changeset.js +231 -0
- package/src/aws/drift.js +59 -0
- package/src/aws/exports.js +213 -0
- package/src/aws/inventory.js +156 -0
- package/src/commands/activate.js +105 -0
- package/src/commands/ci-init.js +212 -0
- package/src/commands/cost-report.js +166 -0
- package/src/commands/deactivate.js +36 -0
- package/src/commands/deploy.js +413 -0
- package/src/commands/diff.js +39 -0
- package/src/commands/drift.js +35 -0
- package/src/commands/init-plugin.js +168 -0
- package/src/commands/init.js +360 -0
- package/src/commands/license-status.js +28 -0
- package/src/commands/rollback.js +126 -0
- package/src/commands/status.js +150 -0
- package/src/commands/sync.js +53 -0
- package/src/commands/validate.js +349 -0
- package/src/hooks/block-dangerous-eval.js +160 -0
- package/src/hooks/block-dangerous.js +62 -0
- package/src/hooks/cfn-lint-after-edit-eval.js +30 -0
- package/src/hooks/cfn-lint-after-edit.js +81 -0
- package/src/hooks/check-catalog-fresh-eval.js +63 -0
- package/src/hooks/check-catalog-fresh.js +33 -0
- package/src/hooks/session-context-eval.js +81 -0
- package/src/hooks/session-context.js +41 -0
- package/src/hooks/subagent-context-eval.js +44 -0
- package/src/hooks/subagent-context.js +48 -0
- package/src/lib/audit-synthesis.js +24 -0
- package/src/lib/aws-client.js +15 -0
- package/src/lib/cfn-yaml.js +92 -0
- package/src/lib/config.js +76 -0
- package/src/lib/constants.js +85 -0
- package/src/lib/defaults.js +16 -0
- package/src/lib/fingerprint.js +65 -0
- package/src/lib/install-workflow.js +28 -0
- package/src/lib/kit-source.js +81 -0
- package/src/lib/license-client.js +84 -0
- package/src/lib/license-config.js +162 -0
- package/src/lib/license-store.js +107 -0
- package/src/lib/license.js +150 -0
- package/src/lib/lint-overrides.js +226 -0
- package/src/lib/logger.js +17 -0
- package/src/lib/payload.js +74 -0
- package/src/lib/project-config.js +145 -0
- package/src/lib/providers/polar.js +215 -0
- package/src/lib/rename-compat.js +50 -0
- package/src/lib/session-state.js +91 -0
- package/src/lib/trial-claim.js +65 -0
- 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
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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,101 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Clawform
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Safe CloudFormation workflows for Claude Code.**
|
|
4
4
|
|
|
5
|
-
Clawform
|
|
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
|
+

|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
12
9
|
|
|
13
|
-
|
|
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.
|
|
14
15
|
|
|
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
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
Clawform reads two files. Neither hardcodes anything about your organization.
|
|
66
|
+
|
|
67
|
+
| File | Scope | Committed? | Holds |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| `clawform.config.json` | per-project | yes | customer, region, accounts, `legacy_prefixes` |
|
|
70
|
+
| `.claude/settings.local.json` | per-user | no (gitignored) | `AWS_PROFILE`, owner/cost-center overrides |
|
|
71
|
+
|
|
72
|
+
`clawform init` writes the first one. For CI or scripted bootstrap, set
|
|
73
|
+
`CLAWFORM_INIT_CUSTOMER=… CLAWFORM_INIT_REGION=… clawform init`.
|
|
74
|
+
|
|
75
|
+
A fresh project with an empty `legacy_prefixes` has no legacy-freeze gate — that
|
|
76
|
+
is correct. Clawform never imposes another project's freeze list on you.
|
|
77
|
+
|
|
78
|
+
## Requirements
|
|
79
|
+
|
|
80
|
+
- Node.js ≥ 20 LTS
|
|
81
|
+
- AWS CLI v2
|
|
82
|
+
- Claude Code CLI
|
|
83
|
+
- *(recommended)* `cfn-lint` — `pip install cfn-lint`
|
|
84
|
+
- *(optional)* `cfn-guard` 3.x for Clawform's custom rules
|
|
85
|
+
|
|
86
|
+
## Links
|
|
87
|
+
|
|
88
|
+
- **[clawform.thejoseki.com](https://clawform.thejoseki.com)** — the product
|
|
89
|
+
page: what Clawform does, per-seat pricing, and how to get a license.
|
|
90
|
+
- **[thejoseki.com](https://thejoseki.com)** — Joseki, the vendor behind
|
|
91
|
+
Clawform. Known-good moves for agentic work.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
Commercial, per-seat. One seat per developer; no resale or redistribution.
|
|
96
|
+
Provided **as is**, with no warranty and no liability for AWS charges — you are
|
|
97
|
+
responsible for reviewing every change before it deploys and for your own cloud
|
|
98
|
+
spend. Full terms in the `LICENSE` file.
|
|
16
99
|
|
|
17
100
|
Clawform is not affiliated with Amazon Web Services, Inc. "AWS" and
|
|
18
101
|
"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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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": "
|
|
3
|
+
"version": "2.2.1",
|
|
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
|
-
"
|
|
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
|
+
}
|