@aipper/aiws 0.0.21 → 0.0.23

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
@@ -47,7 +47,7 @@ aiws codex <install-skills|status-skills|uninstall-skills|install-prompts|status
47
47
 
48
48
  ## Codex(repo skills 优先)
49
49
 
50
- - `aiws init .` 会生成 `.agents/skills/`(随仓库共享),在 Codex 中可显式调用(示例):`$ws-preflight` / `$ws-plan` / `$ws-dev` / `$ws-review` / `$ws-commit`
50
+ - `aiws init .` 会生成 `.agents/skills/`(随仓库共享),在 Codex 中可显式调用(示例):`$ws-preflight` / `$ws-plan` / `$p-tasks-plan` / `$ws-handoff` / `$ws-dev` / `$ws-review` / `$ws-commit`
51
51
  - 交付收尾(submodules+superproject 分步提交 + 安全合并):`$ws-deliver`
52
52
  - 收尾(安全合并回目标分支):`$ws-finish`(底层调用 `aiws change finish`,默认 fast-forward)
53
53
  - 可选:安装全局 skills 到 `~/.codex/skills/`(或 `$CODEX_HOME/skills`):
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@aipper/aiws",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "AI Workspace CLI (init/update/validate) for Claude Code / OpenCode / Codex / iFlow.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "aiws": "./bin/aiws.js"
8
8
  },
9
9
  "dependencies": {
10
- "@aipper/aiws-spec": "0.0.21"
10
+ "@aipper/aiws-spec": "0.0.23"
11
11
  },
