@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/src/cli.js
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { UserError } from "./errors.js";
|
|
5
|
+
import { initCommand } from "./commands/init.js";
|
|
6
|
+
import { updateCommand } from "./commands/update.js";
|
|
7
|
+
import { validateCommand } from "./commands/validate.js";
|
|
8
|
+
import { rollbackCommand } from "./commands/rollback.js";
|
|
9
|
+
import { codexInstallPromptsCommand } from "./commands/codex-install-prompts.js";
|
|
10
|
+
import { codexStatusPromptsCommand } from "./commands/codex-status-prompts.js";
|
|
11
|
+
import { codexUninstallPromptsCommand } from "./commands/codex-uninstall-prompts.js";
|
|
12
|
+
import { codexInstallSkillsCommand } from "./commands/codex-install-skills.js";
|
|
13
|
+
import { codexStatusSkillsCommand } from "./commands/codex-status-skills.js";
|
|
14
|
+
import { codexUninstallSkillsCommand } from "./commands/codex-uninstall-skills.js";
|
|
15
|
+
import { hooksInstallCommand } from "./commands/hooks-install.js";
|
|
16
|
+
import { hooksStatusCommand } from "./commands/hooks-status.js";
|
|
17
|
+
import {
|
|
18
|
+
changeArchiveCommand,
|
|
19
|
+
changeListCommand,
|
|
20
|
+
changeNewCommand,
|
|
21
|
+
changeNextCommand,
|
|
22
|
+
changeStartCommand,
|
|
23
|
+
changeStatusCommand,
|
|
24
|
+
changeSyncCommand,
|
|
25
|
+
changeTemplatesInitCommand,
|
|
26
|
+
changeTemplatesWhichCommand,
|
|
27
|
+
changeValidateCommand,
|
|
28
|
+
} from "./commands/change.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string[]} argv
|
|
32
|
+
* @returns {Promise<number>} exit code
|
|
33
|
+
*/
|
|
34
|
+
export async function cliMain(argv) {
|
|
35
|
+
const args = argv.slice();
|
|
36
|
+
if (args[0] === "codex" && (args.includes("-h") || args.includes("--help"))) {
|
|
37
|
+
printCodexHelp();
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
if (args[0] === "hooks" && (args.includes("-h") || args.includes("--help"))) {
|
|
41
|
+
printHooksHelp();
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
if (args[0] === "change" && (args.includes("-h") || args.includes("--help"))) {
|
|
45
|
+
printChangeHelp();
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
if (args.length === 0 || args.includes("-h") || args.includes("--help") || args[0] === "help") {
|
|
49
|
+
printHelp();
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
53
|
+
console.log(getAiwsVersion());
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cmd = args.shift();
|
|
58
|
+
if (!cmd) {
|
|
59
|
+
printHelp();
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
switch (cmd) {
|
|
64
|
+
case "init": {
|
|
65
|
+
const { positionals, options } = parseArgs(args, {
|
|
66
|
+
template: { type: "string" },
|
|
67
|
+
});
|
|
68
|
+
const targetPath = positionals[0] ?? ".";
|
|
69
|
+
const templateId = options.template ?? "workspace";
|
|
70
|
+
await initCommand({ targetPath, templateId });
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
case "update": {
|
|
74
|
+
const { positionals } = parseArgs(args, {});
|
|
75
|
+
const targetPath = positionals[0] ?? ".";
|
|
76
|
+
await updateCommand({ targetPath });
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
case "validate": {
|
|
80
|
+
const { positionals, options } = parseArgs(args, {
|
|
81
|
+
stamp: { type: "boolean" },
|
|
82
|
+
});
|
|
83
|
+
const targetPath = positionals[0] ?? ".";
|
|
84
|
+
await validateCommand({ targetPath, stamp: options.stamp === true });
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
case "rollback": {
|
|
88
|
+
const { positionals } = parseArgs(args, {});
|
|
89
|
+
if (positionals.length === 0) {
|
|
90
|
+
throw new UserError("rollback requires <timestamp|latest>", { details: "Usage: aiws rollback [path] <timestamp|latest>" });
|
|
91
|
+
}
|
|
92
|
+
let targetPath = ".";
|
|
93
|
+
let stamp = "";
|
|
94
|
+
if (positionals.length === 1) {
|
|
95
|
+
stamp = positionals[0] ?? "";
|
|
96
|
+
} else {
|
|
97
|
+
targetPath = positionals[0] ?? ".";
|
|
98
|
+
stamp = positionals[1] ?? "";
|
|
99
|
+
}
|
|
100
|
+
await rollbackCommand({ targetPath, stamp });
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
case "codex": {
|
|
104
|
+
const sub = args.shift();
|
|
105
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
106
|
+
printCodexHelp();
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
switch (sub) {
|
|
110
|
+
case "install-skills": {
|
|
111
|
+
const { options } = parseArgs(args, {
|
|
112
|
+
template: { type: "string" },
|
|
113
|
+
dir: { type: "string" },
|
|
114
|
+
force: { type: "boolean" },
|
|
115
|
+
"dry-run": { type: "boolean" },
|
|
116
|
+
});
|
|
117
|
+
await codexInstallSkillsCommand({
|
|
118
|
+
templateId: options.template ?? "workspace",
|
|
119
|
+
skillsDir: options.dir,
|
|
120
|
+
force: options.force === true,
|
|
121
|
+
dryRun: options["dry-run"] === true,
|
|
122
|
+
});
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
case "status-skills": {
|
|
126
|
+
const { options } = parseArgs(args, {
|
|
127
|
+
template: { type: "string" },
|
|
128
|
+
dir: { type: "string" },
|
|
129
|
+
});
|
|
130
|
+
await codexStatusSkillsCommand({
|
|
131
|
+
templateId: options.template ?? "workspace",
|
|
132
|
+
skillsDir: options.dir,
|
|
133
|
+
});
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
case "uninstall-skills": {
|
|
137
|
+
const { options } = parseArgs(args, {
|
|
138
|
+
template: { type: "string" },
|
|
139
|
+
dir: { type: "string" },
|
|
140
|
+
});
|
|
141
|
+
await codexUninstallSkillsCommand({
|
|
142
|
+
templateId: options.template ?? "workspace",
|
|
143
|
+
skillsDir: options.dir,
|
|
144
|
+
});
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
case "install-prompts": {
|
|
148
|
+
const { options } = parseArgs(args, {
|
|
149
|
+
template: { type: "string" },
|
|
150
|
+
dir: { type: "string" },
|
|
151
|
+
force: { type: "boolean" },
|
|
152
|
+
"dry-run": { type: "boolean" },
|
|
153
|
+
});
|
|
154
|
+
await codexInstallPromptsCommand({
|
|
155
|
+
templateId: options.template ?? "workspace",
|
|
156
|
+
promptsDir: options.dir,
|
|
157
|
+
force: options.force === true,
|
|
158
|
+
dryRun: options["dry-run"] === true,
|
|
159
|
+
});
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
case "status": {
|
|
163
|
+
const { options } = parseArgs(args, {
|
|
164
|
+
template: { type: "string" },
|
|
165
|
+
dir: { type: "string" },
|
|
166
|
+
});
|
|
167
|
+
await codexStatusPromptsCommand({
|
|
168
|
+
templateId: options.template ?? "workspace",
|
|
169
|
+
promptsDir: options.dir,
|
|
170
|
+
});
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
case "uninstall-prompts": {
|
|
174
|
+
const { options } = parseArgs(args, {
|
|
175
|
+
template: { type: "string" },
|
|
176
|
+
dir: { type: "string" },
|
|
177
|
+
});
|
|
178
|
+
await codexUninstallPromptsCommand({
|
|
179
|
+
templateId: options.template ?? "workspace",
|
|
180
|
+
promptsDir: options.dir,
|
|
181
|
+
});
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
throw new UserError(`Unknown codex subcommand: ${sub}`, { details: "Use `aiws codex --help` to see available subcommands." });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
case "hooks": {
|
|
189
|
+
const sub = args.shift();
|
|
190
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
191
|
+
printHooksHelp();
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
switch (sub) {
|
|
195
|
+
case "install": {
|
|
196
|
+
const { positionals } = parseArgs(args, {});
|
|
197
|
+
const targetPath = positionals[0] ?? ".";
|
|
198
|
+
await hooksInstallCommand({ targetPath });
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
case "status": {
|
|
202
|
+
const { positionals } = parseArgs(args, {});
|
|
203
|
+
const targetPath = positionals[0] ?? ".";
|
|
204
|
+
await hooksStatusCommand({ targetPath });
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
default:
|
|
208
|
+
throw new UserError(`Unknown hooks subcommand: ${sub}`, { details: "Use `aiws hooks --help` to see available subcommands." });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
case "change": {
|
|
212
|
+
const sub = args.shift();
|
|
213
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
214
|
+
printChangeHelp();
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
switch (sub) {
|
|
218
|
+
case "list": {
|
|
219
|
+
await changeListCommand();
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
case "start": {
|
|
223
|
+
const { positionals, options } = parseArgs(args, {
|
|
224
|
+
title: { type: "string" },
|
|
225
|
+
"no-design": { type: "boolean" },
|
|
226
|
+
hooks: { type: "boolean" },
|
|
227
|
+
});
|
|
228
|
+
const changeId = positionals[0] ?? "";
|
|
229
|
+
if (!changeId) throw new UserError("change start requires <change-id>", { details: "Usage: aiws change start <change-id> [--title <title>] [--no-design] [--hooks]" });
|
|
230
|
+
await changeStartCommand({
|
|
231
|
+
changeId,
|
|
232
|
+
title: options.title,
|
|
233
|
+
noDesign: options["no-design"] === true,
|
|
234
|
+
enableHooks: options.hooks === true,
|
|
235
|
+
});
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
case "new": {
|
|
239
|
+
const { positionals, options } = parseArgs(args, {
|
|
240
|
+
title: { type: "string" },
|
|
241
|
+
"no-design": { type: "boolean" },
|
|
242
|
+
});
|
|
243
|
+
const changeId = positionals[0] ?? "";
|
|
244
|
+
if (!changeId) throw new UserError("change new requires <change-id>", { details: "Usage: aiws change new <change-id> [--title <title>] [--no-design]" });
|
|
245
|
+
await changeNewCommand({
|
|
246
|
+
changeId,
|
|
247
|
+
title: options.title,
|
|
248
|
+
noDesign: options["no-design"] === true,
|
|
249
|
+
});
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
case "status": {
|
|
253
|
+
const { positionals } = parseArgs(args, {});
|
|
254
|
+
const changeId = positionals[0];
|
|
255
|
+
await changeStatusCommand({ changeId });
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
case "next": {
|
|
259
|
+
const { positionals } = parseArgs(args, {});
|
|
260
|
+
const changeId = positionals[0];
|
|
261
|
+
await changeNextCommand({ changeId });
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
case "validate": {
|
|
265
|
+
const { positionals, options } = parseArgs(args, {
|
|
266
|
+
strict: { type: "boolean" },
|
|
267
|
+
"allow-truth-drift": { type: "boolean" },
|
|
268
|
+
});
|
|
269
|
+
const changeId = positionals[0];
|
|
270
|
+
await changeValidateCommand({
|
|
271
|
+
changeId,
|
|
272
|
+
strict: options.strict === true,
|
|
273
|
+
allowTruthDrift: options["allow-truth-drift"] === true,
|
|
274
|
+
});
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
case "sync": {
|
|
278
|
+
const { positionals } = parseArgs(args, {});
|
|
279
|
+
const changeId = positionals[0];
|
|
280
|
+
await changeSyncCommand({ changeId });
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
case "archive": {
|
|
284
|
+
const { positionals, options } = parseArgs(args, {
|
|
285
|
+
date: { type: "string" },
|
|
286
|
+
force: { type: "boolean" },
|
|
287
|
+
});
|
|
288
|
+
const changeId = positionals[0];
|
|
289
|
+
await changeArchiveCommand({
|
|
290
|
+
changeId,
|
|
291
|
+
datePrefix: options.date,
|
|
292
|
+
force: options.force === true,
|
|
293
|
+
});
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
case "templates": {
|
|
297
|
+
const templatesSub = args.shift();
|
|
298
|
+
if (!templatesSub) throw new UserError("change templates requires <which|init>", { details: "Usage: aiws change templates which|init" });
|
|
299
|
+
if (templatesSub === "which") {
|
|
300
|
+
const { positionals } = parseArgs(args, {});
|
|
301
|
+
if (positionals.length > 0) throw new UserError("change templates which takes no args");
|
|
302
|
+
await changeTemplatesWhichCommand();
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
if (templatesSub === "init") {
|
|
306
|
+
const { positionals } = parseArgs(args, {});
|
|
307
|
+
if (positionals.length > 0) throw new UserError("change templates init takes no args");
|
|
308
|
+
await changeTemplatesInitCommand();
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
throw new UserError(`Unknown change templates subcommand: ${templatesSub}`, { details: "Use `aiws change --help` to see available commands." });
|
|
312
|
+
}
|
|
313
|
+
default:
|
|
314
|
+
throw new UserError(`Unknown change subcommand: ${sub}`, { details: "Use `aiws change --help` to see available subcommands." });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
default:
|
|
318
|
+
throw new UserError(`Unknown command: ${cmd}`, { details: "Use --help to see available commands." });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function printHelp() {
|
|
323
|
+
console.log(`aiws (WORK IN PROGRESS)
|
|
324
|
+
|
|
325
|
+
Usage:
|
|
326
|
+
aiws init [path] [--template <id>]
|
|
327
|
+
aiws update [path]
|
|
328
|
+
aiws validate [path] [--stamp]
|
|
329
|
+
aiws rollback [path] <timestamp|latest>
|
|
330
|
+
aiws change <subcommand>
|
|
331
|
+
aiws codex <subcommand>
|
|
332
|
+
aiws hooks <subcommand>
|
|
333
|
+
|
|
334
|
+
Options:
|
|
335
|
+
--template <id> Template id (default: workspace)
|
|
336
|
+
--stamp Write evidence stamp (validate)
|
|
337
|
+
-h, --help Show help
|
|
338
|
+
-v, --version Show version
|
|
339
|
+
`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function printCodexHelp() {
|
|
343
|
+
console.log(`aiws codex
|
|
344
|
+
|
|
345
|
+
Usage:
|
|
346
|
+
aiws codex install-skills [--template <id>] [--dir <path>] [--force] [--dry-run]
|
|
347
|
+
aiws codex status-skills [--template <id>] [--dir <path>]
|
|
348
|
+
aiws codex uninstall-skills [--template <id>] [--dir <path>]
|
|
349
|
+
aiws codex install-prompts [--template <id>] [--dir <path>] [--force] [--dry-run]
|
|
350
|
+
aiws codex status [--template <id>] [--dir <path>]
|
|
351
|
+
aiws codex uninstall-prompts [--template <id>] [--dir <path>]
|
|
352
|
+
|
|
353
|
+
Notes:
|
|
354
|
+
- Prefer repo skills: .agents/skills (no install needed)
|
|
355
|
+
- Prefer global skills over legacy prompts
|
|
356
|
+
- Default skills dir: ~/.codex/skills (or $CODEX_HOME/skills)
|
|
357
|
+
- install-prompts is legacy (prompts are deprecated)
|
|
358
|
+
- Default target dir: ~/.codex/prompts
|
|
359
|
+
- If CODEX_HOME is set: $CODEX_HOME/prompts
|
|
360
|
+
`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function printHooksHelp() {
|
|
364
|
+
console.log(`aiws hooks
|
|
365
|
+
|
|
366
|
+
Usage:
|
|
367
|
+
aiws hooks install [path]
|
|
368
|
+
aiws hooks status [path]
|
|
369
|
+
|
|
370
|
+
Notes:
|
|
371
|
+
- install will set: git config core.hooksPath .githooks
|
|
372
|
+
- hooks can be bypassed with: WS_CHANGE_HOOK_BYPASS=1
|
|
373
|
+
`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function printChangeHelp() {
|
|
377
|
+
console.log(`aiws change
|
|
378
|
+
|
|
379
|
+
Usage:
|
|
380
|
+
aiws change list
|
|
381
|
+
aiws change start <change-id> [--title <title>] [--no-design] [--hooks]
|
|
382
|
+
aiws change new <change-id> [--title <title>] [--no-design]
|
|
383
|
+
aiws change status [change-id]
|
|
384
|
+
aiws change next [change-id]
|
|
385
|
+
aiws change validate [change-id] [--strict] [--allow-truth-drift]
|
|
386
|
+
aiws change sync [change-id]
|
|
387
|
+
aiws change archive [change-id] [--date YYYY-MM-DD] [--force]
|
|
388
|
+
aiws change templates which
|
|
389
|
+
aiws change templates init
|
|
390
|
+
|
|
391
|
+
Notes:
|
|
392
|
+
- change-id must be kebab-case: ^[a-z0-9]+(-[a-z0-9]+)*$
|
|
393
|
+
- If your git branch matches change/<change-id> (or changes/ws/ws-change prefixes),
|
|
394
|
+
you can omit <change-id> for status/next/validate/sync/archive.
|
|
395
|
+
- archive runs strict validation and (by default) requires all tasks checked.
|
|
396
|
+
- archive --force also bypasses truth drift gating (not recommended).
|
|
397
|
+
`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Minimal argv parser:
|
|
402
|
+
* - supports: --k=v, --k v
|
|
403
|
+
* - collects unknown flags as error
|
|
404
|
+
*
|
|
405
|
+
* @param {string[]} argv
|
|
406
|
+
* @param {Record<string, { type: "string" | "boolean" }>} schema
|
|
407
|
+
* @returns {{ positionals: string[], options: Record<string, any> }}
|
|
408
|
+
*/
|
|
409
|
+
function parseArgs(argv, schema) {
|
|
410
|
+
/** @type {string[]} */
|
|
411
|
+
const positionals = [];
|
|
412
|
+
/** @type {Record<string, any>} */
|
|
413
|
+
const options = {};
|
|
414
|
+
|
|
415
|
+
for (let i = 0; i < argv.length; i++) {
|
|
416
|
+
const a = argv[i] ?? "";
|
|
417
|
+
if (!a.startsWith("-")) {
|
|
418
|
+
positionals.push(a);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (a === "--") {
|
|
422
|
+
positionals.push(...argv.slice(i + 1));
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
if (!a.startsWith("--")) {
|
|
426
|
+
throw new UserError(`Unknown option: ${a}`);
|
|
427
|
+
}
|
|
428
|
+
const eq = a.indexOf("=");
|
|
429
|
+
const key = (eq === -1 ? a.slice(2) : a.slice(2, eq)).trim();
|
|
430
|
+
if (!schema[key]) {
|
|
431
|
+
throw new UserError(`Unknown option: --${key}`);
|
|
432
|
+
}
|
|
433
|
+
const type = schema[key]?.type;
|
|
434
|
+
if (type === "boolean") {
|
|
435
|
+
if (eq !== -1) {
|
|
436
|
+
const raw = a.slice(eq + 1).trim().toLowerCase();
|
|
437
|
+
options[key] = raw === "1" || raw === "true" || raw === "yes" || raw === "y" || raw === "on";
|
|
438
|
+
} else {
|
|
439
|
+
options[key] = true;
|
|
440
|
+
}
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let value = "";
|
|
445
|
+
if (eq !== -1) {
|
|
446
|
+
value = a.slice(eq + 1);
|
|
447
|
+
} else {
|
|
448
|
+
const next = argv[i + 1];
|
|
449
|
+
if (!next || next.startsWith("-")) {
|
|
450
|
+
throw new UserError(`Missing value for --${key}`);
|
|
451
|
+
}
|
|
452
|
+
value = next;
|
|
453
|
+
i++;
|
|
454
|
+
}
|
|
455
|
+
options[key] = value;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return { positionals, options };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function getAiwsVersion() {
|
|
462
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
463
|
+
const pkgPath = path.resolve(here, "../package.json");
|
|
464
|
+
try {
|
|
465
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
466
|
+
return String(pkg.version || "").trim() || "0.0.0";
|
|
467
|
+
} catch {
|
|
468
|
+
return "0.0.0";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { listFilesRecursive, readText } from "./fs.js";
|
|
4
|
+
import { normalizeNewlines } from "./hash.js";
|
|
5
|
+
import { extractTemplateBlock } from "./managed-blocks.js";
|
|
6
|
+
import { UserError } from "./errors.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve Codex global prompts directory:
|
|
10
|
+
* - If `--dir` provided, use it directly.
|
|
11
|
+
* - Else if `CODEX_HOME` is set, use `$CODEX_HOME/prompts`.
|
|
12
|
+
* - Else use `~/.codex/prompts`.
|
|
13
|
+
*
|
|
14
|
+
* @param {string | undefined} dirOption
|
|
15
|
+
*/
|
|
16
|
+
export function resolveCodexPromptsDir(dirOption) {
|
|
17
|
+
if (dirOption) return path.resolve(dirOption);
|
|
18
|
+
const envHome = String(process.env.CODEX_HOME || "").trim();
|
|
19
|
+
if (envHome) return path.join(path.resolve(envHome), "prompts");
|
|
20
|
+
return path.join(os.homedir(), ".codex", "prompts");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function timestamp() {
|
|
24
|
+
const d = new Date();
|
|
25
|
+
// 2026-01-28T12:34:56Z -> 20260128-123456Z
|
|
26
|
+
return d.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z").replace("T", "-");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function codexBackupStamp() {
|
|
30
|
+
return timestamp();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} promptsDir
|
|
35
|
+
* @param {string} filename
|
|
36
|
+
* @param {string=} stamp
|
|
37
|
+
*/
|
|
38
|
+
export function codexBackupPathFor(promptsDir, filename, stamp) {
|
|
39
|
+
return path.join(promptsDir, ".aiws", "backups", "codex-prompts", stamp || timestamp(), filename);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List Codex prompt files in a template and extract their managed block inner text.
|
|
44
|
+
*
|
|
45
|
+
* @param {{ templateId: string, templateDir: string }} tpl
|
|
46
|
+
* @returns {Promise<Array<{ rel: string, filename: string, name: string, blockId: string, srcAbs: string, templateText: string, innerText: string }>>}
|
|
47
|
+
*/
|
|
48
|
+
export async function listTemplateCodexPrompts(tpl) {
|
|
49
|
+
const relFiles = await listFilesRecursive(tpl.templateDir, ".codex/prompts");
|
|
50
|
+
const promptFiles = relFiles
|
|
51
|
+
.filter((rel) => rel.startsWith(".codex/prompts/"))
|
|
52
|
+
.filter((rel) => rel.split("/").length === 3)
|
|
53
|
+
.filter((rel) => rel.toLowerCase().endsWith(".md"));
|
|
54
|
+
|
|
55
|
+
if (promptFiles.length === 0) {
|
|
56
|
+
throw new UserError(`Template has no Codex prompt files: ${tpl.templateId}`, {
|
|
57
|
+
details: `Missing under: ${path.join(tpl.templateDir, ".codex", "prompts")}`,
|
|
58
|
+
exitCode: 1,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @type {Array<{ rel: string, filename: string, name: string, blockId: string, srcAbs: string, templateText: string, innerText: string }>} */
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const rel of promptFiles) {
|
|
65
|
+
const filename = path.posix.basename(rel);
|
|
66
|
+
const name = filename.replace(/\.md$/i, "");
|
|
67
|
+
const blockId = `codex:${name}`;
|
|
68
|
+
const srcAbs = path.join(tpl.templateDir, ...rel.split("/"));
|
|
69
|
+
const templateText = normalizeNewlines(await readText(srcAbs));
|
|
70
|
+
const { innerText } = extractTemplateBlock(templateText, blockId);
|
|
71
|
+
out.push({ rel, filename, name, blockId, srcAbs, templateText, innerText });
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { listFilesRecursive, readText } from "./fs.js";
|
|
4
|
+
import { normalizeNewlines } from "./hash.js";
|
|
5
|
+
import { UserError } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve Codex global skills directory:
|
|
9
|
+
* - If `--dir` provided, use it directly.
|
|
10
|
+
* - Else if `CODEX_HOME` is set, use `$CODEX_HOME/skills`.
|
|
11
|
+
* - Else use `~/.codex/skills`.
|
|
12
|
+
*
|
|
13
|
+
* @param {string | undefined} dirOption
|
|
14
|
+
*/
|
|
15
|
+
export function resolveCodexSkillsDir(dirOption) {
|
|
16
|
+
if (dirOption) return path.resolve(dirOption);
|
|
17
|
+
const envHome = String(process.env.CODEX_HOME || "").trim();
|
|
18
|
+
if (envHome) return path.join(path.resolve(envHome), "skills");
|
|
19
|
+
return path.join(os.homedir(), ".codex", "skills");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function timestamp() {
|
|
23
|
+
const d = new Date();
|
|
24
|
+
// 2026-01-28T12:34:56Z -> 20260128-123456Z
|
|
25
|
+
return d.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z").replace("T", "-");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function codexSkillsBackupStamp() {
|
|
29
|
+
return timestamp();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} skillsDir
|
|
34
|
+
* @param {string} skillName
|
|
35
|
+
* @param {string=} stamp
|
|
36
|
+
*/
|
|
37
|
+
export function codexSkillsBackupPathFor(skillsDir, skillName, stamp) {
|
|
38
|
+
return path.join(skillsDir, ".aiws", "backups", "codex-skills", stamp || timestamp(), skillName, "SKILL.md");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} text normalized
|
|
43
|
+
* @returns {{ frontMatter: string, body: string }}
|
|
44
|
+
*/
|
|
45
|
+
function splitYamlFrontMatter(text) {
|
|
46
|
+
const t = normalizeNewlines(text);
|
|
47
|
+
const lines = t.split("\n");
|
|
48
|
+
if ((lines[0] ?? "") !== "---") {
|
|
49
|
+
throw new UserError("Invalid SKILL.md: missing YAML front matter at top", { exitCode: 1 });
|
|
50
|
+
}
|
|
51
|
+
let end = -1;
|
|
52
|
+
for (let i = 1; i < lines.length; i++) {
|
|
53
|
+
if ((lines[i] ?? "") === "---") {
|
|
54
|
+
end = i;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (end === -1) {
|
|
59
|
+
throw new UserError("Invalid SKILL.md: YAML front matter not closed", { exitCode: 1 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const frontMatter = `${lines.slice(0, end + 1).join("\n")}\n`;
|
|
63
|
+
const body = lines.slice(end + 1).join("\n");
|
|
64
|
+
return { frontMatter, body };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* List Codex repo skills in a template and prepare a managed-file wrapper for global installation.
|
|
69
|
+
*
|
|
70
|
+
* The wrapper adds an AIWS managed block after YAML front matter so updates can preserve user-added
|
|
71
|
+
* text outside the managed block.
|
|
72
|
+
*
|
|
73
|
+
* @param {{ templateId: string, templateDir: string }} tpl
|
|
74
|
+
* @returns {Promise<Array<{ rel: string, skillName: string, blockId: string, srcAbs: string, templateText: string, managedText: string, innerText: string }>>}
|
|
75
|
+
*/
|
|
76
|
+
export async function listTemplateCodexSkills(tpl) {
|
|
77
|
+
const relFiles = await listFilesRecursive(tpl.templateDir, ".agents/skills");
|
|
78
|
+
const skillFiles = relFiles
|
|
79
|
+
.filter((rel) => rel.startsWith(".agents/skills/"))
|
|
80
|
+
.filter((rel) => rel.split("/").length === 4)
|
|
81
|
+
.filter((rel) => rel.toLowerCase().endsWith("/skill.md"));
|
|
82
|
+
|
|
83
|
+
if (skillFiles.length === 0) {
|
|
84
|
+
throw new UserError(`Template has no Codex skills: ${tpl.templateId}`, {
|
|
85
|
+
details: `Missing under: ${path.join(tpl.templateDir, ".agents", "skills")}`,
|
|
86
|
+
exitCode: 1,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @type {Array<{ rel: string, skillName: string, blockId: string, srcAbs: string, templateText: string, managedText: string, innerText: string }>} */
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const rel of skillFiles) {
|
|
93
|
+
const parts = rel.split("/");
|
|
94
|
+
const skillName = parts[2] ?? "";
|
|
95
|
+
if (!skillName) continue;
|
|
96
|
+
const blockId = `codex-skill:${skillName}`;
|
|
97
|
+
const srcAbs = path.join(tpl.templateDir, ...rel.split("/"));
|
|
98
|
+
const templateText = normalizeNewlines(await readText(srcAbs));
|
|
99
|
+
const { frontMatter, body } = splitYamlFrontMatter(templateText);
|
|
100
|
+
|
|
101
|
+
const innerTextRaw = normalizeNewlines(body);
|
|
102
|
+
const innerText = innerTextRaw.endsWith("\n") ? innerTextRaw : `${innerTextRaw}\n`;
|
|
103
|
+
|
|
104
|
+
const begin = `<!-- AIWS_MANAGED_BEGIN:${blockId} -->`;
|
|
105
|
+
const end = `<!-- AIWS_MANAGED_END:${blockId} -->`;
|
|
106
|
+
const managedText = `${frontMatter}\n${begin}\n${innerText}${end}\n`;
|
|
107
|
+
|
|
108
|
+
out.push({ rel, skillName, blockId, srcAbs, templateText, managedText, innerText });
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|