@aipper/aiws 0.0.1
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 +54 -0
- package/bin/aiws.js +18 -0
- package/package.json +24 -0
- package/src/aiws-package.js +15 -0
- package/src/backup.js +149 -0
- package/src/cli.js +470 -0
- package/src/codex-prompts.js +74 -0
- package/src/codex-skills.js +111 -0
- package/src/commands/change.js +987 -0
- package/src/commands/codex-install-prompts.js +68 -0
- package/src/commands/codex-install-skills.js +68 -0
- package/src/commands/codex-status-prompts.js +55 -0
- package/src/commands/codex-status-skills.js +54 -0
- package/src/commands/codex-uninstall-prompts.js +55 -0
- package/src/commands/codex-uninstall-skills.js +62 -0
- package/src/commands/hooks-install.js +93 -0
- package/src/commands/hooks-status.js +87 -0
- package/src/commands/init.js +93 -0
- package/src/commands/rollback.js +13 -0
- package/src/commands/update.js +98 -0
- package/src/commands/validate.js +155 -0
- package/src/errors.js +15 -0
- package/src/exec.js +34 -0
- package/src/fs.js +91 -0
- package/src/hash.js +25 -0
- package/src/managed-blocks.js +131 -0
- package/src/manifest.js +153 -0
- package/src/path-utils.js +20 -0
- package/src/spec.js +64 -0
- package/src/template.js +107 -0
- package/src/workspace.js +23 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadTemplate } from "../spec.js";
|
|
3
|
+
import { UserError } from "../errors.js";
|
|
4
|
+
import { copyFile, ensureDir, pathExists, readText, writeText } from "../fs.js";
|
|
5
|
+
import { normalizeNewlines } from "../hash.js";
|
|
6
|
+
import { findManagedBlock, upsertManagedBlock } from "../managed-blocks.js";
|
|
7
|
+
import { codexBackupPathFor, codexBackupStamp, listTemplateCodexPrompts, resolveCodexPromptsDir } from "../codex-prompts.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ templateId: string, promptsDir?: string, force?: boolean, dryRun?: boolean }} options
|
|
11
|
+
*/
|
|
12
|
+
export async function codexInstallPromptsCommand(options) {
|
|
13
|
+
const templateId = String(options.templateId || "workspace");
|
|
14
|
+
const tpl = await loadTemplate(templateId);
|
|
15
|
+
|
|
16
|
+
const promptsDir = resolveCodexPromptsDir(options.promptsDir);
|
|
17
|
+
const dryRun = options.dryRun === true;
|
|
18
|
+
if (!dryRun) await ensureDir(promptsDir);
|
|
19
|
+
|
|
20
|
+
const promptFiles = await listTemplateCodexPrompts(tpl);
|
|
21
|
+
|
|
22
|
+
/** @type {string[]} */
|
|
23
|
+
const created = [];
|
|
24
|
+
/** @type {string[]} */
|
|
25
|
+
const updated = [];
|
|
26
|
+
/** @type {string[]} */
|
|
27
|
+
const overwritten = [];
|
|
28
|
+
let backupStamp = "";
|
|
29
|
+
|
|
30
|
+
for (const pf of promptFiles) {
|
|
31
|
+
const { filename, blockId, templateText, innerText } = pf;
|
|
32
|
+
const destAbs = path.join(promptsDir, filename);
|
|
33
|
+
if (!(await pathExists(destAbs))) {
|
|
34
|
+
if (!dryRun) await writeText(destAbs, templateText);
|
|
35
|
+
created.push(filename);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const currentText = normalizeNewlines(await readText(destAbs));
|
|
40
|
+
const existingBlock = findManagedBlock(currentText, blockId);
|
|
41
|
+
if (!existingBlock) {
|
|
42
|
+
if (!options.force) {
|
|
43
|
+
throw new UserError("Codex prompt file exists and is not AIWS-managed; refusing to overwrite.", {
|
|
44
|
+
details: `File: ${destAbs}\nHint: re-run with --force to overwrite (a backup will be created).`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (!backupStamp) backupStamp = codexBackupStamp();
|
|
48
|
+
const backup = codexBackupPathFor(promptsDir, filename, backupStamp);
|
|
49
|
+
if (!dryRun) {
|
|
50
|
+
await copyFile(destAbs, backup);
|
|
51
|
+
await writeText(destAbs, templateText);
|
|
52
|
+
}
|
|
53
|
+
overwritten.push(filename);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const res = upsertManagedBlock(currentText, blockId, innerText, { insertIfMissing: false });
|
|
58
|
+
if (res.changed) {
|
|
59
|
+
if (!dryRun) await writeText(destAbs, res.nextText);
|
|
60
|
+
updated.push(filename);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`${dryRun ? "✓ (dry-run)" : "✓"} aiws codex install-prompts: ${promptsDir}`);
|
|
65
|
+
if (created.length > 0) console.log(`created: ${created.join(", ")}`);
|
|
66
|
+
if (updated.length > 0) console.log(`updated: ${updated.join(", ")}`);
|
|
67
|
+
if (overwritten.length > 0) console.log(`overwritten: ${overwritten.join(", ")}`);
|
|
68
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadTemplate } from "../spec.js";
|
|
3
|
+
import { UserError } from "../errors.js";
|
|
4
|
+
import { copyFile, ensureDir, pathExists, readText, writeText } from "../fs.js";
|
|
5
|
+
import { normalizeNewlines } from "../hash.js";
|
|
6
|
+
import { findManagedBlock, upsertManagedBlock } from "../managed-blocks.js";
|
|
7
|
+
import { codexSkillsBackupPathFor, codexSkillsBackupStamp, listTemplateCodexSkills, resolveCodexSkillsDir } from "../codex-skills.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ templateId: string, skillsDir?: string, force?: boolean, dryRun?: boolean }} options
|
|
11
|
+
*/
|
|
12
|
+
export async function codexInstallSkillsCommand(options) {
|
|
13
|
+
const templateId = String(options.templateId || "workspace");
|
|
14
|
+
const tpl = await loadTemplate(templateId);
|
|
15
|
+
|
|
16
|
+
const skillsDir = resolveCodexSkillsDir(options.skillsDir);
|
|
17
|
+
const dryRun = options.dryRun === true;
|
|
18
|
+
if (!dryRun) await ensureDir(skillsDir);
|
|
19
|
+
|
|
20
|
+
const skillFiles = await listTemplateCodexSkills(tpl);
|
|
21
|
+
|
|
22
|
+
/** @type {string[]} */
|
|
23
|
+
const created = [];
|
|
24
|
+
/** @type {string[]} */
|
|
25
|
+
const updated = [];
|
|
26
|
+
/** @type {string[]} */
|
|
27
|
+
const overwritten = [];
|
|
28
|
+
let backupStamp = "";
|
|
29
|
+
|
|
30
|
+
for (const sf of skillFiles) {
|
|
31
|
+
const destAbs = path.join(skillsDir, sf.skillName, "SKILL.md");
|
|
32
|
+
if (!(await pathExists(destAbs))) {
|
|
33
|
+
if (!dryRun) await writeText(destAbs, sf.managedText);
|
|
34
|
+
created.push(sf.skillName);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const currentText = normalizeNewlines(await readText(destAbs));
|
|
39
|
+
const existingBlock = findManagedBlock(currentText, sf.blockId);
|
|
40
|
+
if (!existingBlock) {
|
|
41
|
+
if (!options.force) {
|
|
42
|
+
throw new UserError("Codex skill exists and is not AIWS-managed; refusing to overwrite.", {
|
|
43
|
+
details: `File: ${destAbs}\nHint: re-run with --force to overwrite (a backup will be created).`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (!backupStamp) backupStamp = codexSkillsBackupStamp();
|
|
47
|
+
const backup = codexSkillsBackupPathFor(skillsDir, sf.skillName, backupStamp);
|
|
48
|
+
if (!dryRun) {
|
|
49
|
+
await copyFile(destAbs, backup);
|
|
50
|
+
await writeText(destAbs, sf.managedText);
|
|
51
|
+
}
|
|
52
|
+
overwritten.push(sf.skillName);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const res = upsertManagedBlock(currentText, sf.blockId, sf.innerText, { insertIfMissing: false });
|
|
57
|
+
if (res.changed) {
|
|
58
|
+
if (!dryRun) await writeText(destAbs, res.nextText);
|
|
59
|
+
updated.push(sf.skillName);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`${dryRun ? "✓ (dry-run)" : "✓"} aiws codex install-skills: ${skillsDir}`);
|
|
64
|
+
if (created.length > 0) console.log(`created: ${created.join(", ")}`);
|
|
65
|
+
if (updated.length > 0) console.log(`updated: ${updated.join(", ")}`);
|
|
66
|
+
if (overwritten.length > 0) console.log(`overwritten: ${overwritten.join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadTemplate } from "../spec.js";
|
|
3
|
+
import { normalizeNewlines } from "../hash.js";
|
|
4
|
+
import { findManagedBlock } from "../managed-blocks.js";
|
|
5
|
+
import { pathExists, readText } from "../fs.js";
|
|
6
|
+
import { listTemplateCodexPrompts, resolveCodexPromptsDir } from "../codex-prompts.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {{ templateId: string, promptsDir?: string }} options
|
|
10
|
+
*/
|
|
11
|
+
export async function codexStatusPromptsCommand(options) {
|
|
12
|
+
const templateId = String(options.templateId || "workspace");
|
|
13
|
+
const tpl = await loadTemplate(templateId);
|
|
14
|
+
|
|
15
|
+
const promptsDir = resolveCodexPromptsDir(options.promptsDir);
|
|
16
|
+
const promptFiles = await listTemplateCodexPrompts(tpl);
|
|
17
|
+
|
|
18
|
+
/** @type {Array<{ filename: string, status: "ok" | "missing" | "unmanaged" | "outdated" }>} */
|
|
19
|
+
const rows = [];
|
|
20
|
+
|
|
21
|
+
for (const pf of promptFiles) {
|
|
22
|
+
const destAbs = path.join(promptsDir, pf.filename);
|
|
23
|
+
if (!(await pathExists(destAbs))) {
|
|
24
|
+
rows.push({ filename: pf.filename, status: "missing" });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const currentText = normalizeNewlines(await readText(destAbs));
|
|
29
|
+
const block = findManagedBlock(currentText, pf.blockId);
|
|
30
|
+
if (!block) {
|
|
31
|
+
rows.push({ filename: pf.filename, status: "unmanaged" });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
rows.push({ filename: pf.filename, status: block.innerText === pf.innerText ? "ok" : "outdated" });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const counts = rows.reduce(
|
|
39
|
+
(acc, r) => {
|
|
40
|
+
acc[r.status]++;
|
|
41
|
+
return acc;
|
|
42
|
+
},
|
|
43
|
+
/** @type {{ ok: number, missing: number, unmanaged: number, outdated: number }} */ ({ ok: 0, missing: 0, unmanaged: 0, outdated: 0 }),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
console.log(`✓ aiws codex status: ${promptsDir}`);
|
|
47
|
+
console.log(`ok=${counts.ok} missing=${counts.missing} unmanaged=${counts.unmanaged} outdated=${counts.outdated}`);
|
|
48
|
+
for (const r of rows) {
|
|
49
|
+
console.log(`${r.status}\t${r.filename}`);
|
|
50
|
+
}
|
|
51
|
+
if (counts.missing > 0 || counts.outdated > 0) {
|
|
52
|
+
console.log("Next: aiws codex install-prompts");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadTemplate } from "../spec.js";
|
|
3
|
+
import { normalizeNewlines } from "../hash.js";
|
|
4
|
+
import { findManagedBlock } from "../managed-blocks.js";
|
|
5
|
+
import { pathExists, readText } from "../fs.js";
|
|
6
|
+
import { listTemplateCodexSkills, resolveCodexSkillsDir } from "../codex-skills.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {{ templateId: string, skillsDir?: string }} options
|
|
10
|
+
*/
|
|
11
|
+
export async function codexStatusSkillsCommand(options) {
|
|
12
|
+
const templateId = String(options.templateId || "workspace");
|
|
13
|
+
const tpl = await loadTemplate(templateId);
|
|
14
|
+
|
|
15
|
+
const skillsDir = resolveCodexSkillsDir(options.skillsDir);
|
|
16
|
+
const skillFiles = await listTemplateCodexSkills(tpl);
|
|
17
|
+
|
|
18
|
+
/** @type {Array<{ name: string, status: "ok" | "missing" | "unmanaged" | "outdated" }>} */
|
|
19
|
+
const rows = [];
|
|
20
|
+
|
|
21
|
+
for (const sf of skillFiles) {
|
|
22
|
+
const destAbs = path.join(skillsDir, sf.skillName, "SKILL.md");
|
|
23
|
+
if (!(await pathExists(destAbs))) {
|
|
24
|
+
rows.push({ name: sf.skillName, status: "missing" });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const currentText = normalizeNewlines(await readText(destAbs));
|
|
29
|
+
const block = findManagedBlock(currentText, sf.blockId);
|
|
30
|
+
if (!block) {
|
|
31
|
+
rows.push({ name: sf.skillName, status: "unmanaged" });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
rows.push({ name: sf.skillName, status: block.innerText === sf.innerText ? "ok" : "outdated" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const counts = rows.reduce(
|
|
38
|
+
(acc, r) => {
|
|
39
|
+
acc[r.status]++;
|
|
40
|
+
return acc;
|
|
41
|
+
},
|
|
42
|
+
/** @type {{ ok: number, missing: number, unmanaged: number, outdated: number }} */ ({ ok: 0, missing: 0, unmanaged: 0, outdated: 0 }),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
console.log(`✓ aiws codex status-skills: ${skillsDir}`);
|
|
46
|
+
console.log(`ok=${counts.ok} missing=${counts.missing} unmanaged=${counts.unmanaged} outdated=${counts.outdated}`);
|
|
47
|
+
for (const r of rows) {
|
|
48
|
+
console.log(`${r.status}\t${r.name}`);
|
|
49
|
+
}
|
|
50
|
+
if (counts.missing > 0 || counts.outdated > 0) {
|
|
51
|
+
console.log("Next: aiws codex install-skills");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { loadTemplate } from "../spec.js";
|
|
4
|
+
import { normalizeNewlines } from "../hash.js";
|
|
5
|
+
import { findManagedBlock } from "../managed-blocks.js";
|
|
6
|
+
import { copyFile, pathExists, readText } from "../fs.js";
|
|
7
|
+
import { codexBackupPathFor, codexBackupStamp, listTemplateCodexPrompts, resolveCodexPromptsDir } from "../codex-prompts.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ templateId: string, promptsDir?: string }} options
|
|
11
|
+
*/
|
|
12
|
+
export async function codexUninstallPromptsCommand(options) {
|
|
13
|
+
const templateId = String(options.templateId || "workspace");
|
|
14
|
+
const tpl = await loadTemplate(templateId);
|
|
15
|
+
|
|
16
|
+
const promptsDir = resolveCodexPromptsDir(options.promptsDir);
|
|
17
|
+
const promptFiles = await listTemplateCodexPrompts(tpl);
|
|
18
|
+
|
|
19
|
+
const backupStamp = codexBackupStamp();
|
|
20
|
+
const backupDir = path.join(promptsDir, ".aiws", "backups", "codex-prompts", backupStamp);
|
|
21
|
+
|
|
22
|
+
/** @type {string[]} */
|
|
23
|
+
const removed = [];
|
|
24
|
+
/** @type {string[]} */
|
|
25
|
+
const skippedUnmanaged = [];
|
|
26
|
+
/** @type {string[]} */
|
|
27
|
+
const missing = [];
|
|
28
|
+
|
|
29
|
+
for (const pf of promptFiles) {
|
|
30
|
+
const destAbs = path.join(promptsDir, pf.filename);
|
|
31
|
+
if (!(await pathExists(destAbs))) {
|
|
32
|
+
missing.push(pf.filename);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const currentText = normalizeNewlines(await readText(destAbs));
|
|
37
|
+
const block = findManagedBlock(currentText, pf.blockId);
|
|
38
|
+
if (!block) {
|
|
39
|
+
skippedUnmanaged.push(pf.filename);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const backup = codexBackupPathFor(promptsDir, pf.filename, backupStamp);
|
|
44
|
+
await copyFile(destAbs, backup);
|
|
45
|
+
await fs.unlink(destAbs);
|
|
46
|
+
removed.push(pf.filename);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`✓ aiws codex uninstall-prompts: ${promptsDir}`);
|
|
50
|
+
if (removed.length > 0) console.log(`removed: ${removed.join(", ")}`);
|
|
51
|
+
if (removed.length > 0) console.log(`backup: ${backupDir}`);
|
|
52
|
+
if (skippedUnmanaged.length > 0) console.log(`skipped (unmanaged): ${skippedUnmanaged.join(", ")}`);
|
|
53
|
+
if (missing.length > 0) console.log(`missing: ${missing.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { loadTemplate } from "../spec.js";
|
|
4
|
+
import { normalizeNewlines } from "../hash.js";
|
|
5
|
+
import { findManagedBlock } from "../managed-blocks.js";
|
|
6
|
+
import { copyFile, pathExists, readText } from "../fs.js";
|
|
7
|
+
import { codexSkillsBackupPathFor, codexSkillsBackupStamp, listTemplateCodexSkills, resolveCodexSkillsDir } from "../codex-skills.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ templateId: string, skillsDir?: string }} options
|
|
11
|
+
*/
|
|
12
|
+
export async function codexUninstallSkillsCommand(options) {
|
|
13
|
+
const templateId = String(options.templateId || "workspace");
|
|
14
|
+
const tpl = await loadTemplate(templateId);
|
|
15
|
+
|
|
16
|
+
const skillsDir = resolveCodexSkillsDir(options.skillsDir);
|
|
17
|
+
const skillFiles = await listTemplateCodexSkills(tpl);
|
|
18
|
+
|
|
19
|
+
const backupStamp = codexSkillsBackupStamp();
|
|
20
|
+
const backupDir = path.join(skillsDir, ".aiws", "backups", "codex-skills", backupStamp);
|
|
21
|
+
|
|
22
|
+
/** @type {string[]} */
|
|
23
|
+
const removed = [];
|
|
24
|
+
/** @type {string[]} */
|
|
25
|
+
const skippedUnmanaged = [];
|
|
26
|
+
/** @type {string[]} */
|
|
27
|
+
const missing = [];
|
|
28
|
+
|
|
29
|
+
for (const sf of skillFiles) {
|
|
30
|
+
const destDir = path.join(skillsDir, sf.skillName);
|
|
31
|
+
const destAbs = path.join(destDir, "SKILL.md");
|
|
32
|
+
if (!(await pathExists(destAbs))) {
|
|
33
|
+
missing.push(sf.skillName);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const currentText = normalizeNewlines(await readText(destAbs));
|
|
38
|
+
const block = findManagedBlock(currentText, sf.blockId);
|
|
39
|
+
if (!block) {
|
|
40
|
+
skippedUnmanaged.push(sf.skillName);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const backup = codexSkillsBackupPathFor(skillsDir, sf.skillName, backupStamp);
|
|
45
|
+
await copyFile(destAbs, backup);
|
|
46
|
+
await fs.unlink(destAbs);
|
|
47
|
+
// Only remove the directory if it's empty after removing SKILL.md.
|
|
48
|
+
try {
|
|
49
|
+
await fs.rmdir(destDir);
|
|
50
|
+
} catch {
|
|
51
|
+
// ignore
|
|
52
|
+
}
|
|
53
|
+
removed.push(sf.skillName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(`✓ aiws codex uninstall-skills: ${skillsDir}`);
|
|
57
|
+
if (removed.length > 0) console.log(`removed: ${removed.join(", ")}`);
|
|
58
|
+
if (removed.length > 0) console.log(`backup: ${backupDir}`);
|
|
59
|
+
if (skippedUnmanaged.length > 0) console.log(`skipped (unmanaged): ${skippedUnmanaged.join(", ")}`);
|
|
60
|
+
if (missing.length > 0) console.log(`missing: ${missing.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
4
|
+
import { runCommand } from "../exec.js";
|
|
5
|
+
import { pathExists, readText } from "../fs.js";
|
|
6
|
+
import { UserError } from "../errors.js";
|
|
7
|
+
import { loadTemplate } from "../spec.js";
|
|
8
|
+
import { copyTemplateFileToWorkspace } from "../template.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} absPath
|
|
12
|
+
*/
|
|
13
|
+
async function resolveGitRoot(absPath) {
|
|
14
|
+
let res;
|
|
15
|
+
try {
|
|
16
|
+
res = await runCommand("git", ["rev-parse", "--show-toplevel"], { cwd: absPath });
|
|
17
|
+
} catch (e) {
|
|
18
|
+
throw new UserError("git is required for hooks commands.", { details: e instanceof Error ? e.message : String(e) });
|
|
19
|
+
}
|
|
20
|
+
if (res.code !== 0) {
|
|
21
|
+
throw new UserError("Not a git repository.", { details: res.stderr || res.stdout });
|
|
22
|
+
}
|
|
23
|
+
const root = String(res.stdout || "").trim();
|
|
24
|
+
if (!root) throw new UserError("Failed to resolve git repository root.");
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {fs.Stats} st
|
|
30
|
+
*/
|
|
31
|
+
function isExecutable(st) {
|
|
32
|
+
return (st.mode & 0o111) !== 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve template id for a repo:
|
|
37
|
+
* - Prefer `.aiws/manifest.json` if present.
|
|
38
|
+
* - Else default to `workspace`.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} gitRoot
|
|
41
|
+
*/
|
|
42
|
+
async function resolveTemplateIdForRepo(gitRoot) {
|
|
43
|
+
const manifestPath = path.join(gitRoot, ".aiws", "manifest.json");
|
|
44
|
+
if (!(await pathExists(manifestPath))) return "workspace";
|
|
45
|
+
try {
|
|
46
|
+
const stored = JSON.parse(await readText(manifestPath));
|
|
47
|
+
const templateId = String(stored.template_id || "").trim();
|
|
48
|
+
return templateId || "workspace";
|
|
49
|
+
} catch {
|
|
50
|
+
return "workspace";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {{ targetPath: string }} options
|
|
56
|
+
*/
|
|
57
|
+
export async function hooksInstallCommand(options) {
|
|
58
|
+
const workspaceRoot = await resolveWorkspaceRoot(options.targetPath, { create: false });
|
|
59
|
+
const gitRoot = await resolveGitRoot(workspaceRoot);
|
|
60
|
+
|
|
61
|
+
const templateId = await resolveTemplateIdForRepo(gitRoot);
|
|
62
|
+
const tpl = await loadTemplate(templateId);
|
|
63
|
+
|
|
64
|
+
const hookFiles = [".githooks/pre-commit", ".githooks/pre-push"];
|
|
65
|
+
/** @type {string[]} */
|
|
66
|
+
const created = [];
|
|
67
|
+
/** @type {string[]} */
|
|
68
|
+
const chmodded = [];
|
|
69
|
+
|
|
70
|
+
for (const rel of hookFiles) {
|
|
71
|
+
const abs = path.join(gitRoot, ...rel.split("/"));
|
|
72
|
+
if (!(await pathExists(abs))) {
|
|
73
|
+
await copyTemplateFileToWorkspace({ templateDir: tpl.templateDir, workspaceRoot: gitRoot, relPosix: rel, chmod: 0o755 });
|
|
74
|
+
created.push(rel);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const st = await fs.stat(abs);
|
|
78
|
+
if (!isExecutable(st)) {
|
|
79
|
+
await fs.chmod(abs, 0o755);
|
|
80
|
+
chmodded.push(rel);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const setRes = await runCommand("git", ["config", "core.hooksPath", ".githooks"], { cwd: gitRoot });
|
|
85
|
+
if (setRes.code !== 0) {
|
|
86
|
+
throw new UserError("Failed to set git core.hooksPath.", { details: setRes.stderr || setRes.stdout });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`✓ aiws hooks install: ${gitRoot}`);
|
|
90
|
+
if (created.length > 0) console.log(`created: ${created.join(", ")}`);
|
|
91
|
+
if (chmodded.length > 0) console.log(`chmod: ${chmodded.join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
4
|
+
import { runCommand } from "../exec.js";
|
|
5
|
+
import { pathExists } from "../fs.js";
|
|
6
|
+
import { UserError } from "../errors.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} absPath
|
|
10
|
+
*/
|
|
11
|
+
async function resolveGitRoot(absPath) {
|
|
12
|
+
let res;
|
|
13
|
+
try {
|
|
14
|
+
res = await runCommand("git", ["rev-parse", "--show-toplevel"], { cwd: absPath });
|
|
15
|
+
} catch (e) {
|
|
16
|
+
throw new UserError("git is required for hooks commands.", { details: e instanceof Error ? e.message : String(e) });
|
|
17
|
+
}
|
|
18
|
+
if (res.code !== 0) {
|
|
19
|
+
throw new UserError("Not a git repository.", { details: res.stderr || res.stdout });
|
|
20
|
+
}
|
|
21
|
+
const root = String(res.stdout || "").trim();
|
|
22
|
+
if (!root) throw new UserError("Failed to resolve git repository root.");
|
|
23
|
+
return root;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} gitRoot
|
|
28
|
+
* @param {string} key
|
|
29
|
+
*/
|
|
30
|
+
async function readGitConfig(gitRoot, key) {
|
|
31
|
+
const res = await runCommand("git", ["config", "--get", key], { cwd: gitRoot });
|
|
32
|
+
if (res.code !== 0) return "";
|
|
33
|
+
return String(res.stdout || "").trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {fs.Stats} st
|
|
38
|
+
*/
|
|
39
|
+
function isExecutable(st) {
|
|
40
|
+
return (st.mode & 0o111) !== 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {{ targetPath: string }} options
|
|
45
|
+
*/
|
|
46
|
+
export async function hooksStatusCommand(options) {
|
|
47
|
+
const workspaceRoot = await resolveWorkspaceRoot(options.targetPath, { create: false });
|
|
48
|
+
const gitRoot = await resolveGitRoot(workspaceRoot);
|
|
49
|
+
|
|
50
|
+
const hooksPath = await readGitConfig(gitRoot, "core.hooksPath");
|
|
51
|
+
const normalizedHooksPath = hooksPath.replace(/[\\/]+$/, "");
|
|
52
|
+
|
|
53
|
+
const githooksDir = path.join(gitRoot, ".githooks");
|
|
54
|
+
const preCommit = path.join(githooksDir, "pre-commit");
|
|
55
|
+
const prePush = path.join(githooksDir, "pre-push");
|
|
56
|
+
|
|
57
|
+
const githooksExists = await pathExists(githooksDir);
|
|
58
|
+
const preCommitExists = await pathExists(preCommit);
|
|
59
|
+
const prePushExists = await pathExists(prePush);
|
|
60
|
+
|
|
61
|
+
let preCommitExec = false;
|
|
62
|
+
let prePushExec = false;
|
|
63
|
+
if (preCommitExists) {
|
|
64
|
+
const st = await fs.stat(preCommit);
|
|
65
|
+
preCommitExec = isExecutable(st);
|
|
66
|
+
}
|
|
67
|
+
if (prePushExists) {
|
|
68
|
+
const st = await fs.stat(prePush);
|
|
69
|
+
prePushExec = isExecutable(st);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`✓ aiws hooks status: ${gitRoot}`);
|
|
73
|
+
console.log(`core.hooksPath: ${hooksPath ? hooksPath : "(default: .git/hooks)"}`);
|
|
74
|
+
console.log(`.githooks/: ${githooksExists ? "ok" : "missing"}`);
|
|
75
|
+
console.log(`.githooks/pre-commit: ${preCommitExists ? (preCommitExec ? "ok" : "not-executable") : "missing"}`);
|
|
76
|
+
console.log(`.githooks/pre-push: ${prePushExists ? (prePushExec ? "ok" : "not-executable") : "missing"}`);
|
|
77
|
+
|
|
78
|
+
const enabled = normalizedHooksPath === ".githooks";
|
|
79
|
+
if (!enabled) {
|
|
80
|
+
console.log("Next: aiws hooks install .");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!githooksExists || !preCommitExists || !prePushExists || !preCommitExec || !prePushExec) {
|
|
84
|
+
console.log("Next: aiws hooks install .");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { loadTemplate } from "../spec.js";
|
|
4
|
+
import { expandManifestEntries, copyTemplateFileToWorkspace, applyManagedBlocksFromTemplate } from "../template.js";
|
|
5
|
+
import { loadAiwsPackage } from "../aiws-package.js";
|
|
6
|
+
import { writeWorkspaceManifest } from "../manifest.js";
|
|
7
|
+
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
8
|
+
import { joinRel, normalizeRel } from "../path-utils.js";
|
|
9
|
+
import { pathExists } from "../fs.js";
|
|
10
|
+
import { BackupSession } from "../backup.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {{ targetPath: string, templateId: string }} options
|
|
14
|
+
*/
|
|
15
|
+
export async function initCommand(options) {
|
|
16
|
+
const workspaceRoot = await resolveWorkspaceRoot(options.targetPath, { create: true });
|
|
17
|
+
const tpl = await loadTemplate(options.templateId);
|
|
18
|
+
const aiws = await loadAiwsPackage();
|
|
19
|
+
|
|
20
|
+
const defaults = tpl.manifest.defaults || {};
|
|
21
|
+
const includeOptional = defaults.include_optional !== false;
|
|
22
|
+
const tools = Array.isArray(defaults.tools) ? defaults.tools.map(String) : ["claude", "opencode", "codex", "iflow"];
|
|
23
|
+
|
|
24
|
+
const required = await expandManifestEntries(tpl.templateDir, tpl.manifest.required || []);
|
|
25
|
+
const optional = includeOptional ? await expandManifestEntries(tpl.templateDir, tpl.manifest.optional || []) : [];
|
|
26
|
+
const allFiles = Array.from(new Set([...required, ...optional]));
|
|
27
|
+
|
|
28
|
+
const update = tpl.manifest.update || {};
|
|
29
|
+
const replaceFiles = new Set((update.replace_file || []).map(normalizeRel));
|
|
30
|
+
const managedBlocks = update.managed_blocks && typeof update.managed_blocks === "object" ? update.managed_blocks : {};
|
|
31
|
+
|
|
32
|
+
// If we are overwriting existing files, take a backup for rollback.
|
|
33
|
+
const backup = new BackupSession({ workspaceRoot, operation: "init" });
|
|
34
|
+
await backup.recordFile(".aiws/manifest.json");
|
|
35
|
+
for (const f of replaceFiles) {
|
|
36
|
+
if (f && f !== ".aiws/manifest.json") await backup.recordFile(f);
|
|
37
|
+
}
|
|
38
|
+
for (const f of Object.keys(managedBlocks)) await backup.recordFile(f);
|
|
39
|
+
await backup.finalize({ extra: { template_id: tpl.templateId } });
|
|
40
|
+
|
|
41
|
+
// Ensure .aiws directory (manifest will be generated at the end)
|
|
42
|
+
await fs.mkdir(path.join(workspaceRoot, ".aiws"), { recursive: true });
|
|
43
|
+
|
|
44
|
+
for (const rel of allFiles) {
|
|
45
|
+
const r = normalizeRel(rel);
|
|
46
|
+
if (!r) continue;
|
|
47
|
+
if (r === ".aiws/manifest.json") {
|
|
48
|
+
// Always generated, never copied from template.
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (replaceFiles.has(r)) {
|
|
53
|
+
await copyTemplateFileToWorkspace({
|
|
54
|
+
templateDir: tpl.templateDir,
|
|
55
|
+
workspaceRoot,
|
|
56
|
+
relPosix: r,
|
|
57
|
+
chmod: r.startsWith(".githooks/") ? 0o755 : undefined,
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (Object.prototype.hasOwnProperty.call(managedBlocks, r)) {
|
|
63
|
+
const blockIds = Array.isArray(managedBlocks[r]) ? managedBlocks[r].map(String) : [];
|
|
64
|
+
await applyManagedBlocksFromTemplate({
|
|
65
|
+
templateDir: tpl.templateDir,
|
|
66
|
+
workspaceRoot,
|
|
67
|
+
fileRel: r,
|
|
68
|
+
blockIds,
|
|
69
|
+
insertIfMissing: true,
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// unmanaged: only create if missing
|
|
75
|
+
const dest = joinRel(workspaceRoot, r);
|
|
76
|
+
if (await pathExists(dest)) continue;
|
|
77
|
+
await copyTemplateFileToWorkspace({ templateDir: tpl.templateDir, workspaceRoot, relPosix: r });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
81
|
+
await writeWorkspaceManifest({
|
|
82
|
+
workspaceRoot,
|
|
83
|
+
templateId: tpl.templateId,
|
|
84
|
+
specVersion: tpl.specVersion,
|
|
85
|
+
aiwsVersion: aiws.version,
|
|
86
|
+
installedAt: now,
|
|
87
|
+
updatedAt: now,
|
|
88
|
+
tools,
|
|
89
|
+
templateManifest: tpl.manifest,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
console.log(`✓ aiws init: ${workspaceRoot} (template=${tpl.templateId})`);
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
2
|
+
import { resolveBackupRoot, rollbackFromBackup } from "../backup.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {{ targetPath: string, stamp: string }} options
|
|
6
|
+
*/
|
|
7
|
+
export async function rollbackCommand(options) {
|
|
8
|
+
const workspaceRoot = await resolveWorkspaceRoot(options.targetPath, { create: false });
|
|
9
|
+
const backupRoot = await resolveBackupRoot(workspaceRoot, options.stamp);
|
|
10
|
+
await rollbackFromBackup(workspaceRoot, backupRoot);
|
|
11
|
+
console.log(`✓ aiws rollback: ${workspaceRoot} (${options.stamp})`);
|
|
12
|
+
}
|
|
13
|
+
|