@thejoseki/clawform 2.3.1 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +141 -120
  2. package/bin/clawform.js +161 -151
  3. package/package.json +65 -65
  4. package/src/aws/catalog.js +116 -116
  5. package/src/aws/changeset.js +231 -231
  6. package/src/aws/drift.js +59 -59
  7. package/src/aws/exports.js +213 -213
  8. package/src/aws/inventory.js +156 -156
  9. package/src/commands/activate.js +137 -105
  10. package/src/commands/ci-init.js +393 -212
  11. package/src/commands/cost-report.js +163 -163
  12. package/src/commands/deactivate.js +9 -2
  13. package/src/commands/deploy.js +413 -413
  14. package/src/commands/diff.js +39 -39
  15. package/src/commands/drift.js +35 -35
  16. package/src/commands/init-plugin.js +6 -6
  17. package/src/commands/init.js +360 -360
  18. package/src/commands/rollback.js +126 -126
  19. package/src/commands/status.js +147 -147
  20. package/src/commands/sync.js +50 -50
  21. package/src/commands/update.js +24 -0
  22. package/src/commands/validate.js +349 -349
  23. package/src/hooks/block-dangerous.js +62 -62
  24. package/src/hooks/cfn-lint-after-edit-eval.js +30 -30
  25. package/src/hooks/cfn-lint-after-edit.js +81 -81
  26. package/src/hooks/check-catalog-fresh-eval.js +63 -63
  27. package/src/hooks/check-catalog-fresh.js +33 -33
  28. package/src/hooks/session-context-eval.js +81 -81
  29. package/src/hooks/session-context.js +41 -41
  30. package/src/hooks/subagent-context-eval.js +44 -44
  31. package/src/hooks/subagent-context.js +48 -48
  32. package/src/lib/audit-synthesis.js +24 -24
  33. package/src/lib/aws-client.js +15 -15
  34. package/src/lib/cfn-yaml.js +92 -92
  35. package/src/lib/config.js +76 -76
  36. package/src/lib/constants.js +85 -85
  37. package/src/lib/defaults.js +16 -16
  38. package/src/lib/install-workflow.js +28 -28
  39. package/src/lib/kit-source.js +43 -5
  40. package/src/lib/license-client.js +84 -84
  41. package/src/lib/license-config.js +162 -162
  42. package/src/lib/license-store.js +43 -8
  43. package/src/lib/license.js +163 -150
  44. package/src/lib/lint-overrides.js +226 -226
  45. package/src/lib/logger.js +16 -16
  46. package/src/lib/project-config.js +145 -145
  47. package/src/lib/providers/polar.js +215 -215
  48. package/src/lib/session-state.js +91 -91
