feature-loop-harness-cli 0.1.0
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 +53 -0
- package/bin/flh.js +391 -0
- package/package.json +29 -0
- package/templates/default/.codex/config.toml +2 -0
- package/templates/default/.codex/hooks/user-prompt-submit.sh +5 -0
- package/templates/default/.codex/hooks.json +16 -0
- package/templates/default/.flh/docs/FEATURE_IMPLEMENTATION_PIPELINE.md +454 -0
- package/templates/default/.flh/docs/PROJECT_WORKFLOW.md +270 -0
- package/templates/default/.flh/docs/REVIEW_PATCH_PIPELINE.md +166 -0
- package/templates/default/.flh/hooks/user_prompt_submit.py +1440 -0
- package/templates/default/.flh/runtime/STATE.md +84 -0
- package/templates/default/.flh/scripts/pre_commit.py +674 -0
- package/templates/default/.flh/workflow/docs-spec.yml +134 -0
- package/templates/default/.flh/workflow/flow.yml +82 -0
- package/templates/default/.flh/workflow/request-patterns.yml +265 -0
- package/templates/default/.flh/workflow/state-actions.yml +117 -0
- package/templates/default/.flh/workflow/transition-guards.yml +57 -0
- package/templates/default/.husky/pre-commit +3 -0
- package/templates/default/AGENTS.md +44 -0
- package/templates/default/HARNESS_MANUAL.md +1105 -0
- package/templates/default/README.md +251 -0
- package/templates/default/docs/API.md +41 -0
- package/templates/default/docs/ARCHITECTURE.md +86 -0
- package/templates/default/docs/DB_SCHEMA.md +149 -0
- package/templates/default/docs/DESIGN.md +52 -0
- package/templates/default/docs/MVP.md +47 -0
- package/templates/default/docs/QUALITY_SCORE.md +54 -0
- package/templates/default/docs/docs-map.md +64 -0
- package/templates/default/docs/features/active/.gitkeep +1 -0
- package/templates/default/docs/features/backlog/.gitkeep +1 -0
- package/templates/default/docs/features/blocked/.gitkeep +1 -0
- package/templates/default/docs/features/done/.gitkeep +1 -0
- package/templates/default/docs/features/feature-index.md +21 -0
- package/templates/default/docs/features/postponed/.gitkeep +1 -0
- package/templates/default/docs/features/ready/.gitkeep +1 -0
- package/templates/default/docs/features/review/.gitkeep +1 -0
- package/templates/default/docs/source-layout.yml +33 -0
- package/templates/default/gitignore.template +9 -0
- package/templates/default/tests/hooks/test_pre_commit.py +659 -0
- package/templates/default/tests/hooks/test_user_prompt_submit.py +750 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Feature Loop Harness CLI
|
|
2
|
+

