@aipper/aiws 0.0.11 → 0.0.14

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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@aipper/aiws",
3
- "version": "0.0.11",
3
+ "version": "0.0.14",
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.11"
10
+ "@aipper/aiws-spec": "0.0.14"
11
11
  },
12
12
  "files": [
13
13
  "bin",
@@ -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 { UserError } from "../errors.js";
@@ -16,6 +17,11 @@ export async function codexInstallSkillsCommand(options) {
16
17
  const skillsDir = resolveCodexSkillsDir(options.skillsDir);
17
18
  const dryRun = options.dryRun === true;
18
19
  if (!dryRun) await ensureDir(skillsDir);
20
+ const skillsDirIsSymlink = await fs
21
+ .lstat(skillsDir)
22
+ .then((st) => st.isSymbolicLink())
23
+ .catch(() => false);
24
+ const skillsDirReal = skillsDirIsSymlink ? await fs.realpath(skillsDir).catch(() => skillsDir) : skillsDir;
19
25
 
20
26
  const skillFiles = await listTemplateCodexSkills(tpl);
21
27
 
@@ -60,9 +66,9 @@ export async function codexInstallSkillsCommand(options) {
60
66
  }
61
67
  }
62
68
 
63
- console.log(`${dryRun ? "✓ (dry-run)" : "✓"} aiws codex install-skills: ${skillsDir}`);
69
+ const header = skillsDirIsSymlink && skillsDirReal !== skillsDir ? `${skillsDir} -> ${skillsDirReal}` : skillsDir;
70
+ console.log(`${dryRun ? "✓ (dry-run)" : "✓"} aiws codex install-skills: ${header}`);
64
71
  if (created.length > 0) console.log(`created: ${created.join(", ")}`);
65
72
  if (updated.length > 0) console.log(`updated: ${updated.join(", ")}`);
66
73
  if (overwritten.length > 0) console.log(`overwritten: ${overwritten.join(", ")}`);
67
74
  }
68
-
@@ -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 { normalizeNewlines } from "../hash.js";
@@ -13,6 +14,11 @@ export async function codexStatusSkillsCommand(options) {
13
14
  const tpl = await loadTemplate(templateId);
14
15
 
15
16
  const skillsDir = resolveCodexSkillsDir(options.skillsDir);
17
+ const skillsDirIsSymlink = await fs
18
+ .lstat(skillsDir)
19
+ .then((st) => st.isSymbolicLink())
20
+ .catch(() => false);
21
+ const skillsDirReal = skillsDirIsSymlink ? await fs.realpath(skillsDir).catch(() => skillsDir) : skillsDir;
16
22
  const skillFiles = await listTemplateCodexSkills(tpl);
17
23
 
18
24
  /** @type {Array<{ name: string, status: "ok" | "missing" | "unmanaged" | "outdated" }>} */
@@ -42,13 +48,16 @@ export async function codexStatusSkillsCommand(options) {
42
48
  /** @type {{ ok: number, missing: number, unmanaged: number, outdated: number }} */ ({ ok: 0, missing: 0, unmanaged: 0, outdated: 0 }),
43
49
  );
44
50
 
45
- console.log(`✓ aiws codex status-skills: ${skillsDir}`);
51
+ const header = skillsDirIsSymlink && skillsDirReal !== skillsDir ? `${skillsDir} -> ${skillsDirReal}` : skillsDir;
52
+ console.log(`✓ aiws codex status-skills: ${header}`);
46
53
  console.log(`ok=${counts.ok} missing=${counts.missing} unmanaged=${counts.unmanaged} outdated=${counts.outdated}`);
47
54
  for (const r of rows) {
48
55
  console.log(`${r.status}\t${r.name}`);
49
56
  }
57
+ if (counts.ok === 0 && counts.missing === rows.length && skillsDirIsSymlink) {
58
+ console.log("Note: skills dir is a symlink; if you installed to a different dir, pass --dir (or set CODEX_HOME).");
59
+ }
50
60
  if (counts.missing > 0 || counts.outdated > 0) {
51
61
  console.log("Next: aiws codex install-skills");
52
62
  }
53
63
  }
