@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
@@ -1,156 +1,156 @@
1
- import { paginateListStacks } from '@aws-sdk/client-cloudformation';
2
-
3
- export const STALE_DAYS = 180;
4
-
5
- const LIVE_STATUSES = [
6
- 'CREATE_COMPLETE',
7
- 'UPDATE_COMPLETE',
8
- 'UPDATE_ROLLBACK_COMPLETE',
9
- 'IMPORT_COMPLETE',
10
- ];
11
-
12
- const hasLegacyPrefix = (name, prefixes) => prefixes.some((p) => name.startsWith(p + '-'));
13
-
14
- const SERVERLESS_DESC_RE = /Serverless application/i;
15
-
16
- export async function fetchStacks(client) {
17
- const stacks = [];
18
- for await (const page of paginateListStacks(
19
- { client },
20
- { StackStatusFilter: LIVE_STATUSES },
21
- )) {
22
- for (const s of page.StackSummaries ?? []) {
23
- stacks.push({
24
- name: s.StackName,
25
- status: s.StackStatus,
26
- templateDescription: s.TemplateDescription,
27
- creationTime: s.CreationTime,
28
- lastUpdatedTime: s.LastUpdatedTime,
29
- });
30
- }
31
- }
32
- return stacks;
33
- }
34
-
35
- export function classifyStacks(stacks, now = new Date(), legacyPrefixes = []) {
36
- // legacyPrefixes comes from the consumer's clawform.config.json. Empty = a fresh
37
- // consumer with no legacy stacks; nothing gets routed to the `legacy` bucket.
38
- const result = { legacy: [], serverless: [], active: [], stale: [] };
39
- const staleMs = STALE_DAYS * 24 * 60 * 60 * 1000;
40
- for (const s of stacks) {
41
- if (hasLegacyPrefix(s.name, legacyPrefixes)) {
42
- result.legacy.push(s);
43
- continue;
44
- }
45
- if (SERVERLESS_DESC_RE.test(s.templateDescription ?? '')) {
46
- result.serverless.push(s);
47
- continue;
48
- }
49
- const lastTouch = s.lastUpdatedTime ?? s.creationTime;
50
- const ageMs = lastTouch ? now.getTime() - new Date(lastTouch).getTime() : 0;
51
- if (ageMs > staleMs) result.stale.push(s);
52
- else result.active.push(s);
53
- }
54
- return result;
55
- }
56
-
57
- function fmtDate(d) {
58
- if (!d) return '';
59
- return new Date(d).toISOString().slice(0, 10);
60
- }
61
-
62
- function escapePipe(s) {
63
- return String(s ?? '').replace(/\|/g, '\\|');
64
- }
65
-
66
- function renderSection(lines, title, items, headers, fmtRow) {
67
- lines.push(`## ${title} (${items.length})`, '');
68
- if (items.length === 0) {
69
- lines.push('_None_', '');
70
- return;
71
- }
72
- lines.push('| ' + headers.join(' | ') + ' |');
73
- lines.push('|' + headers.map(() => '---').join('|') + '|');
74
- const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
75
- for (const it of sorted) {
76
- lines.push('| ' + fmtRow(it).map((v) => `\`${escapePipe(v)}\``).join(' | ') + ' |');
77
- }
78
- lines.push('');
79
- }
80
-
81
- export function renderInventory(classified, meta) {
82
- const lines = [
83
- '# CloudFormation Stacks Inventory',
84
- '',
85
- `> Generated by \`clawform sync\` at ${meta.generatedAt}`,
86
- `> Account: ${meta.account} ยท Region: ${meta.region}`,
87
- '',
88
- ];
89
-
90
- renderSection(
91
- lines,
92
- '๐Ÿ”’ Legacy (refuse modify โ€” see rules/legacy-freeze.md)',
93
- classified.legacy,
94
- ['Stack', 'Status', 'Last update'],
95
- (s) => [s.name, s.status, fmtDate(s.lastUpdatedTime ?? s.creationTime)],
96
- );
97
-
98
- renderSection(
99
- lines,
100
- '๐Ÿ› ๏ธ Serverless Framework (managed by `sls` tool โ€” modify via serverless.yml)',
101
- classified.serverless,
102
- ['Stack', 'Status'],
103
- (s) => [s.name, s.status],
104
- );
105
-
106
- renderSection(
107
- lines,
108
- 'โœ… Active (Clawform-modifiable)',
109
- classified.active,
110
- ['Stack', 'Status', 'Last update'],
111
- (s) => [s.name, s.status, fmtDate(s.lastUpdatedTime ?? s.creationTime)],
112
- );
113
-
114
- renderSection(
115
- lines,
116
- `๐Ÿ’ค Stale (โ‰ฅ${STALE_DAYS} days no update โ€” review/decommission)`,
117
- classified.stale,
118
- ['Stack', 'Last update'],
119
- (s) => [s.name, fmtDate(s.lastUpdatedTime ?? s.creationTime)],
120
- );
121
-
122
- return lines.join('\n');
123
- }
124
-
125
- const SECTION_KEY_BY_TITLE_FRAGMENT = [
126
- ['Legacy', 'legacy'],
127
- ['Serverless', 'serverless'],
128
- ['Active', 'active'],
129
- ['Stale', 'stale'],
130
- ];
131
-
132
- function sectionKeyFromTitle(title) {
133
- const hit = SECTION_KEY_BY_TITLE_FRAGMENT.find(([fragment]) => title.includes(fragment));
134
- return hit ? hit[1] : null;
135
- }
136
-
137
- // Read-back counterpart to renderInventory โ€” recovers the stack list from a
138
- // committed .clawform/catalog/stacks-inventory.md without a live AWS call. Used by the
139
- // project-audit workflow (Phase 3), which runs with no network access.
140
- export function parseInventory(markdown) {
141
- const result = { legacy: [], serverless: [], active: [], stale: [] };
142
- let currentKey = null;
143
- for (const line of markdown.split(/\r?\n/)) {
144
- const heading = line.match(/^## (.+?) \(\d+\)$/);
145
- if (heading) {
146
- currentKey = sectionKeyFromTitle(heading[1]);
147
- continue;
148
- }
149
- if (!currentKey || !line.startsWith('| ')) continue;
150
- const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
151
- if (cells.length === 0 || cells[0] === 'Stack') continue;
152
- const name = cells[0].replace(/^`|`$/g, '');
153
- result[currentKey].push(name);
154
- }
155
- return result;
156
- }
1
+ import { paginateListStacks } from '@aws-sdk/client-cloudformation';
2
+
3
+ export const STALE_DAYS = 180;
4
+
5
+ const LIVE_STATUSES = [
6
+ 'CREATE_COMPLETE',
7
+ 'UPDATE_COMPLETE',
8
+ 'UPDATE_ROLLBACK_COMPLETE',
9
+ 'IMPORT_COMPLETE',
10
+ ];
11
+
12
+ const hasLegacyPrefix = (name, prefixes) => prefixes.some((p) => name.startsWith(p + '-'));
13
+
14
+ const SERVERLESS_DESC_RE = /Serverless application/i;
15
+
16
+ export async function fetchStacks(client) {
17
+ const stacks = [];
18
+ for await (const page of paginateListStacks(
19
+ { client },
20
+ { StackStatusFilter: LIVE_STATUSES },
21
+ )) {
22
+ for (const s of page.StackSummaries ?? []) {
23
+ stacks.push({
24
+ name: s.StackName,
25
+ status: s.StackStatus,
26
+ templateDescription: s.TemplateDescription,
27
+ creationTime: s.CreationTime,
28
+ lastUpdatedTime: s.LastUpdatedTime,
29
+ });
30
+ }
31
+ }
32
+ return stacks;
33
+ }
34
+
35
+ export function classifyStacks(stacks, now = new Date(), legacyPrefixes = []) {
36
+ // legacyPrefixes comes from the consumer's clawform.config.json. Empty = a fresh
37
+ // consumer with no legacy stacks; nothing gets routed to the `legacy` bucket.
38
+ const result = { legacy: [], serverless: [], active: [], stale: [] };
39
+ const staleMs = STALE_DAYS * 24 * 60 * 60 * 1000;
40
+ for (const s of stacks) {
41
+ if (hasLegacyPrefix(s.name, legacyPrefixes)) {
42
+ result.legacy.push(s);
43
+ continue;
44
+ }
45
+ if (SERVERLESS_DESC_RE.test(s.templateDescription ?? '')) {
46
+ result.serverless.push(s);
47
+ continue;
48
+ }
49
+ const lastTouch = s.lastUpdatedTime ?? s.creationTime;
50
+ const ageMs = lastTouch ? now.getTime() - new Date(lastTouch).getTime() : 0;
51
+ if (ageMs > staleMs) result.stale.push(s);
52
+ else result.active.push(s);
53
+ }
54
+ return result;
55
+ }
56
+
57
+ function fmtDate(d) {
58
+ if (!d) return '';
59
+ return new Date(d).toISOString().slice(0, 10);
60
+ }
61
+
62
+ function escapePipe(s) {
63
+ return String(s ?? '').replace(/\|/g, '\\|');
64
+ }
65
+
66
+ function renderSection(lines, title, items, headers, fmtRow) {
67
+ lines.push(`## ${title} (${items.length})`, '');
68
+ if (items.length === 0) {
69
+ lines.push('_None_', '');
70
+ return;
71
+ }
72
+ lines.push('| ' + headers.join(' | ') + ' |');
73
+ lines.push('|' + headers.map(() => '---').join('|') + '|');
74
+ const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
75
+ for (const it of sorted) {
76
+ lines.push('| ' + fmtRow(it).map((v) => `\`${escapePipe(v)}\``).join(' | ') + ' |');
77
+ }
78
+ lines.push('');
79
+ }
80
+
81
+ export function renderInventory(classified, meta) {
82
+ const lines = [
83
+ '# CloudFormation Stacks Inventory',
84
+ '',
85
+ `> Generated by \`clawform sync\` at ${meta.generatedAt}`,
86
+ `> Account: ${meta.account} ยท Region: ${meta.region}`,
87
+ '',
88
+ ];
89
+
90
+ renderSection(
91
+ lines,
92
+ '๐Ÿ”’ Legacy (refuse modify โ€” see rules/legacy-freeze.md)',
93
+ classified.legacy,
94
+ ['Stack', 'Status', 'Last update'],
95
+ (s) => [s.name, s.status, fmtDate(s.lastUpdatedTime ?? s.creationTime)],
96
+ );
97
+
98
+ renderSection(
99
+ lines,
100
+ '๐Ÿ› ๏ธ Serverless Framework (managed by `sls` tool โ€” modify via serverless.yml)',
101
+ classified.serverless,
102
+ ['Stack', 'Status'],
103
+ (s) => [s.name, s.status],
104
+ );
105
+
106
+ renderSection(
107
+ lines,
108
+ 'โœ… Active (Clawform-modifiable)',
109
+ classified.active,
110
+ ['Stack', 'Status', 'Last update'],
111
+ (s) => [s.name, s.status, fmtDate(s.lastUpdatedTime ?? s.creationTime)],
112
+ );
113
+
114
+ renderSection(
115
+ lines,
116
+ `๐Ÿ’ค Stale (โ‰ฅ${STALE_DAYS} days no update โ€” review/decommission)`,
117
+ classified.stale,
118
+ ['Stack', 'Last update'],
119
+ (s) => [s.name, fmtDate(s.lastUpdatedTime ?? s.creationTime)],
120
+ );
121
+
122
+ return lines.join('\n');
123
+ }
124
+
125
+ const SECTION_KEY_BY_TITLE_FRAGMENT = [
126
+ ['Legacy', 'legacy'],
127
+ ['Serverless', 'serverless'],
128
+ ['Active', 'active'],
129
+ ['Stale', 'stale'],
130
+ ];
131
+
132
+ function sectionKeyFromTitle(title) {
133
+ const hit = SECTION_KEY_BY_TITLE_FRAGMENT.find(([fragment]) => title.includes(fragment));
134
+ return hit ? hit[1] : null;
135
+ }
136
+
137
+ // Read-back counterpart to renderInventory โ€” recovers the stack list from a
138
+ // committed .clawform/catalog/stacks-inventory.md without a live AWS call. Used by the
139
+ // project-audit workflow (Phase 3), which runs with no network access.
140
+ export function parseInventory(markdown) {
141
+ const result = { legacy: [], serverless: [], active: [], stale: [] };
142
+ let currentKey = null;
143
+ for (const line of markdown.split(/\r?\n/)) {
144
+ const heading = line.match(/^## (.+?) \(\d+\)$/);
145
+ if (heading) {
146
+ currentKey = sectionKeyFromTitle(heading[1]);
147
+ continue;
148
+ }
149
+ if (!currentKey || !line.startsWith('| ')) continue;
150
+ const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
151
+ if (cells.length === 0 || cells[0] === 'Stack') continue;
152
+ const name = cells[0].replace(/^`|`$/g, '');
153
+ result[currentKey].push(name);
154
+ }
155
+ return result;
156
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { logger } from '../lib/logger.js';
6
6
  import { resolveClient, resolveTrialBenefitIds } from '../lib/license-client.js';
7
- import { writeLicense } from '../lib/license-store.js';
7
+ import { writeLicense, readLicenseRaw } from '../lib/license-store.js';
8
8
  import { machineFingerprint } from '../lib/fingerprint.js';
9
9
  import { claimTrialDevice } from '../lib/trial-claim.js';
10
10
 
@@ -12,6 +12,7 @@ const maskKey = (key) => `โ€ฆ${String(key).slice(-4)}`;
12
12
 
13
13
  export default async function activate({ key } = {}, {
14
14
  client = resolveClient(),
15
+ read = readLicenseRaw,
15
16
  write = writeLicense,
16
17
  fingerprint = machineFingerprint,
17
18
  trialBenefitIds = resolveTrialBenefitIds(),
@@ -25,6 +26,37 @@ export default async function activate({ key } = {}, {
25
26
 
26
27
  const fp = fingerprint();
27
28
 
29
+ // Idempotent re-activation. Polar offers no key-only "list activations", so a
30
+ // re-run would otherwise POST a brand-new activation every time and burn a
31
+ // slot โ€” the leak that locks a user out after `limit_activations` upgrades.
32
+ // Instead, if this machine's local record already points at an activation for
33
+ // THIS key, probe it with `validate(key, instanceId)`; if the vendor still
34
+ // honours it, re-adopt it (and re-sign the record, healing a stale one and
35
+ // stamping the CURRENT fingerprint) without minting a second slot. Only when
36
+ // the stored slot is gone/invalid do we fall through to a fresh activation.
37
+ //
38
+ // The slot's validity is judged SERVER-SIDE, not by matching the stored
39
+ // fingerprint: on Windows/macOS the fingerprint has no stable machine-id and
40
+ // shifts when the hostname changes (rename, MDM, DHCP), so a fingerprint guard
41
+ // would skip the reuse and re-create exactly the leak this fixes. The record
42
+ // is per-machine local state (~/.clawform), so a key match is the identity
43
+ // that matters. Deliberately BEFORE the trial-claim block: an already-activated
44
+ // machine must not re-claim a trial.
45
+ const prior = read();
46
+ if (prior.status === 'active' && prior.key === key && prior.instanceId) {
47
+ let check = null;
48
+ try { check = await client.validate(key, prior.instanceId); } catch { /* fall through to a fresh activate */ }
49
+ if (check?.status === 'valid') {
50
+ write({
51
+ status: 'active', key, instanceId: prior.instanceId, holder: prior.holder,
52
+ fingerprint: fp, lastValidatedAt: new Date(now()).toISOString(),
53
+ ...(check.expiresAt ? { expiresAt: check.expiresAt } : (prior.expiresAt ? { expiresAt: prior.expiresAt } : {})),
54
+ });
55
+ log(`License ${maskKey(key)} โ€” re-adopted this machine's existing activation. No extra seat used.`);
56
+ return;
57
+ }
58
+ }
59
+
28
60
  // Which tier this key belongs to decides whether our own service is consulted
29
61
  // at all. A PAID activation must never depend on it: someone who has just
30
62
  // paid, blocked by our downtime, is the failure the deploy/rollback invariant
@@ -49,7 +81,7 @@ export default async function activate({ key } = {}, {
49
81
  if (claim && claim.allowed === false) {
50
82
  throw new Error(
51
83
  'This machine has already used a Clawform trial, so a second one cannot start here. '
52
- + 'Buy a seat at https://clawform.thejoseki.com โ€” or, if you believe this is a mistake, '
84
+ + 'Buy a seat at https://clawform.thejoseki.com/?utm_source=cli โ€” or, if you believe this is a mistake, '
53
85
  + 'write to support@thejoseki.com and it will be sorted out by a person.',
54
86
  );
55
87
  }