|
|
3
|
+
|
|
4
|
+
npx installer CLI for Feature Loop Harness, a Codex workflow guardrail template.
|
|
5
|
+
|
|
6
|
+
For more details, or if you prefer the clone-based installation flow, see [jkuminga/FeatureLoopHarness](https://github.com/jkuminga/FeatureLoopHarness).
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
Run the installer from the root of a new or early-stage project:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx feature-loop-harness-cli init
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or target a specific directory:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx feature-loop-harness-cli init --target ./my-project
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The installer copies the harness template, merges npm scripts and dev dependencies into `package.json`, initializes Git when needed, runs `npm install`, installs Husky metadata, and runs a lightweight verification pass.
|
|
23
|
+
|
|
24
|
+
## Options
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
init [directory]
|
|
28
|
+
--target <path> Install into a target directory.
|
|
29
|
+
--force Overwrite existing harness files when conflicts are found.
|
|
30
|
+
--skip-install Do not run npm install or Husky setup.
|
|
31
|
+
--skip-verify Do not run the verification commands.
|
|
32
|
+
--skip-git Do not run git init when the target has no .git directory.
|
|
33
|
+
--dry-run Print planned actions without writing files.
|
|
34
|
+
-h, --help Show help.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## After Install
|
|
38
|
+
|
|
39
|
+
Codex project-local hook approval still requires an explicit user action. From the target project root:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
codex
|
|
43
|
+
/hooks
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Approve the `UserPromptSubmit` hook after checking that it points to `.codex/hooks/user-prompt-submit.sh`.
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm test
|
|
52
|
+
npm pack
|
|
53
|
+
```
|
package/bin/flh.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { spawnSync } = require("child_process");
|
|
7
|
+
|
|
8
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
9
|
+
const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "templates", "default");
|
|
10
|
+
const OPTIONAL_SKIP_IF_EXISTS = new Set(["README.md"]);
|
|
11
|
+
|
|
12
|
+
function printHelp() {
|
|
13
|
+
console.log(`Feature Loop Harness CLI
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
flh init [directory] [options]
|
|
17
|
+
feature-loop-harness-cli init [directory] [options]
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--target <path> Install into a target directory.
|
|
21
|
+
--force Overwrite existing harness files when conflicts are found.
|
|
22
|
+
--skip-install Do not run npm install or Husky setup.
|
|
23
|
+
--skip-verify Do not run verification commands.
|
|
24
|
+
--skip-git Do not run git init when the target has no .git directory.
|
|
25
|
+
--dry-run Print planned actions without writing files.
|
|
26
|
+
-h, --help Show help.
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fail(message) {
|
|
31
|
+
console.error(`flh: ${message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseArgs(argv) {
|
|
36
|
+
const args = [...argv];
|
|
37
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
38
|
+
return { help: true };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const command = args[0] && !args[0].startsWith("-") ? args.shift() : "init";
|
|
42
|
+
const options = {
|
|
43
|
+
target: null,
|
|
44
|
+
force: false,
|
|
45
|
+
skipInstall: false,
|
|
46
|
+
skipVerify: false,
|
|
47
|
+
skipGit: false,
|
|
48
|
+
dryRun: false,
|
|
49
|
+
};
|
|
50
|
+
let positionalTarget = null;
|
|
51
|
+
|
|
52
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
53
|
+
const arg = args[index];
|
|
54
|
+
|
|
55
|
+
if (arg === "--target") {
|
|
56
|
+
options.target = args[index + 1];
|
|
57
|
+
index += 1;
|
|
58
|
+
} else if (arg.startsWith("--target=")) {
|
|
59
|
+
options.target = arg.slice("--target=".length);
|
|
60
|
+
} else if (arg === "--force") {
|
|
61
|
+
options.force = true;
|
|
62
|
+
} else if (arg === "--skip-install") {
|
|
63
|
+
options.skipInstall = true;
|
|
64
|
+
} else if (arg === "--skip-verify") {
|
|
65
|
+
options.skipVerify = true;
|
|
66
|
+
} else if (arg === "--skip-git") {
|
|
67
|
+
options.skipGit = true;
|
|
68
|
+
} else if (arg === "--dry-run") {
|
|
69
|
+
options.dryRun = true;
|
|
70
|
+
} else if (!arg.startsWith("-") && positionalTarget === null) {
|
|
71
|
+
positionalTarget = arg;
|
|
72
|
+
} else {
|
|
73
|
+
fail(`unknown option: ${arg}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
options.target = options.target || positionalTarget || ".";
|
|
78
|
+
return { command, options };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function listFiles(root) {
|
|
82
|
+
const result = [];
|
|
83
|
+
|
|
84
|
+
function walk(current) {
|
|
85
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
86
|
+
const fullPath = path.join(current, entry.name);
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
walk(fullPath);
|
|
89
|
+
} else if (entry.isFile()) {
|
|
90
|
+
result.push(fullPath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
walk(root);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function relativeTemplatePath(filePath) {
|
|
100
|
+
return path.relative(TEMPLATE_ROOT, filePath).split(path.sep).join("/");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readJson(filePath) {
|
|
104
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function writeJson(filePath, value, dryRun) {
|
|
108
|
+
if (dryRun) {
|
|
109
|
+
console.log(`[dry-run] write ${filePath}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ensureDirectory(dir, dryRun) {
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
console.log(`[dry-run] mkdir -p ${dir}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function copyFile(src, dest, dryRun) {
|
|
124
|
+
if (dryRun) {
|
|
125
|
+
console.log(`[dry-run] copy ${relativeTemplatePath(src)} -> ${dest}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
130
|
+
fs.copyFileSync(src, dest);
|
|
131
|
+
fs.chmodSync(dest, fs.statSync(src).mode & 0o777);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function mergeGitignore(templatePath, targetPath, dryRun) {
|
|
135
|
+
const templateLines = fs
|
|
136
|
+
.readFileSync(templatePath, "utf8")
|
|
137
|
+
.split(/\r?\n/)
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
|
|
140
|
+
if (!fs.existsSync(targetPath)) {
|
|
141
|
+
copyFile(templatePath, targetPath, dryRun);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const current = fs.readFileSync(targetPath, "utf8");
|
|
146
|
+
const currentLines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
147
|
+
const missing = templateLines.filter((line) => !currentLines.has(line.trim()));
|
|
148
|
+
|
|
149
|
+
if (missing.length === 0) {
|
|
150
|
+
console.log("gitignore: already contains harness ignore rules");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (dryRun) {
|
|
155
|
+
console.log(`[dry-run] append ${missing.length} entries to ${targetPath}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const separator = current.endsWith("\n") ? "" : "\n";
|
|
160
|
+
fs.appendFileSync(targetPath, `${separator}\n# Feature Loop Harness\n${missing.join("\n")}\n`, "utf8");
|
|
161
|
+
console.log(`gitignore: appended ${missing.length} harness ignore rules`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function copyTemplate(targetDir, options) {
|
|
165
|
+
const files = listFiles(TEMPLATE_ROOT);
|
|
166
|
+
const conflicts = [];
|
|
167
|
+
|
|
168
|
+
for (const src of files) {
|
|
169
|
+
const rel = relativeTemplatePath(src);
|
|
170
|
+
const dest = path.join(targetDir, rel);
|
|
171
|
+
|
|
172
|
+
if (rel === "gitignore.template" || OPTIONAL_SKIP_IF_EXISTS.has(rel)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (fs.existsSync(dest) && !options.force) {
|
|
177
|
+
conflicts.push(rel);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (conflicts.length > 0) {
|
|
182
|
+
fail(
|
|
183
|
+
[
|
|
184
|
+
"target already contains harness files:",
|
|
185
|
+
...conflicts.map((file) => ` - ${file}`),
|
|
186
|
+
"rerun with --force only if overwriting is intended.",
|
|
187
|
+
].join("\n")
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const src of files) {
|
|
192
|
+
const rel = relativeTemplatePath(src);
|
|
193
|
+
const dest = path.join(targetDir, rel);
|
|
194
|
+
|
|
195
|
+
if (rel === "gitignore.template") {
|
|
196
|
+
mergeGitignore(src, path.join(targetDir, ".gitignore"), options.dryRun);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (OPTIONAL_SKIP_IF_EXISTS.has(rel) && fs.existsSync(dest) && !options.force) {
|
|
201
|
+
console.log(`${rel}: kept existing file`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
copyFile(src, dest, options.dryRun);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function packageNameFromDirectory(targetDir) {
|
|
210
|
+
const fallback = "feature-loop-project";
|
|
211
|
+
const name = path.basename(targetDir).toLowerCase();
|
|
212
|
+
const normalized = name
|
|
213
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
214
|
+
.replace(/^-+|-+$/g, "");
|
|
215
|
+
return normalized || fallback;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function mergePackageJson(targetDir, dryRun) {
|
|
219
|
+
const packagePath = path.join(targetDir, "package.json");
|
|
220
|
+
const exists = fs.existsSync(packagePath);
|
|
221
|
+
const pkg = exists
|
|
222
|
+
? readJson(packagePath)
|
|
223
|
+
: {
|
|
224
|
+
name: packageNameFromDirectory(targetDir),
|
|
225
|
+
version: "0.1.0",
|
|
226
|
+
private: true,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
pkg.scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
230
|
+
pkg.devDependencies =
|
|
231
|
+
pkg.devDependencies && typeof pkg.devDependencies === "object" ? pkg.devDependencies : {};
|
|
232
|
+
|
|
233
|
+
if (!pkg.scripts["flh:test"]) {
|
|
234
|
+
pkg.scripts["flh:test"] =
|
|
235
|
+
"python3 -m unittest tests/hooks/test_user_prompt_submit.py tests/hooks/test_pre_commit.py";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!pkg.scripts["flh:precommit"]) {
|
|
239
|
+
pkg.scripts["flh:precommit"] = "python3 .flh/scripts/pre_commit.py";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!pkg.scripts.test) {
|
|
243
|
+
pkg.scripts.test = "npm run flh:test";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!pkg.scripts.prepare) {
|
|
247
|
+
pkg.scripts.prepare = "husky";
|
|
248
|
+
} else if (!String(pkg.scripts.prepare).includes("husky")) {
|
|
249
|
+
console.log("package.json: kept existing prepare script; run `npx husky` manually if needed");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const allDependencies = {
|
|
253
|
+
...(pkg.dependencies || {}),
|
|
254
|
+
...(pkg.devDependencies || {}),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (!allDependencies.husky) {
|
|
258
|
+
pkg.devDependencies.husky = "^9.1.7";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!allDependencies["lint-staged"]) {
|
|
262
|
+
pkg.devDependencies["lint-staged"] = "^17.0.6";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!pkg["lint-staged"] || typeof pkg["lint-staged"] !== "object") {
|
|
266
|
+
pkg["lint-staged"] = {};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!pkg["lint-staged"]["*.{js,jsx,ts,tsx}"]) {
|
|
270
|
+
pkg["lint-staged"]["*.{js,jsx,ts,tsx}"] = ["eslint --fix", "prettier --write"];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
writeJson(packagePath, pkg, dryRun);
|
|
274
|
+
console.log(`${exists ? "updated" : "created"} package.json`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function runCommand(command, args, cwd, options) {
|
|
278
|
+
const printable = [command, ...args].join(" ");
|
|
279
|
+
if (options.dryRun) {
|
|
280
|
+
console.log(`[dry-run] ${printable}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log(`running: ${printable}`);
|
|
285
|
+
const result = spawnSync(command, args, {
|
|
286
|
+
cwd,
|
|
287
|
+
stdio: "inherit",
|
|
288
|
+
shell: false,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (result.status !== 0) {
|
|
292
|
+
fail(`${printable} failed with exit code ${result.status}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function runHookSmoke(targetDir, dryRun) {
|
|
297
|
+
const hookPath = path.join(targetDir, ".codex", "hooks", "user-prompt-submit.sh");
|
|
298
|
+
if (dryRun) {
|
|
299
|
+
console.log(`[dry-run] ${hookPath} < /q hook smoke test`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const result = spawnSync(hookPath, {
|
|
304
|
+
cwd: targetDir,
|
|
305
|
+
input: "/q hook smoke test\n",
|
|
306
|
+
text: true,
|
|
307
|
+
encoding: "utf8",
|
|
308
|
+
stdout: "pipe",
|
|
309
|
+
stderr: "pipe",
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (result.status !== 0) {
|
|
313
|
+
process.stdout.write(result.stdout || "");
|
|
314
|
+
process.stderr.write(result.stderr || "");
|
|
315
|
+
fail(`hook smoke test failed with exit code ${result.status}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log("hook smoke test passed");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function initGit(targetDir, options) {
|
|
322
|
+
if (options.skipGit) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (fs.existsSync(path.join(targetDir, ".git"))) {
|
|
327
|
+
console.log("git: existing repository detected");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
runCommand("git", ["init"], targetDir, options);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function installDependencies(targetDir, options) {
|
|
335
|
+
if (options.skipInstall) {
|
|
336
|
+
console.log("npm install: skipped");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
runCommand("npm", ["install"], targetDir, options);
|
|
341
|
+
runCommand("npx", ["husky"], targetDir, options);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function verifyInstall(targetDir, options) {
|
|
345
|
+
if (options.skipVerify) {
|
|
346
|
+
console.log("verification: skipped");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
runCommand("npm", ["run", "flh:test"], targetDir, options);
|
|
351
|
+
runHookSmoke(targetDir, options.dryRun);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function printNextSteps(targetDir) {
|
|
355
|
+
console.log(`
|
|
356
|
+
Feature Loop Harness installed in:
|
|
357
|
+
${targetDir}
|
|
358
|
+
|
|
359
|
+
Next steps:
|
|
360
|
+
1. Run: codex
|
|
361
|
+
2. Inside Codex, run: /hooks
|
|
362
|
+
3. Approve the UserPromptSubmit hook after checking .codex/hooks/user-prompt-submit.sh.
|
|
363
|
+
`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function main() {
|
|
367
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
368
|
+
if (parsed.help) {
|
|
369
|
+
printHelp();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (parsed.command !== "init") {
|
|
374
|
+
fail(`unknown command: ${parsed.command}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!fs.existsSync(TEMPLATE_ROOT)) {
|
|
378
|
+
fail(`template directory is missing: ${TEMPLATE_ROOT}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const targetDir = path.resolve(process.cwd(), parsed.options.target);
|
|
382
|
+
ensureDirectory(targetDir, parsed.options.dryRun);
|
|
383
|
+
copyTemplate(targetDir, parsed.options);
|
|
384
|
+
mergePackageJson(targetDir, parsed.options.dryRun);
|
|
385
|
+
initGit(targetDir, parsed.options);
|
|
386
|
+
installDependencies(targetDir, parsed.options);
|
|
387
|
+
verifyInstall(targetDir, parsed.options);
|
|
388
|
+
printNextSteps(targetDir);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "feature-loop-harness-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "npx installer CLI for Feature Loop Harness, a Codex workflow guardrail template.",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"flh": "bin/flh.js",
|
|
9
|
+
"feature-loop-harness-cli": "bin/flh.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"templates",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node bin/flh.js --help && npm pack --dry-run"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"codex",
|
|
24
|
+
"workflow",
|
|
25
|
+
"harness",
|
|
26
|
+
"npx",
|
|
27
|
+
"cli"
|
|
28
|
+
]
|
|
29
|
+
}
|