54
-
@@ -8,6 +8,70 @@ import { runCommand } from "../exec.js";
8
8
  import { expandManifestEntries } from "../template.js";
9
9
  import { loadAiwsPackage } from "../aiws-package.js";
10
10
 
11
+ /**
12
+ * Enforce deterministic submodule branch policy:
13
+ * - If `.gitmodules` exists and declares submodules, each submodule must declare `submodule.<name>.branch`.
14
+ * - This avoids guessing which branch to attach/push for workflows like ws-pull/ws-finish.
15
+ *
16
+ * @param {string} workspaceRoot
17
+ */
18
+ async function validateSubmoduleBranchPolicy(workspaceRoot) {
19
+ const gitmodules = path.join(workspaceRoot, ".gitmodules");
20
+ if (!(await pathExists(gitmodules))) return;
21
+
22
+ const list = await runCommand("git", ["config", "--file", ".gitmodules", "--get-regexp", "^submodule\\..*\\.path$"], {
23
+ cwd: workspaceRoot,
24
+ });
25
+ if (list.code !== 0) {
26
+ // `.gitmodules` exists but no submodule path entries: treat as ok.
27
+ return;
28
+ }
29
+
30
+ /** @type {Array<{ name: string, path: string }>} */
31
+ const subs = [];
32
+ for (const line of String(list.stdout || "").split("\n")) {
33
+ const t = line.trim();
34
+ if (!t) continue;
35
+ const idx = t.indexOf(" ");
36
+ if (idx <= 0) continue;
37
+ const key = t.slice(0, idx).trim();
38
+ const subPath = t.slice(idx + 1).trim();
39
+ const m = key.match(/^submodule\.([^.]+)\.path$/);
40
+ if (!m) continue;
41
+ const name = m[1] || "";
42
+ if (!name || !subPath) continue;
43
+ subs.push({ name, path: subPath });
44
+ }
45
+ if (subs.length === 0) return;
46
+
47
+ /** @type {Array<{ name: string, path: string }>} */
48
+ const missing = [];
49
+ for (const s of subs) {
50
+ const br = await runCommand("git", ["config", "--file", ".gitmodules", "--get", `submodule.${s.name}.branch`], {
51
+ cwd: workspaceRoot,
52
+ });
53
+ if (br.code !== 0 || !String(br.stdout || "").trim()) {
54
+ missing.push(s);
55
+ }
56
+ }
57
+
58
+ if (missing.length > 0) {
59
+ const lines = missing.map((m) => `- ${m.name} (${m.path}): missing submodule.${m.name}.branch`);
60
+ const hints = missing
61
+ .slice(0, 8)
62
+ .map((m) => `git submodule set-branch --branch main ${m.path}`)
63
+ .join("\n");
64
+ throw new UserError("Submodule branch policy failed: missing `.gitmodules` branch config.", {
65
+ details:
66
+ `${lines.join("\n")}\n\n` +
67
+ "Fix:\n" +
68
+ "- Run `ws-submodule-setup` (recommended), or set per submodule:\n" +
69
+ `${hints}\n\n` +
70
+ "Then commit `.gitmodules` in the superproject.",
71
+ });
72
+ }
73
+ }
74
+
11
75
  /**
12
76
  * @param {string | undefined} v
13
77
  */
@@ -100,6 +164,9 @@ export async function validateCommand(options) {
100
164
  // Drift detection.
101
165
  await validateDrift({ workspaceRoot, storedManifest: stored, templateManifest: tpl.manifest });
102
166
 
167
+ // Submodule branch policy (deterministic workflow; avoids guessing target branches).
168
+ await validateSubmoduleBranchPolicy(workspaceRoot);
169
+
103
170
  // python3 gate.
104
171
  const py = await runCommand("python3", ["--version"], { cwd: workspaceRoot });
105
172
  if (py.code !== 0) {