@@ -1,231 +1,231 @@
1
- import {
2
- CreateChangeSetCommand,
3
- DescribeChangeSetCommand,
4
- DeleteChangeSetCommand,
5
- DescribeStacksCommand,
6
- DescribeStackEventsCommand,
7
- ExecuteChangeSetCommand,
8
- } from '@aws-sdk/client-cloudformation';
9
- import { isDataResource } from '../lib/constants.js';
10
-
11
- const DEPLOYABLE_FROM_UPDATE = new Set([
12
- 'CREATE_COMPLETE',
13
- 'UPDATE_COMPLETE',
14
- 'UPDATE_ROLLBACK_COMPLETE',
15
- 'IMPORT_COMPLETE',
16
- 'IMPORT_ROLLBACK_COMPLETE',
17
- ]);
18
-
19
- const TERMINAL_EXECUTE_STATUSES = new Set([
20
- 'CREATE_COMPLETE',
21
- 'UPDATE_COMPLETE',
22
- 'ROLLBACK_COMPLETE',
23
- 'UPDATE_ROLLBACK_COMPLETE',
24
- 'CREATE_FAILED',
25
- 'ROLLBACK_FAILED',
26
- 'UPDATE_ROLLBACK_FAILED',
27
- 'UPDATE_FAILED',
28
- 'DELETE_COMPLETE',
29
- 'DELETE_FAILED',
30
- ]);
31
-
32
- const EMPTY_CHANGESET_RE = /didn'?t contain changes|no updates are to be performed/i;
33
-
34
- export async function stackStatus(client, stackName) {
35
- try {
36
- const out = await client.send(new DescribeStacksCommand({ StackName: stackName }));
37
- const stack = out.Stacks?.[0];
38
- if (!stack) return { exists: false };
39
- return { exists: true, status: stack.StackStatus, stack };
40
- } catch (err) {
41
- if (err.name === 'ValidationError' && /does not exist/i.test(err.message ?? '')) {
42
- return { exists: false };
43
- }
44
- throw err;
45
- }
46
- }
47
-
48
- export function decideChangeSetType({ exists, status }) {
49
- if (!exists) return 'CREATE';
50
- if (status === 'REVIEW_IN_PROGRESS') return 'CREATE';
51
- if (DEPLOYABLE_FROM_UPDATE.has(status)) return 'UPDATE';
52
- if (/_IN_PROGRESS$/.test(status)) {
53
- throw new Error(`Stack is ${status}; another operation is running. Wait or check the console.`);
54
- }
55
- throw new Error(`Stack is in ${status}; use /rollback to recover before deploying.`);
56
- }
57
-
58
- export function changeSetName(prefix = 'claude') {
59
- const d = new Date();
60
- const pad = (n) => String(n).padStart(2, '0');
61
- const stamp = `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}`;
62
- return `${prefix}-${stamp}`;
63
- }
64
-
65
- export async function createAndWait(client, {
66
- stackName,
67
- changeSetType,
68
- templateBody,
69
- parameters = [],
70
- tags = [],
71
- capabilities = [],
72
- name = changeSetName(),
73
- pollIntervalMs = 2000,
74
- timeoutMs = 5 * 60 * 1000,
75
- }) {
76
- await client.send(new CreateChangeSetCommand({
77
- StackName: stackName,
78
- ChangeSetName: name,
79
- ChangeSetType: changeSetType,
80
- TemplateBody: templateBody,
81
- Parameters: parameters,
82
- Tags: tags,
83
- Capabilities: capabilities,
84
- }));
85
-
86
- const started = Date.now();
87
- while (true) {
88
- const desc = await describeFull(client, stackName, name);
89
- if (desc.Status === 'CREATE_COMPLETE') {
90
- return {
91
- name,
92
- id: desc.ChangeSetId,
93
- isEmpty: false,
94
- changes: desc.Changes ?? [],
95
- statusReason: desc.StatusReason ?? null,
96
- };
97
- }
98
- if (desc.Status === 'FAILED') {
99
- if (EMPTY_CHANGESET_RE.test(desc.StatusReason ?? '')) {
100
- await safeDelete(client, stackName, name);
101
- return { name, id: desc.ChangeSetId, isEmpty: true, changes: [], statusReason: desc.StatusReason };
102
- }
103
- throw new Error(`Change-set creation failed: ${desc.StatusReason ?? '(no reason)'}`);
104
- }
105
- if (Date.now() - started > timeoutMs) {
106
- throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for change-set ${name} to be ready (last status: ${desc.Status}).`);
107
- }
108
- await sleep(pollIntervalMs);
109
- }
110
- }
111
-
112
- async function describeFull(client, stackName, name) {
113
- const all = [];
114
- let nextToken;
115
- let first;
116
- do {
117
- const out = await client.send(new DescribeChangeSetCommand({
118
- StackName: stackName,
119
- ChangeSetName: name,
120
- NextToken: nextToken,
121
- }));
122
- if (!first) first = out;
123
- for (const c of out.Changes ?? []) all.push(c);
124
- nextToken = out.NextToken;
125
- } while (nextToken);
126
- return { ...first, Changes: all };
127
- }
128
-
129
- async function safeDelete(client, stackName, name) {
130
- try {
131
- await client.send(new DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: name }));
132
- } catch {
133
- // Empty change-sets sometimes auto-clean; ignore deletion errors here.
134
- }
135
- }
136
-
137
- export function classifyChanges(changes) {
138
- const buckets = { add: [], modify: [], replace: [], remove: [], dataResourceReplace: [] };
139
- for (const c of changes) {
140
- if (c.Type !== 'Resource' || !c.ResourceChange) continue;
141
- const rc = c.ResourceChange;
142
- const entry = {
143
- logicalId: rc.LogicalResourceId,
144
- physicalId: rc.PhysicalResourceId ?? null,
145
- resourceType: rc.ResourceType,
146
- replacement: rc.Replacement,
147
- scope: rc.Scope ?? [],
148
- detailsTargets: (rc.Details ?? []).map((d) => d.Target?.Attribute).filter(Boolean),
149
- };
150
- switch (rc.Action) {
151
- case 'Add':
152
- buckets.add.push(entry);
153
- break;
154
- case 'Remove':
155
- buckets.remove.push(entry);
156
- break;
157
- case 'Modify':
158
- if (rc.Replacement === 'True' || rc.Replacement === 'Conditional') {
159
- buckets.replace.push(entry);
160
- if (isDataResource(rc.ResourceType)) buckets.dataResourceReplace.push(entry);
161
- } else {
162
- buckets.modify.push(entry);
163
- }
164
- break;
165
- default:
166
- buckets.modify.push(entry);
167
- }
168
- }
169
- return buckets;
170
- }
171
-
172
- export async function executeAndPoll(client, {
173
- stackName,
174
- changeSetName: name,
175
- onEvent = () => {},
176
- pollIntervalMs = 3000,
177
- timeoutMs = 60 * 60 * 1000,
178
- }) {
179
- await client.send(new ExecuteChangeSetCommand({ StackName: stackName, ChangeSetName: name }));
180
-
181
- const seen = new Set();
182
- const started = Date.now();
183
- while (true) {
184
- const events = await fetchEvents(client, stackName, seen);
185
- for (const ev of events) {
186
- if (seen.has(ev.EventId)) continue;
187
- seen.add(ev.EventId);
188
- onEvent(ev);
189
- }
190
-
191
- const { stack } = await stackStatus(client, stackName);
192
- const status = stack?.StackStatus;
193
- if (status && TERMINAL_EXECUTE_STATUSES.has(status)) {
194
- return { stackStatus: status, stack };
195
- }
196
- if (Date.now() - started > timeoutMs) {
197
- throw new Error(`Timed out after ${Math.round(timeoutMs / 60000)}m waiting for stack ${stackName} (last status: ${status}).`);
198
- }
199
- await sleep(pollIntervalMs);
200
- }
201
- }
202
-
203
- // Paginate DescribeStackEvents until we encounter an event we've already narrated.
204
- // DescribeStackEvents returns newest-first within a page AND pages are newest-first,
205
- // so once we hit a known EventId every older event is also already in `seen` — safe
206
- // to stop. Without this, a poll window emitting >100 events (large stacks, nested-
207
- // stack expansion, rollback fan-out) silently drops the older end off page 1 and
208
- // onEvent is never called for them — the operator misses CREATE_FAILED rows that
209
- // scrolled off between polls. (Tracked as merged_bug_008 from ultrareview.)
210
- export async function fetchEvents(client, stackName, seen = new Set()) {
211
- const events = [];
212
- let nextToken;
213
- do {
214
- const resp = await client.send(new DescribeStackEventsCommand({
215
- StackName: stackName,
216
- NextToken: nextToken,
217
- }));
218
- let stopHere = false;
219
- for (const ev of resp.StackEvents ?? []) {
220
- if (seen.has(ev.EventId)) { stopHere = true; break; }
221
- events.push(ev);
222
- }
223
- if (stopHere) break;
224
- nextToken = resp.NextToken;
225
- } while (nextToken);
226
- return events.reverse(); // present oldest-first for narration
227
- }
228
-
229
- function sleep(ms) {
230
- return new Promise((r) => setTimeout(r, ms));
231
- }
1
+ import {
2
+ CreateChangeSetCommand,
3
+ DescribeChangeSetCommand,
4
+ DeleteChangeSetCommand,
5
+ DescribeStacksCommand,
6
+ DescribeStackEventsCommand,
7
+ ExecuteChangeSetCommand,
8
+ } from '@aws-sdk/client-cloudformation';
9
+ import { isDataResource } from '../lib/constants.js';
10
+
11
+ const DEPLOYABLE_FROM_UPDATE = new Set([
12
+ 'CREATE_COMPLETE',
13
+ 'UPDATE_COMPLETE',
14
+ 'UPDATE_ROLLBACK_COMPLETE',
15
+ 'IMPORT_COMPLETE',
16
+ 'IMPORT_ROLLBACK_COMPLETE',
17
+ ]);
18
+
19
+ const TERMINAL_EXECUTE_STATUSES = new Set([
20
+ 'CREATE_COMPLETE',
21
+ 'UPDATE_COMPLETE',
22
+ 'ROLLBACK_COMPLETE',
23
+ 'UPDATE_ROLLBACK_COMPLETE',
24
+ 'CREATE_FAILED',
25
+ 'ROLLBACK_FAILED',
26
+ 'UPDATE_ROLLBACK_FAILED',
27
+ 'UPDATE_FAILED',
28
+ 'DELETE_COMPLETE',
29
+ 'DELETE_FAILED',
30
+ ]);
31
+
32
+ const EMPTY_CHANGESET_RE = /didn'?t contain changes|no updates are to be performed/i;
33
+
34
+ export async function stackStatus(client, stackName) {
35
+ try {
36
+ const out = await client.send(new DescribeStacksCommand({ StackName: stackName }));
37
+ const stack = out.Stacks?.[0];
38
+ if (!stack) return { exists: false };
39
+ return { exists: true, status: stack.StackStatus, stack };
40
+ } catch (err) {
41
+ if (err.name === 'ValidationError' && /does not exist/i.test(err.message ?? '')) {
42
+ return { exists: false };
43
+ }
44
+ throw err;
45
+ }
46
+ }
47
+
48
+ export function decideChangeSetType({ exists, status }) {
49
+ if (!exists) return 'CREATE';
50
+ if (status === 'REVIEW_IN_PROGRESS') return 'CREATE';
51
+ if (DEPLOYABLE_FROM_UPDATE.has(status)) return 'UPDATE';
52
+ if (/_IN_PROGRESS$/.test(status)) {
53
+ throw new Error(`Stack is ${status}; another operation is running. Wait or check the console.`);
54
+ }
55
+ throw new Error(`Stack is in ${status}; use /rollback to recover before deploying.`);
56
+ }
57
+
58
+ export function changeSetName(prefix = 'claude') {
59
+ const d = new Date();
60
+ const pad = (n) => String(n).padStart(2, '0');
61
+ const stamp = `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}`;
62
+ return `${prefix}-${stamp}`;
63
+ }
64
+
65
+ export async function createAndWait(client, {
66
+ stackName,
67
+ changeSetType,
68
+ templateBody,
69
+ parameters = [],
70
+ tags = [],
71
+ capabilities = [],
72
+ name = changeSetName(),
73
+ pollIntervalMs = 2000,
74
+ timeoutMs = 5 * 60 * 1000,
75
+ }) {
76
+ await client.send(new CreateChangeSetCommand({
77
+ StackName: stackName,
78
+ ChangeSetName: name,
79
+ ChangeSetType: changeSetType,
80
+ TemplateBody: templateBody,
81
+ Parameters: parameters,
82
+ Tags: tags,
83
+ Capabilities: capabilities,
84
+ }));
85
+
86
+ const started = Date.now();
87
+ while (true) {
88
+ const desc = await describeFull(client, stackName, name);
89
+ if (desc.Status === 'CREATE_COMPLETE') {
90
+ return {
91
+ name,
92
+ id: desc.ChangeSetId,
93
+ isEmpty: false,
94
+ changes: desc.Changes ?? [],
95
+ statusReason: desc.StatusReason ?? null,
96
+ };
97
+ }
98
+ if (desc.Status === 'FAILED') {
99
+ if (EMPTY_CHANGESET_RE.test(desc.StatusReason ?? '')) {
100
+ await safeDelete(client, stackName, name);
101
+ return { name, id: desc.ChangeSetId, isEmpty: true, changes: [], statusReason: desc.StatusReason };
102
+ }
103
+ throw new Error(`Change-set creation failed: ${desc.StatusReason ?? '(no reason)'}`);
104
+ }
105
+ if (Date.now() - started > timeoutMs) {
106
+ throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for change-set ${name} to be ready (last status: ${desc.Status}).`);
107
+ }
108
+ await sleep(pollIntervalMs);
109
+ }
110
+ }
111
+
112
+ async function describeFull(client, stackName, name) {
113
+ const all = [];
114
+ let nextToken;
115
+ let first;
116
+ do {
117
+ const out = await client.send(new DescribeChangeSetCommand({
118
+ StackName: stackName,
119
+ ChangeSetName: name,
120
+ NextToken: nextToken,
121
+ }));
122
+ if (!first) first = out;
123
+ for (const c of out.Changes ?? []) all.push(c);
124
+ nextToken = out.NextToken;
125
+ } while (nextToken);
126
+ return { ...first, Changes: all };
127
+ }
128
+
129
+ async function safeDelete(client, stackName, name) {
130
+ try {
131
+ await client.send(new DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: name }));
132
+ } catch {
133
+ // Empty change-sets sometimes auto-clean; ignore deletion errors here.
134
+ }
135
+ }
136
+
137
+ export function classifyChanges(changes) {
138
+ const buckets = { add: [], modify: [], replace: [], remove: [], dataResourceReplace: [] };
139
+ for (const c of changes) {
140
+ if (c.Type !== 'Resource' || !c.ResourceChange) continue;
141
+ const rc = c.ResourceChange;
142
+ const entry = {
143
+ logicalId: rc.LogicalResourceId,
144
+ physicalId: rc.PhysicalResourceId ?? null,
145
+ resourceType: rc.ResourceType,
146
+ replacement: rc.Replacement,
147
+ scope: rc.Scope ?? [],
148
+ detailsTargets: (rc.Details ?? []).map((d) => d.Target?.Attribute).filter(Boolean),
149
+ };
150
+ switch (rc.Action) {
151
+ case 'Add':
152
+ buckets.add.push(entry);
153
+ break;
154
+ case 'Remove':
155
+ buckets.remove.push(entry);
156
+ break;
157
+ case 'Modify':
158
+ if (rc.Replacement === 'True' || rc.Replacement === 'Conditional') {
159
+ buckets.replace.push(entry);
160
+ if (isDataResource(rc.ResourceType)) buckets.dataResourceReplace.push(entry);
161
+ } else {
162
+ buckets.modify.push(entry);
163
+ }
164
+ break;
165
+ default:
166
+ buckets.modify.push(entry);
167
+ }
168
+ }
169
+ return buckets;
170
+ }
171
+
172
+ export async function executeAndPoll(client, {
173
+ stackName,
174
+ changeSetName: name,
175
+ onEvent = () => {},
176
+ pollIntervalMs = 3000,
177
+ timeoutMs = 60 * 60 * 1000,
178
+ }) {
179
+ await client.send(new ExecuteChangeSetCommand({ StackName: stackName, ChangeSetName: name }));
180
+
181
+ const seen = new Set();
182
+ const started = Date.now();
183
+ while (true) {
184
+ const events = await fetchEvents(client, stackName, seen);
185
+ for (const ev of events) {
186
+ if (seen.has(ev.EventId)) continue;
187
+ seen.add(ev.EventId);
188
+ onEvent(ev);
189
+ }
190
+
191
+ const { stack } = await stackStatus(client, stackName);
192
+ const status = stack?.StackStatus;
193
+ if (status && TERMINAL_EXECUTE_STATUSES.has(status)) {
194
+ return { stackStatus: status, stack };
195
+ }
196
+ if (Date.now() - started > timeoutMs) {
197
+ throw new Error(`Timed out after ${Math.round(timeoutMs / 60000)}m waiting for stack ${stackName} (last status: ${status}).`);
198
+ }
199
+ await sleep(pollIntervalMs);
200
+ }
201
+ }
202
+
203
+ // Paginate DescribeStackEvents until we encounter an event we've already narrated.
204
+ // DescribeStackEvents returns newest-first within a page AND pages are newest-first,
205
+ // so once we hit a known EventId every older event is also already in `seen` — safe
206
+ // to stop. Without this, a poll window emitting >100 events (large stacks, nested-
207
+ // stack expansion, rollback fan-out) silently drops the older end off page 1 and
208
+ // onEvent is never called for them — the operator misses CREATE_FAILED rows that
209
+ // scrolled off between polls. (Tracked as merged_bug_008 from ultrareview.)
210
+ export async function fetchEvents(client, stackName, seen = new Set()) {
211
+ const events = [];
212
+ let nextToken;
213
+ do {
214
+ const resp = await client.send(new DescribeStackEventsCommand({
215
+ StackName: stackName,
216
+ NextToken: nextToken,
217
+ }));
218
+ let stopHere = false;
219
+ for (const ev of resp.StackEvents ?? []) {
220
+ if (seen.has(ev.EventId)) { stopHere = true; break; }
221
+ events.push(ev);
222
+ }
223
+ if (stopHere) break;
224
+ nextToken = resp.NextToken;
225
+ } while (nextToken);
226
+ return events.reverse(); // present oldest-first for narration
227
+ }
228
+
229
+ function sleep(ms) {
230
+ return new Promise((r) => setTimeout(r, ms));
231
+ }
package/src/aws/drift.js CHANGED
@@ -1,59 +1,59 @@
1
- import {
2
- DetectStackDriftCommand,
3
- DescribeStackDriftDetectionStatusCommand,
4
- DescribeStackResourceDriftsCommand,
5
- } from '@aws-sdk/client-cloudformation';
6
-
7
- const TERMINAL = new Set(['DETECTION_COMPLETE', 'DETECTION_FAILED']);
8
-
9
- export async function detectAndWait(client, {
10
- stackName,
11
- pollIntervalMs = 3000,
12
- timeoutMs = 10 * 60 * 1000,
13
- }) {
14
- const { StackDriftDetectionId } = await client.send(new DetectStackDriftCommand({ StackName: stackName }));
15
-
16
- const started = Date.now();
17
- while (true) {
18
- const out = await client.send(new DescribeStackDriftDetectionStatusCommand({ StackDriftDetectionId }));
19
- if (TERMINAL.has(out.DetectionStatus)) {
20
- if (out.DetectionStatus === 'DETECTION_FAILED') {
21
- throw new Error(`Drift detection failed for ${stackName}: ${out.DetectionStatusReason ?? '(no reason)'}`);
22
- }
23
- return {
24
- stackDriftStatus: out.StackDriftStatus,
25
- driftedResourceCount: out.DriftedStackResourceCount ?? 0,
26
- };
27
- }
28
- if (Date.now() - started > timeoutMs) {
29
- throw new Error(`Timed out after ${Math.round(timeoutMs / 60000)}m waiting for drift detection on ${stackName}.`);
30
- }
31
- await sleep(pollIntervalMs);
32
- }
33
- }
34
-
35
- export async function listResourceDrifts(client, stackName, statusFilter = ['MODIFIED', 'DELETED']) {
36
- const drifts = [];
37
- let nextToken;
38
- do {
39
- const out = await client.send(new DescribeStackResourceDriftsCommand({
40
- StackName: stackName,
41
- StackResourceDriftStatusFilters: statusFilter,
42
- NextToken: nextToken,
43
- }));
44
- for (const d of out.StackResourceDrifts ?? []) {
45
- drifts.push({
46
- logicalId: d.LogicalResourceId,
47
- resourceType: d.ResourceType,
48
- driftStatus: d.StackResourceDriftStatus,
49
- propertyDifferences: d.PropertyDifferences ?? [],
50
- });
51
- }
52
- nextToken = out.NextToken;
53
- } while (nextToken);
54
- return drifts;
55
- }
56
-
57
- function sleep(ms) {
58
- return new Promise((r) => setTimeout(r, ms));
59
- }
1
+ import {
2
+ DetectStackDriftCommand,
3
+ DescribeStackDriftDetectionStatusCommand,
4
+ DescribeStackResourceDriftsCommand,
5
+ } from '@aws-sdk/client-cloudformation';
6
+
7
+ const TERMINAL = new Set(['DETECTION_COMPLETE', 'DETECTION_FAILED']);
8
+
9
+ export async function detectAndWait(client, {
10
+ stackName,
11
+ pollIntervalMs = 3000,
12
+ timeoutMs = 10 * 60 * 1000,
13
+ }) {
14
+ const { StackDriftDetectionId } = await client.send(new DetectStackDriftCommand({ StackName: stackName }));
15
+
16
+ const started = Date.now();
17
+ while (true) {
18
+ const out = await client.send(new DescribeStackDriftDetectionStatusCommand({ StackDriftDetectionId }));
19
+ if (TERMINAL.has(out.DetectionStatus)) {
20
+ if (out.DetectionStatus === 'DETECTION_FAILED') {
21
+ throw new Error(`Drift detection failed for ${stackName}: ${out.DetectionStatusReason ?? '(no reason)'}`);
22
+ }
23
+ return {
24
+ stackDriftStatus: out.StackDriftStatus,
25
+ driftedResourceCount: out.DriftedStackResourceCount ?? 0,
26
+ };
27
+ }
28
+ if (Date.now() - started > timeoutMs) {
29
+ throw new Error(`Timed out after ${Math.round(timeoutMs / 60000)}m waiting for drift detection on ${stackName}.`);
30
+ }
31
+ await sleep(pollIntervalMs);
32
+ }
33
+ }
34
+
35
+ export async function listResourceDrifts(client, stackName, statusFilter = ['MODIFIED', 'DELETED']) {
36
+ const drifts = [];
37
+ let nextToken;
38
+ do {
39
+ const out = await client.send(new DescribeStackResourceDriftsCommand({
40
+ StackName: stackName,
41
+ StackResourceDriftStatusFilters: statusFilter,
42
+ NextToken: nextToken,
43
+ }));
44
+ for (const d of out.StackResourceDrifts ?? []) {
45
+ drifts.push({
46
+ logicalId: d.LogicalResourceId,
47
+ resourceType: d.ResourceType,
48
+ driftStatus: d.StackResourceDriftStatus,
49
+ propertyDifferences: d.PropertyDifferences ?? [],
50
+ });
51
+ }
52
+ nextToken = out.NextToken;
53
+ } while (nextToken);
54
+ return drifts;
55
+ }
56
+
57
+ function sleep(ms) {
58
+ return new Promise((r) => setTimeout(r, ms));
59
+ }