@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
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @aipper/aiws
|
|
2
|
+
|
|
3
|
+
AI Workspace CLI:`aiws init/update/validate/rollback`。
|
|
4
|
+
|
|
5
|
+
真值来源(SSOT):`@aipper/aiws-spec`(模板与契约在 `packages/spec/`)。
|
|
6
|
+
|
|
7
|
+
需求真值:仓库根 `REQUIREMENTS.md`(TOOLING-001B)。
|
|
8
|
+
|
|
9
|
+
开发期最小验证(本仓库内):
|
|
10
|
+
- `node packages/aiws/bin/aiws.js --help`
|
|
11
|
+
|
|
12
|
+
Codex repo skills(推荐):
|
|
13
|
+
- `aiws init .` 会生成 `.agents/skills/`(随仓库共享)
|
|
14
|
+
- 在 Codex 中可显式调用(示例):`$ws-preflight` / `$ws-plan` / `$ws-dev` / `$ws-review` / `$ws-commit`
|
|
15
|
+
|
|
16
|
+
Codex 全局 skills(推荐;可选):
|
|
17
|
+
- 安装到 `~/.codex/skills/`(或 `$CODEX_HOME/skills`):`aiws codex install-skills`
|
|
18
|
+
- 预演(不写入):`aiws codex install-skills --dry-run`
|
|
19
|
+
- 状态检查:`aiws codex status-skills`
|
|
20
|
+
- 卸载(仅移除 AIWS 托管 skills;会备份):`aiws codex uninstall-skills`
|
|
21
|
+
- 本仓库内可复现验证(不写入 home):`CODEX_HOME="$(mktemp -d)" node packages/aiws/bin/aiws.js codex install-skills`
|
|
22
|
+
|
|
23
|
+
Codex 全局 prompts(遗留;deprecated):
|
|
24
|
+
- 安装到 `~/.codex/prompts/`(或 `$CODEX_HOME/prompts`):`aiws codex install-prompts`
|
|
25
|
+
- 预演(不写入):`aiws codex install-prompts --dry-run`
|
|
26
|
+
- 状态检查:`aiws codex status`
|
|
27
|
+
- 卸载(仅移除 AIWS 托管 prompts;会备份):`aiws codex uninstall-prompts`
|
|
28
|
+
- 本仓库内可复现验证(不写入 home):`CODEX_HOME="$(mktemp -d)" node packages/aiws/bin/aiws.js codex install-prompts`
|
|
29
|
+
|
|
30
|
+
Git hooks 门禁(推荐):
|
|
31
|
+
- 启用(会自动补齐 `.githooks/*`,并设置 `core.hooksPath=.githooks`):`aiws hooks install .`
|
|
32
|
+
- 查看状态:`aiws hooks status .`
|
|
33
|
+
- 证据落盘(可选):`AIWS_VALIDATE_STAMP=1 aiws validate .` 会写入 `.agentdocs/tmp/aiws-validate/*.json`(`.agentdocs/` 被 `.gitignore` 忽略)
|
|
34
|
+
|
|
35
|
+
Change 工作流(脱离 dotfiles):
|
|
36
|
+
- 创建工件:`aiws change new <change-id>`(生成 `changes/<change-id>/{proposal,tasks,design}.md` 与 `.ws-change.json`)
|
|
37
|
+
- 切分支并初始化:`aiws change start <change-id> [--hooks]`
|
|
38
|
+
- 进度/建议:`aiws change list` / `aiws change status <change-id>` / `aiws change next <change-id>`
|
|
39
|
+
- 同步真值基线:`aiws change sync <change-id>`
|
|
40
|
+
- 严格校验:`aiws change validate <change-id> --strict`(在你把 `WS:TODO` 与归因补齐前,预期会失败)
|
|
41
|
+
- 归档:`aiws change archive <change-id>`
|
|
42
|
+
|
|
43
|
+
本仓库内可复现验证(不污染本仓库):
|
|
44
|
+
```bash
|
|
45
|
+
repo_root="$(pwd)"
|
|
46
|
+
tmpdir="$(mktemp -d)"
|
|
47
|
+
cd "$tmpdir"
|
|
48
|
+
git init
|
|
49
|
+
node "$repo_root/packages/aiws/bin/aiws.js" init .
|
|
50
|
+
node "$repo_root/packages/aiws/bin/aiws.js" change new demo-change --no-design
|
|
51
|
+
node "$repo_root/packages/aiws/bin/aiws.js" change list
|
|
52
|
+
node "$repo_root/packages/aiws/bin/aiws.js" change sync demo-change
|
|
53
|
+
node "$repo_root/packages/aiws/bin/aiws.js" change validate demo-change --strict
|
|
54
|
+
```
|
package/bin/aiws.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cliMain } from "../src/cli.js";
|
|
3
|
+
import { UserError } from "../src/errors.js";
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const exitCode = await cliMain(process.argv.slice(2));
|
|
7
|
+
process.exitCode = exitCode;
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error instanceof UserError) {
|
|
10
|
+
process.exitCode = error.exitCode;
|
|
11
|
+
if (error.message) console.error(error.message);
|
|
12
|
+
if (error.details) console.error(error.details);
|
|
13
|
+
} else {
|
|
14
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
console.error(e.stack || e.message);
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aipper/aiws",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "AI Workspace CLI (init/update/validate) for Claude Code / OpenCode / Codex / iFlow.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aiws": "./bin/aiws.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@aipper/aiws-spec": "0.0.1"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { readText } from "./fs.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @returns {Promise<{ rootDir: string, version: string }>}
|
|
7
|
+
*/
|
|
8
|
+
export async function loadAiwsPackage() {
|
|
9
|
+
const here = fileURLToPath(import.meta.url);
|
|
10
|
+
const rootDir = path.resolve(path.dirname(here), "..");
|
|
11
|
+
const pkg = JSON.parse(await readText(path.join(rootDir, "package.json")));
|
|
12
|
+
const version = String(pkg.version || "").trim() || "0.0.0";
|
|
13
|
+
return { rootDir, version };
|
|
14
|
+
}
|
|
15
|
+
|
package/src/backup.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureDir, pathExists } from "./fs.js";
|
|
4
|
+
import { joinRel, normalizeRel } from "./path-utils.js";
|
|
5
|
+
import { UserError } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {Date} d
|
|
9
|
+
*/
|
|
10
|
+
export function formatBackupTimestamp(d) {
|
|
11
|
+
return d.toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "-");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class BackupSession {
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ workspaceRoot: string, operation: "init" | "update" }} options
|
|
17
|
+
*/
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.workspaceRoot = options.workspaceRoot;
|
|
20
|
+
this.operation = options.operation;
|
|
21
|
+
this.timestamp = formatBackupTimestamp(new Date());
|
|
22
|
+
this.backupRoot = path.join(this.workspaceRoot, ".aiws", "backups", this.timestamp);
|
|
23
|
+
/** @type {Map<string, { existed: boolean, mode: number | null }>} */
|
|
24
|
+
this.files = new Map();
|
|
25
|
+
this._prepared = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async _prepare() {
|
|
29
|
+
if (this._prepared) return;
|
|
30
|
+
await ensureDir(this.backupRoot);
|
|
31
|
+
this._prepared = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record a file before modification.
|
|
36
|
+
*
|
|
37
|
+
* For update: record both existing and missing files (for rollback deletions).
|
|
38
|
+
* For init: typically record only existing files that will be modified.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} relPosix
|
|
41
|
+
* @param {{ recordMissing?: boolean }=} options
|
|
42
|
+
*/
|
|
43
|
+
async recordFile(relPosix, options) {
|
|
44
|
+
const rel = normalizeRel(relPosix);
|
|
45
|
+
if (this.files.has(rel)) return;
|
|
46
|
+
|
|
47
|
+
const abs = joinRel(this.workspaceRoot, rel);
|
|
48
|
+
const exists = await pathExists(abs);
|
|
49
|
+
if (!exists) {
|
|
50
|
+
if (options?.recordMissing) {
|
|
51
|
+
this.files.set(rel, { existed: false, mode: null });
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const st = await fs.stat(abs);
|
|
57
|
+
if (!st.isFile()) {
|
|
58
|
+
if (options?.recordMissing) this.files.set(rel, { existed: false, mode: null });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await this._prepare();
|
|
63
|
+
const dest = joinRel(this.backupRoot, rel);
|
|
64
|
+
await ensureDir(path.dirname(dest));
|
|
65
|
+
await fs.copyFile(abs, dest);
|
|
66
|
+
try {
|
|
67
|
+
await fs.chmod(dest, st.mode & 0o777);
|
|
68
|
+
} catch {
|
|
69
|
+
// ignore chmod errors
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.files.set(rel, { existed: true, mode: st.mode & 0o777 });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {{ extra?: any }=} options
|
|
77
|
+
*/
|
|
78
|
+
async finalize(options) {
|
|
79
|
+
if (!this._prepared && this.files.size === 0) return null;
|
|
80
|
+
await this._prepare();
|
|
81
|
+
const meta = {
|
|
82
|
+
operation: this.operation,
|
|
83
|
+
timestamp: this.timestamp,
|
|
84
|
+
created_at: new Date().toISOString(),
|
|
85
|
+
files: Array.from(this.files.entries()).map(([p, v]) => ({ path: p, existed: v.existed, mode: v.mode })),
|
|
86
|
+
extra: options?.extra ?? null,
|
|
87
|
+
};
|
|
88
|
+
await fs.writeFile(path.join(this.backupRoot, "backup.json"), JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
89
|
+
return this.backupRoot;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} workspaceRoot
|
|
95
|
+
* @param {string} stamp
|
|
96
|
+
* @returns {Promise<string>} backup root dir
|
|
97
|
+
*/
|
|
98
|
+
export async function resolveBackupRoot(workspaceRoot, stamp) {
|
|
99
|
+
const backupsRoot = path.join(workspaceRoot, ".aiws", "backups");
|
|
100
|
+
if (stamp === "latest") {
|
|
101
|
+
if (!(await pathExists(backupsRoot))) throw new UserError("No backups found.");
|
|
102
|
+
const entries = await fs.readdir(backupsRoot, { withFileTypes: true });
|
|
103
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
104
|
+
const latest = dirs[dirs.length - 1];
|
|
105
|
+
if (!latest) throw new UserError("No backups found.");
|
|
106
|
+
return path.join(backupsRoot, latest);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const target = path.join(backupsRoot, stamp);
|
|
110
|
+
if (!(await pathExists(target))) {
|
|
111
|
+
throw new UserError(`Backup not found: ${stamp}`, { details: `Missing: ${target}` });
|
|
112
|
+
}
|
|
113
|
+
return target;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} workspaceRoot
|
|
118
|
+
* @param {string} backupRoot
|
|
119
|
+
*/
|
|
120
|
+
export async function rollbackFromBackup(workspaceRoot, backupRoot) {
|
|
121
|
+
const metaPath = path.join(backupRoot, "backup.json");
|
|
122
|
+
if (!(await pathExists(metaPath))) {
|
|
123
|
+
throw new UserError("backup.json not found; cannot rollback safely.", { details: `Missing: ${metaPath}` });
|
|
124
|
+
}
|
|
125
|
+
const meta = JSON.parse(await fs.readFile(metaPath, "utf8"));
|
|
126
|
+
const files = Array.isArray(meta.files) ? meta.files : [];
|
|
127
|
+
for (const f of files) {
|
|
128
|
+
const rel = normalizeRel(String(f.path || ""));
|
|
129
|
+
if (!rel) continue;
|
|
130
|
+
const dest = joinRel(workspaceRoot, rel);
|
|
131
|
+
if (f.existed) {
|
|
132
|
+
const src = joinRel(backupRoot, rel);
|
|
133
|
+
await ensureDir(path.dirname(dest));
|
|
134
|
+
await fs.copyFile(src, dest);
|
|
135
|
+
if (typeof f.mode === "number") {
|
|
136
|
+
try {
|
|
137
|
+
await fs.chmod(dest, f.mode);
|
|
138
|
+
} catch {
|
|
139
|
+
// ignore chmod errors
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
if (await pathExists(dest)) {
|
|
144
|
+
await fs.rm(dest, { force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|