promptarch 1.0.0 → 1.0.2
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 +8 -3
- package/dist/index.js +141 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,17 +50,19 @@ Store your API key (`pk_…`) in `~/.config/promptarch/config.json` (written wit
|
|
|
50
50
|
```bash
|
|
51
51
|
promptarch login # paste the key when prompted
|
|
52
52
|
promptarch login --key pk_xxx # non-interactive (CI)
|
|
53
|
-
promptarch login --url https://promptarch-preview.workers.dev # target a non-prod deploy
|
|
54
53
|
```
|
|
55
54
|
|
|
56
55
|
| Option | Description |
|
|
57
56
|
|--------|-------------|
|
|
58
57
|
| `--key <key>` | Provide the key non-interactively (for CI) |
|
|
59
|
-
|
|
58
|
+
|
|
59
|
+
> To point the CLI at a non-prod API (local dev / self-hosted), set the `PROMPTARCH_API_URL` env var.
|
|
60
60
|
|
|
61
61
|
### `promptarch init`
|
|
62
62
|
|
|
63
|
-
Scan the current repository (
|
|
63
|
+
Scan the current repository (stack manifest, build commands, directory tree, README, and any existing agent config), generate a canonical **Context Pack** via the API, and write every supported format (`CLAUDE.md`, `AGENTS.md`, Cursor `.mdc`, Copilot instructions).
|
|
64
|
+
|
|
65
|
+
Stack detection spans ecosystems, not just Node: `package.json` (JS/TS frameworks), `Package.swift`/`.xcodeproj` (Swift), `pyproject.toml`/`requirements.txt` (Python + FastAPI/Django/Flask), `go.mod` (Go), `Cargo.toml` (Rust), and `Gemfile` (Ruby/Rails). Commands are pulled from npm scripts, `Makefile` targets, and `justfile` recipes.
|
|
64
66
|
|
|
65
67
|
```bash
|
|
66
68
|
promptarch init --dry-run # print the extracted payload, no network call
|
|
@@ -72,6 +74,9 @@ promptarch init --out ./generated # write to a specific directory
|
|
|
72
74
|
|--------|-------------|
|
|
73
75
|
| `--dry-run` | Print the extracted payload without calling the API (no key needed) |
|
|
74
76
|
| `--out <dir>` | Output directory (default: current directory) |
|
|
77
|
+
| `--force` | Overwrite existing files without prompting |
|
|
78
|
+
|
|
79
|
+
> If `init` would overwrite existing files (e.g. a hand-maintained `CLAUDE.md`), it lists them and asks for confirmation first. In a non-interactive shell it aborts unless you pass `--force`. Use `--out` to write elsewhere.
|
|
75
80
|
|
|
76
81
|
> `init` consumes credits. The first generation on a new account is free.
|
|
77
82
|
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
|
-
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
7
|
+
import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
8
8
|
import { dirname, join as join3, resolve } from "path";
|
|
9
9
|
|
|
10
10
|
// ../packages/core/context-model/types.ts
|
|
@@ -250,9 +250,9 @@ async function extractRepo(cwd) {
|
|
|
250
250
|
return {
|
|
251
251
|
name,
|
|
252
252
|
description,
|
|
253
|
-
techStack: detectStack(pkg),
|
|
253
|
+
techStack: await detectStack(cwd, pkg),
|
|
254
254
|
tree: await buildTree(cwd),
|
|
255
|
-
commands:
|
|
255
|
+
commands: await detectCommands(cwd, pkg),
|
|
256
256
|
conventions: await readExistingConfig(cwd)
|
|
257
257
|
};
|
|
258
258
|
}
|
|
@@ -263,22 +263,96 @@ async function readJson(path) {
|
|
|
263
263
|
return null;
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
266
|
+
async function readFileSafe(path) {
|
|
267
|
+
try {
|
|
268
|
+
return await readFile(path, "utf8");
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async function dirHasEntry(cwd, match) {
|
|
274
|
+
try {
|
|
275
|
+
const entries = await readdir(cwd);
|
|
276
|
+
return entries.some(match);
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function detectStack(cwd, pkg) {
|
|
273
282
|
const found = /* @__PURE__ */ new Set();
|
|
274
|
-
|
|
275
|
-
|
|
283
|
+
if (pkg) {
|
|
284
|
+
const deps = {
|
|
285
|
+
...pkg.dependencies ?? {},
|
|
286
|
+
...pkg.devDependencies ?? {}
|
|
287
|
+
};
|
|
288
|
+
const names = Object.keys(deps);
|
|
289
|
+
for (const [pattern, label] of STACK_HINTS) {
|
|
290
|
+
if (names.some((n) => pattern.test(n))) found.add(label);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const swiftPkg = await readFileSafe(join(cwd, "Package.swift"));
|
|
294
|
+
const hasXcode = await dirHasEntry(
|
|
295
|
+
cwd,
|
|
296
|
+
(n) => n.endsWith(".xcodeproj") || n.endsWith(".xcworkspace")
|
|
297
|
+
);
|
|
298
|
+
if (swiftPkg !== null || hasXcode) {
|
|
299
|
+
found.add("Swift");
|
|
300
|
+
if (swiftPkg && /supabase/i.test(swiftPkg)) found.add("Supabase");
|
|
301
|
+
}
|
|
302
|
+
const pyproject = await readFileSafe(join(cwd, "pyproject.toml"));
|
|
303
|
+
const requirements = await readFileSafe(join(cwd, "requirements.txt"));
|
|
304
|
+
const hasSetupPy = await dirHasEntry(cwd, (n) => n === "setup.py");
|
|
305
|
+
if (pyproject !== null || requirements !== null || hasSetupPy) {
|
|
306
|
+
found.add("Python");
|
|
307
|
+
const py = `${pyproject ?? ""}
|
|
308
|
+
${requirements ?? ""}`;
|
|
309
|
+
if (/fastapi/i.test(py)) found.add("FastAPI");
|
|
310
|
+
if (/\bdjango\b/i.test(py)) found.add("Django");
|
|
311
|
+
if (/\bflask\b/i.test(py)) found.add("Flask");
|
|
312
|
+
if (/sqlalchemy/i.test(py)) found.add("SQLAlchemy");
|
|
313
|
+
}
|
|
314
|
+
const gomod = await readFileSafe(join(cwd, "go.mod"));
|
|
315
|
+
if (gomod !== null) found.add("Go");
|
|
316
|
+
const cargo = await readFileSafe(join(cwd, "Cargo.toml"));
|
|
317
|
+
if (cargo !== null) {
|
|
318
|
+
found.add("Rust");
|
|
319
|
+
if (/\baxum\b/i.test(cargo)) found.add("Axum");
|
|
320
|
+
if (/actix-web/i.test(cargo)) found.add("Actix");
|
|
321
|
+
}
|
|
322
|
+
const gemfile = await readFileSafe(join(cwd, "Gemfile"));
|
|
323
|
+
if (gemfile !== null) {
|
|
324
|
+
found.add("Ruby");
|
|
325
|
+
if (/\brails\b/i.test(gemfile)) found.add("Rails");
|
|
276
326
|
}
|
|
277
327
|
return [...found].join(", ");
|
|
278
328
|
}
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
329
|
+
async function detectCommands(cwd, pkg) {
|
|
330
|
+
const parts = [];
|
|
331
|
+
const scripts = pkg?.scripts;
|
|
332
|
+
if (scripts && typeof scripts === "object") {
|
|
333
|
+
parts.push(
|
|
334
|
+
Object.entries(scripts).map(([k, v]) => `npm run ${k}: ${v}`).join("\n")
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const make = await readFileSafe(join(cwd, "Makefile"));
|
|
338
|
+
if (make) {
|
|
339
|
+
const targets = parseTargets(make);
|
|
340
|
+
if (targets.length) parts.push(targets.map((t) => `make ${t}`).join("\n"));
|
|
341
|
+
}
|
|
342
|
+
const just = await readFileSafe(join(cwd, "justfile")) ?? await readFileSafe(join(cwd, "Justfile"));
|
|
343
|
+
if (just) {
|
|
344
|
+
const recipes = parseTargets(just);
|
|
345
|
+
if (recipes.length) parts.push(recipes.map((r) => `just ${r}`).join("\n"));
|
|
346
|
+
}
|
|
347
|
+
return parts.filter(Boolean).join("\n");
|
|
348
|
+
}
|
|
349
|
+
function parseTargets(text) {
|
|
350
|
+
const out = [];
|
|
351
|
+
for (const line of text.split("\n")) {
|
|
352
|
+
const m = /^([a-zA-Z][\w-]*)\s*:/.exec(line);
|
|
353
|
+
if (m && m[1] !== ".PHONY") out.push(m[1]);
|
|
354
|
+
}
|
|
355
|
+
return [...new Set(out)].slice(0, 20);
|
|
282
356
|
}
|
|
283
357
|
async function buildTree(cwd) {
|
|
284
358
|
const lines = [];
|
|
@@ -348,7 +422,31 @@ function resolveApiKey(cfg) {
|
|
|
348
422
|
return process.env.PROMPTARCH_API_KEY || cfg.apiKey;
|
|
349
423
|
}
|
|
350
424
|
|
|
425
|
+
// src/prompt.ts
|
|
426
|
+
import { createInterface } from "readline";
|
|
427
|
+
function prompt(question) {
|
|
428
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
429
|
+
return new Promise(
|
|
430
|
+
(resolve2) => rl.question(question, (answer) => {
|
|
431
|
+
rl.close();
|
|
432
|
+
resolve2(answer);
|
|
433
|
+
})
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
async function confirm(question) {
|
|
437
|
+
const ans = (await prompt(`${question} `)).trim().toLowerCase();
|
|
438
|
+
return ans === "y" || ans === "yes";
|
|
439
|
+
}
|
|
440
|
+
|
|
351
441
|
// src/commands/init.ts
|
|
442
|
+
async function fileExists(path) {
|
|
443
|
+
try {
|
|
444
|
+
await access(path);
|
|
445
|
+
return true;
|
|
446
|
+
} catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
352
450
|
async function initCmd(opts) {
|
|
353
451
|
const cwd = process.cwd();
|
|
354
452
|
const info = await extractRepo(cwd);
|
|
@@ -419,6 +517,31 @@ async function initCmd(opts) {
|
|
|
419
517
|
const pack = parseContextModel(body.prompt);
|
|
420
518
|
const files = exportPack(pack, ALL_EXPORT_FORMATS);
|
|
421
519
|
const outDir = opts.out ? resolve(cwd, opts.out) : cwd;
|
|
520
|
+
if (!opts.force) {
|
|
521
|
+
const existing = [];
|
|
522
|
+
for (const f of files) {
|
|
523
|
+
if (await fileExists(join3(outDir, f.path))) existing.push(f.path);
|
|
524
|
+
}
|
|
525
|
+
if (existing.length > 0) {
|
|
526
|
+
console.log(
|
|
527
|
+
`
|
|
528
|
+
These file(s) already exist in ${outDir}:
|
|
529
|
+
${existing.join("\n ")}`
|
|
530
|
+
);
|
|
531
|
+
if (!process.stdin.isTTY) {
|
|
532
|
+
console.error(
|
|
533
|
+
"\nAborted (non-interactive). Re-run with --force to overwrite, or --out <dir> to write elsewhere."
|
|
534
|
+
);
|
|
535
|
+
process.exitCode = 1;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const ok = await confirm("\nOverwrite them? [y/N]");
|
|
539
|
+
if (!ok) {
|
|
540
|
+
console.log("Aborted. No files written.");
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
422
545
|
for (const f of files) {
|
|
423
546
|
const dest = join3(outDir, f.path);
|
|
424
547
|
await mkdir2(dirname(dest), { recursive: true });
|
|
@@ -914,7 +1037,6 @@ ${file} - grade ${report.grade} (${report.objectiveScore}/100, ${report.format})
|
|
|
914
1037
|
}
|
|
915
1038
|
|
|
916
1039
|
// src/commands/login.ts
|
|
917
|
-
import { createInterface } from "readline";
|
|
918
1040
|
async function login(opts) {
|
|
919
1041
|
let key = opts.key;
|
|
920
1042
|
if (!key) {
|
|
@@ -927,28 +1049,18 @@ async function login(opts) {
|
|
|
927
1049
|
}
|
|
928
1050
|
const cfg = await loadConfig();
|
|
929
1051
|
cfg.apiKey = key;
|
|
930
|
-
if (opts.url) cfg.apiUrl = opts.url;
|
|
931
1052
|
await saveConfig(cfg);
|
|
932
1053
|
console.log(`Saved credentials to ${CONFIG_PATH}`);
|
|
933
1054
|
}
|
|
934
|
-
function prompt(question) {
|
|
935
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
936
|
-
return new Promise(
|
|
937
|
-
(resolve2) => rl.question(question, (answer) => {
|
|
938
|
-
rl.close();
|
|
939
|
-
resolve2(answer);
|
|
940
|
-
})
|
|
941
|
-
);
|
|
942
|
-
}
|
|
943
1055
|
|
|
944
1056
|
// src/index.ts
|
|
945
1057
|
var program = new Command();
|
|
946
1058
|
program.name("promptarch").description(
|
|
947
1059
|
"Generate and lint AI coding-agent context files (CLAUDE.md, AGENTS.md, Cursor rules) from your repo."
|
|
948
|
-
).version("
|
|
949
|
-
program.command("login").description("Store a PromptArch API key (pk_...) in ~/.config/promptarch").option("--key <key>", "API key (non-interactive; for CI)").
|
|
1060
|
+
).version("1.0.2");
|
|
1061
|
+
program.command("login").description("Store a PromptArch API key (pk_...) in ~/.config/promptarch").option("--key <key>", "API key (non-interactive; for CI)").action(login);
|
|
950
1062
|
program.command("lint").description("Lint agent context files offline (deterministic, free, CI-friendly)").argument("<files...>", "Files to lint (CLAUDE.md, AGENTS.md, *.mdc, ...)").option("--strict", "Fail on warnings too, not just errors").action((files, opts) => lintCmd(files, opts));
|
|
951
|
-
program.command("init").description("Extract repo context and generate CLAUDE.md / AGENTS.md / Cursor / Copilot files").option("--dry-run", "Print the extracted payload without calling the API").option("--out <dir>", "Output directory (default: current directory)").action(initCmd);
|
|
1063
|
+
program.command("init").description("Extract repo context and generate CLAUDE.md / AGENTS.md / Cursor / Copilot files").option("--dry-run", "Print the extracted payload without calling the API").option("--out <dir>", "Output directory (default: current directory)").option("--force", "Overwrite existing files without prompting").action(initCmd);
|
|
952
1064
|
program.parseAsync().catch((err) => {
|
|
953
1065
|
console.error(err instanceof Error ? err.message : String(err));
|
|
954
1066
|
process.exitCode = 1;
|