@haaaiawd/anws 2.0.2 → 2.0.4

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <img src="assets/logo-cli.png" width="260" alt="Anws">
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
- [![Version](https://img.shields.io/badge/version-v2.0.2-7FB5B6)](https://github.com/Haaaiawd/Anws/releases)
6
+ [![Version](https://img.shields.io/badge/version-v2.0.3-7FB5B6)](https://github.com/Haaaiawd/Anws/releases)
7
7
  [![Targets](https://img.shields.io/badge/Targets-Windsurf%20%7C%20Claude%20Code%20%7C%20Copilot%20%7C%20Cursor%20%7C%20Codex%20Preview%20%7C%20OpenCode%20%7C%20Trae%20%7C%20Qoder%20%7C%20Kilo%20Code-blueviolet)](https://github.com/Haaaiawd/Anws)
8
8
 
9
9
  [English](./README.md) | [中文](./README_CN.md)
@@ -140,6 +140,7 @@ anws update
140
140
  - **State source**
141
141
  - `anws update` reads `.anws/install-lock.json`
142
142
  - if the lock is missing or invalid, it falls back to directory scan
143
+ - if lock drift is detected, directory scan becomes the effective source for the current update
143
144
  - a real `anws update` can rebuild `.anws/install-lock.json` from detected targets when fallback is active
144
145
 
145
146
  - **`AGENTS.md` behavior**
package/bin/cli.js CHANGED
@@ -40,7 +40,7 @@ SUPPORTED TARGETS
40
40
  EXAMPLES
41
41
  anws init # Choose target IDEs and install their managed workflow projections
42
42
  anws init --target windsurf,codex,opencode
43
- anws update # Update all matched targets from install-lock or directory scan fallback
43
+ anws update # Update all matched targets from install-lock, fallback scan, or drift repair
44
44
  anws update --check # Preview grouped changes per target without writing files
45
45
  `.trimStart();
46
46
 
package/lib/init.js CHANGED
@@ -15,10 +15,11 @@ const { success, warn, info, fileLine, skippedLine, blank, logo, section } = req
15
15
  async function init() {
16
16
  const cwd = process.cwd();
17
17
  logo();
18
- const targets = await selectTargets();
18
+ const installState = await detectInstallState(cwd);
19
+ const retainedTargetIds = await resolveRetainedTargetIds(cwd, installState);
20
+ const targets = await selectTargets(installState, retainedTargetIds);
19
21
  const targetIds = Array.from(new Set(targets.map((item) => item.id)));
20
22
  const targetPlans = buildProjectionPlan(targetIds);
21
- const installState = await detectInstallState(cwd);
22
23
  const srcAgents = ROOT_AGENTS_FILE;
23
24
  const cliVersion = require(path.join(__dirname, '..', 'package.json')).version;
24
25
 
@@ -40,6 +41,7 @@ async function init() {
40
41
 
41
42
  for (const targetPlan of targetPlans) {
42
43
  const target = getTarget(targetPlan.targetId);
44
+ const targetAlreadyInstalled = retainedTargetIds.includes(target.id);
43
45
  const rootAgentsExists = await pathExists(path.join(cwd, 'AGENTS.md'));
44
46
  const agentsDecision = target.id === 'antigravity'
45
47
  ? await resolveAgentsInstall({
@@ -60,7 +62,11 @@ async function init() {
60
62
  agentsUpdatePlan = planAgentsUpdate({ templateContent, existingContent });
61
63
  }
62
64
 
63
- const conflicting = await findConflicts(cwd, targetPlan.managedFiles, sessionWrittenFiles);
65
+ const conflicting = await findConflicts(
66
+ cwd,
67
+ targetAlreadyInstalled ? [] : targetPlan.managedFiles.filter((rel) => rel !== 'AGENTS.md'),
68
+ sessionWrittenFiles
69
+ );
64
70
  if (conflicting.length > 0) {
65
71
  const confirmed = await askOverwrite(conflicting.length, target.label);
66
72
  if (!confirmed) {
@@ -214,24 +220,79 @@ function printSummary(files, skipped = [], action) {
214
220
  }
215
221
  }
216
222
 
217
- async function selectTargets() {
223
+ async function selectTargets(installState, retainedTargetIds = []) {
224
+ const installedTargetIds = new Set(retainedTargetIds);
225
+
218
226
  if (global.__ANWS_TARGET_IDS && global.__ANWS_TARGET_IDS.length > 0) {
219
- return global.__ANWS_TARGET_IDS.map((targetId) => getTarget(targetId));
227
+ return Array.from(new Set([
228
+ ...retainedTargetIds,
229
+ ...global.__ANWS_TARGET_IDS
230
+ ])).map((targetId) => getTarget(targetId));
220
231
  }
221
232
 
222
233
  if (!process.stdin.isTTY) {
223
- return [getTarget('antigravity')];
234
+ return retainedTargetIds.length > 0
235
+ ? retainedTargetIds.map((targetId) => getTarget(targetId))
236
+ : [getTarget('antigravity')];
224
237
  }
225
238
 
226
239
  const targets = listTargets();
240
+ const lockedIndexes = [];
241
+ const initialSelectedIndexes = [];
242
+
243
+ for (const [index, target] of targets.entries()) {
244
+ if (installedTargetIds.has(target.id)) {
245
+ lockedIndexes.push(index);
246
+ initialSelectedIndexes.push(index);
247
+ }
248
+ }
249
+
250
+ if (initialSelectedIndexes.length === 0) {
251
+ const antigravityIndex = targets.findIndex((target) => target.id === 'antigravity');
252
+ if (antigravityIndex >= 0) {
253
+ initialSelectedIndexes.push(antigravityIndex);
254
+ }
255
+ }
227
256
 
228
257
  return selectMultiple({
229
258
  message: 'Choose your target AI IDEs:',
230
- options: targets.map((target) => ({ label: target.label, value: target.id })),
231
- initialSelectedIndexes: [1]
259
+ options: targets.map((target) => ({
260
+ label: target.label,
261
+ value: target.id,
262
+ locked: installedTargetIds.has(target.id)
263
+ })),
264
+ initialSelectedIndexes,
265
+ lockedIndexes
232
266
  }).then((selectedOptions) => selectedOptions.map((option) => getTarget(option.value)));
233
267
  }
234
268
 
269
+ async function resolveRetainedTargetIds(cwd, installState) {
270
+ if (!installState.needsFallback && !installState.drift.hasDrift) {
271
+ return installState.selectedTargets;
272
+ }
273
+
274
+ const retainedTargetIds = [];
275
+
276
+ for (const targetId of installState.selectedTargets) {
277
+ const [targetPlan] = buildProjectionPlan([targetId]);
278
+ const managedFiles = (targetPlan?.managedFiles || []).filter((rel) => rel !== 'AGENTS.md');
279
+ if (managedFiles.length === 0) {
280
+ retainedTargetIds.push(targetId);
281
+ continue;
282
+ }
283
+
284
+ const managedExists = await Promise.all(
285
+ managedFiles.map((rel) => pathExists(path.join(cwd, rel)))
286
+ );
287
+
288
+ if (managedExists.every(Boolean)) {
289
+ retainedTargetIds.push(targetId);
290
+ }
291
+ }
292
+
293
+ return retainedTargetIds;
294
+ }
295
+
235
296
  function printNextSteps(targets) {
236
297
  blank();
237
298
  section('Next steps', targets.some((target) => target.rootAgentFile)
@@ -170,10 +170,14 @@ async function detectInstallState(cwd) {
170
170
  const fallbackReason = !needsFallback
171
171
  ? null
172
172
  : (!lockResult.exists ? 'missing_lock' : 'invalid_lock');
173
- const selectedTargets = lockTargets.length > 0
174
- ? lockTargets.map((item) => item.targetId)
175
- : scannedTargets.map((item) => item.id);
176
173
  const drift = detectLockDrift(lockResult.lock, scannedTargets);
174
+ const shouldPreferScannedTargets = needsFallback || drift.hasDrift || lockTargets.length === 0;
175
+ const selectedTargets = shouldPreferScannedTargets
176
+ ? scannedTargets.map((item) => item.id)
177
+ : lockTargets.map((item) => item.targetId);
178
+ const stateSource = needsFallback
179
+ ? 'directory_scan_fallback'
180
+ : (drift.hasDrift ? 'directory_scan_drift' : 'install_lock');
177
181
 
178
182
  return {
179
183
  lockResult,
@@ -182,8 +186,8 @@ async function detectInstallState(cwd) {
182
186
  drift,
183
187
  needsFallback,
184
188
  fallbackReason,
185
- stateSource: needsFallback ? 'directory_scan_fallback' : 'install_lock',
186
- canRebuildLock: needsFallback && selectedTargets.length > 0
189
+ stateSource,
190
+ canRebuildLock: (needsFallback || drift.hasDrift) && selectedTargets.length > 0
187
191
  };
188
192
  }
189
193
 
package/lib/prompt.js CHANGED
@@ -14,12 +14,18 @@ const KEY = {
14
14
  ARROW_LEFT: '\u001b[D'
15
15
  };
16
16
 
17
- async function selectMultiple({ message, options, initialSelectedIndexes = [] }) {
17
+ async function selectMultiple({ message, options, initialSelectedIndexes = [], lockedIndexes = [] }) {
18
+ const normalizedLockedIndexes = new Set(lockedIndexes.filter((index) => index >= 0 && index < options.length));
19
+ const normalizedInitialSelectedIndexes = Array.from(new Set([
20
+ ...initialSelectedIndexes.filter((index) => index >= 0 && index < options.length),
21
+ ...normalizedLockedIndexes
22
+ ]));
23
+
18
24
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
19
- return initialSelectedIndexes.map((index) => options[index]).filter(Boolean);
25
+ return normalizedInitialSelectedIndexes.map((index) => options[index]).filter(Boolean);
20
26
  }
21
27
 
22
- const selected = new Set(initialSelectedIndexes.filter((index) => index >= 0 && index < options.length));
28
+ const selected = new Set(normalizedInitialSelectedIndexes);
23
29
  const state = {
24
30
  cursorIndex: 0,
25
31
  errorMessage: ''
@@ -49,6 +55,10 @@ async function selectMultiple({ message, options, initialSelectedIndexes = [] })
49
55
  }
50
56
 
51
57
  if (key === KEY.SPACE) {
58
+ if (normalizedLockedIndexes.has(state.cursorIndex)) {
59
+ state.errorMessage = 'Already installed targets are locked. You can only add more targets.';
60
+ return 'render';
61
+ }
52
62
  if (selected.has(state.cursorIndex)) {
53
63
  selected.delete(state.cursorIndex);
54
64
  } else {
@@ -121,7 +131,8 @@ function renderMultiSelect({ message, options, selected, cursorIndex, errorMessa
121
131
  const isSelected = selected.has(index);
122
132
  const cursor = isActive ? colorize('❯', PALETTE.brand) : ' ';
123
133
  const mark = isSelected ? colorize('◉', PALETTE.brand) : colorize('◌', PALETTE.muted);
124
- const label = isActive ? colorize(option.label, PALETTE.ink) : option.label;
134
+ const labelText = option.locked ? `${option.label} ${colorize('(installed)', PALETTE.muted)}` : option.label;
135
+ const label = isActive ? colorize(labelText, PALETTE.ink) : labelText;
125
136
  return `${cursor} ${mark} ${label}`;
126
137
  });
127
138
 
@@ -133,7 +144,7 @@ function renderMultiSelect({ message, options, selected, cursorIndex, errorMessa
133
144
  '',
134
145
  ...optionLines,
135
146
  '',
136
- errorMessage ? colorize(errorMessage, c.yellow) : colorize('Choose any set of targets, then press Enter.', PALETTE.muted)
147
+ errorMessage ? colorize(errorMessage, c.yellow) : colorize('Choose targets to add. Installed targets stay selected.', PALETTE.muted)
137
148
  ],
138
149
  accent: PALETTE.brand,
139
150
  borderTone: PALETTE.muted,
package/lib/update.js CHANGED
@@ -129,7 +129,7 @@ async function update(options = {}) {
129
129
  blank();
130
130
  }
131
131
  printTargetSelection(installState, targetContexts.map((context) => context.target));
132
- if (!check && installState.needsFallback && selectedTargetIds.length > 0) {
132
+ if (!check && installState.canRebuildLock && selectedTargetIds.length > 0) {
133
133
  const generatedAt = new Date().toISOString();
134
134
  await writeInstallLock(cwd, createInstallLock({
135
135
  cliVersion: version,
@@ -217,11 +217,14 @@ async function update(options = {}) {
217
217
  }
218
218
  });
219
219
  const generatedAt = new Date().toISOString();
220
+ const existingLockTargets = installState.canRebuildLock
221
+ ? []
222
+ : (installState.lockResult.lock?.targets || []);
220
223
  await writeInstallLock(cwd, createInstallLock({
221
224
  cliVersion: version,
222
225
  generatedAt,
223
226
  targets: dedupeTargets([
224
- ...(installState.lockResult.lock?.targets || []),
227
+ ...existingLockTargets,
225
228
  ...successfulTargets
226
229
  ]),
227
230
  lastUpdateSummary: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/anws",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Anws — A spec-driven workflow framework for AI-assisted development. Empowers prompt engineers to build production-ready software through structured PRD → Architecture → Task decomposition. Works with Claude Code, GitHub Copilot, Cursor, Windsurf, and any tool that reads AGENTS.md.",
5
5
  "keywords": [
6
6
  "anws",