12
12
  "files": [
13
13
  "bin",
package/src/cli.js CHANGED
@@ -260,6 +260,7 @@ export async function cliMain(argv) {
260
260
  hooks: { type: "boolean" },
261
261
  "no-switch": { type: "boolean" },
262
262
  switch: { type: "boolean" },
263
+ "allow-dirty": { type: "boolean" },
263
264
  worktree: { type: "boolean" },
264
265
  "worktree-dir": { type: "string" },
265
266
  submodules: { type: "boolean" },
@@ -268,7 +269,7 @@ export async function cliMain(argv) {
268
269
  if (!changeId)
269
270
  throw new UserError("change start requires <change-id>", {
270
271
  details:
271
- "Usage: aiws change start <change-id> [--title <title>] [--no-design] [--hooks] [--no-switch] [--switch] [--worktree] [--worktree-dir <path>] [--submodules]",
272
+ "Usage: aiws change start <change-id> [--title <title>] [--no-design] [--hooks] [--no-switch] [--switch] [--allow-dirty] [--worktree] [--worktree-dir <path>] [--submodules]",
272
273
  });
273
274
  await changeStartCommand({
274
275
  changeId,
@@ -277,6 +278,7 @@ export async function cliMain(argv) {
277
278
  enableHooks: options.hooks === true,
278
279
  noSwitch: options["no-switch"] === true,
279
280
  forceSwitch: options.switch === true,
281
+ allowDirty: options["allow-dirty"] === true,
280
282
  worktree: options.worktree === true,
281
283
  worktreeDir: options["worktree-dir"],
282
284
  submodules: options.submodules === true,
@@ -490,7 +492,7 @@ function printChangeHelp() {
490
492
 
491
493
  Usage:
492
494
  aiws change list
493
- aiws change start <change-id> [--title <title>] [--no-design] [--hooks] [--no-switch] [--switch] [--worktree] [--worktree-dir <path>] [--submodules]
495
+ aiws change start <change-id> [--title <title>] [--no-design] [--hooks] [--no-switch] [--switch] [--allow-dirty] [--worktree] [--worktree-dir <path>]
494
496
  aiws change finish [change-id] [--into <branch> | --base <branch>]
495
497
  aiws change new <change-id> [--title <title>] [--no-design]
496
498
  aiws change status [change-id]
@@ -510,6 +512,7 @@ Notes:
510
512
  - Execution quality gate first: aiws change validate [change-id] --strict
511
513
  (equivalent to running $ws-plan-verify in AI tools).
512
514
  - status/next now prioritize quality-gate guidance before coding ($ws-dev).
515
+ - start refuses to switch/create worktree with a dirty working tree (unless --allow-dirty).
513
516
  - If .gitmodules exists and you didn't specify --switch/--no-switch/--worktree,
514
517
  start will prefer --worktree (fallback: --no-switch) to avoid switching the superproject branch.
515
518
  - archive runs strict validation and (by default) requires all tasks checked.
@@ -1417,7 +1417,7 @@ export async function changeNewCommand(options) {
1417
1417
  /**
1418
1418
  * aiws change start
1419
1419
  *
1420
- * @param {{ changeId: string, title?: string, noDesign: boolean, enableHooks: boolean, noSwitch: boolean, forceSwitch: boolean, worktree: boolean, worktreeDir?: string, submodules: boolean }} options
1420
+ * @param {{ changeId: string, title?: string, noDesign: boolean, enableHooks: boolean, noSwitch: boolean, forceSwitch: boolean, allowDirty?: boolean, worktree: boolean, worktreeDir?: string, submodules: boolean }} options
1421
1421
  */
1422
1422
  export async function changeStartCommand(options) {
1423
1423
  const gitRoot = await resolveGitRoot(process.cwd());
@@ -1429,6 +1429,7 @@ export async function changeStartCommand(options) {
1429
1429
  const startFromBranch = await currentBranch(gitRoot);
1430
1430
  const branch = `change/${changeId}`;
1431
1431
  const branchRef = `refs/heads/${branch}`;
1432
+ const allowDirty = options.allowDirty === true;
1432
1433
 
1433
1434
  if (options.worktree === true && options.noSwitch === true) {
1434
1435
  throw new UserError("change start: cannot combine --worktree with --no-switch");
@@ -1468,6 +1469,24 @@ export async function changeStartCommand(options) {
1468
1469
  console.error("warn: recommended for submodules: aiws change start <change-id> --worktree");
1469
1470
  }
1470
1471
 
1472
+ if (!allowDirty && (effectiveWorktree || !effectiveNoSwitch)) {
1473
+ const clean = await checkGitClean(gitRoot);
1474
+ if (!clean.clean) {
1475
+ const mode = effectiveWorktree ? "worktree" : "switch";
1476
+ throw new UserError(`Refusing to start change with a dirty working tree (${mode} mode).`, {
1477
+ details:
1478
+ `${clean.details}\n\n` +
1479
+ `Why: starting in ${mode} mode changes your checkout context; uncommitted changes may not appear where you expect.\n` +
1480
+ "Fix: commit first, then retry:\n" +
1481
+ " git add -A && git commit -m \"wip: save before change start\"\n" +
1482
+ "Fix: or stash:\n" +
1483
+ " git stash\n" +
1484
+ `Alt: prepare artifacts without switching: aiws change start ${changeId} --no-switch\n` +
1485
+ "Alt: proceed anyway (not recommended): add --allow-dirty",
1486
+ });
1487
+ }
1488
+ }
1489
+
1471
1490
  if (effectiveWorktree) {
1472
1491
  const prereq = await checkWorktreePrereqs(gitRoot);
1473
1492
  if (!prereq.ok) {
@@ -1545,12 +1564,30 @@ export async function changeStartCommand(options) {
1545
1564
  await maybeRecordBaseBranch(changeDir, startFromBranch, branch);
1546
1565
  }
1547
1566
 
1548
- if (options.submodules === true) {
1549
- const sm = await runCommand("git", ["submodule", "update", "--init", "--recursive"], { cwd: destKey });
1550
- if (sm.code !== 0) throw new UserError("Failed to init/update submodules in worktree.", { details: sm.stderr || sm.stdout });
1551
- } else if (hasGitmodules) {
1552
- console.error("warn: .gitmodules detected; new worktree may have empty submodule dirs.");
1553
- console.error("warn: consider running in the worktree: git submodule update --init --recursive (or re-run with --submodules)");
1567
+ // Worktree mode: if this repo declares submodules, always initialize them in the new worktree.
1568
+ // --submodules is now implicit (kept for backwards compat but has no effect).
1569
+ if (hasGitmodules) {
1570
+ const hasEntries = await runCommand("git", ["config", "--file", ".gitmodules", "--get-regexp", "^submodule\\..*\\.path$"], { cwd: destKey }).then(
1571
+ (r) => r.code === 0 && String(r.stdout || "").trim().length > 0
1572
+ );
1573
+ if (hasEntries) {
1574
+ console.error("info: .gitmodules detected; initializing submodules in the new worktree.");
1575
+ const sm = await runCommand("git", ["submodule", "update", "--init", "--recursive"], { cwd: destKey });
1576
+ if (sm.code !== 0) throw new UserError("Failed to init/update submodules in worktree.", { details: sm.stderr || sm.stdout });
1577
+
1578
+ // Sanity check: fail fast if anything remains uninitialized or conflicted.
1579
+ const st = await runCommand("git", ["submodule", "status", "--recursive"], { cwd: destKey });
1580
+ const lines = String(st.stdout || "")
1581
+ .split("\n")
1582
+ .map((l) => l.trimEnd())
1583
+ .filter(Boolean);
1584
+ const bad = lines.filter((l) => l.startsWith("-") || l.startsWith("U"));
1585
+ if (bad.length > 0) {
1586
+ throw new UserError("Submodules not fully initialized in worktree.", {
1587
+ details: `${bad.slice(0, 20).join("\n")}${bad.length > 20 ? "\n..." : ""}\n\nHint: run in the worktree:\n git submodule update --init --recursive`,
1588
+ });
1589
+ }
1590
+ }
1554
1591
  }
1555
1592
 
1556
1593
  if (options.enableHooks) {
@@ -1562,7 +1599,7 @@ export async function changeStartCommand(options) {
1562
1599
  worktree: destKey,
1563
1600
  branch,
1564
1601
  from_branch: startFromBranch || "",
1565
- submodules: options.submodules === true,
1602
+ submodules: true,
1566
1603
  hooks: options.enableHooks === true,
1567
1604
  });
1568
1605
 
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs/promises";
1
2
  import path from "node:path";
2
3
  import { loadTemplate } from "../spec.js";
3
4
  import { loadAiwsPackage } from "../aiws-package.js";
@@ -59,13 +60,32 @@ export async function updateCommand(options) {
59
60
  }
60
61
  }
61
62
 
63
+ const removeFiles = (update.remove || []).map(normalizeRel);
64
+
62
65
  const backup = new BackupSession({ workspaceRoot, operation: "update" });
63
66
  // Backup every file we might touch (including missing, for rollback deletions).
64
67
  await backup.recordFile(".aiws/manifest.json", { recordMissing: true });
65
68
  for (const f of replaceFiles) await backup.recordFile(f, { recordMissing: true });
66
69
  for (const f of Object.keys(managedBlocks)) await backup.recordFile(f, { recordMissing: true });
70
+ for (const f of removeFiles) await backup.recordFile(f);
67
71
  await backup.finalize({ extra: { template_id: tpl.templateId } });
68
72
 
73
+ // Remove deprecated files (backed up above; restorable via `aiws rollback`).
74
+ let removedCount = 0;
75
+ for (const rel of removeFiles) {
76
+ if (!rel) continue;
77
+ const abs = joinRel(workspaceRoot, rel);
78
+ if (await pathExists(abs)) {
79
+ await fs.rm(abs, { force: true });
80
+ removedCount++;
81
+ // Remove empty parent directory (safe: rmdir fails on non-empty).
82
+ try { await fs.rmdir(path.dirname(abs)); } catch { /* non-empty or gone */ }
83
+ }
84
+ }
85
+ if (removedCount > 0) {
86
+ console.log(` removed ${removedCount} deprecated file(s)`);
87
+ }
88
+
69
89
  // Replace files (except manifest, generated later).
70
90
  for (const rel of replaceFiles) {
71
91
  if (!rel || rel === ".aiws/manifest.json") continue;