promptarch 1.0.1 → 1.0.3
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 +153 -34
- 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
|
|
@@ -243,17 +243,21 @@ var STACK_HINTS = [
|
|
|
243
243
|
[/^vitest$|^jest$/, "unit tests"],
|
|
244
244
|
[/^@playwright\/test$|^playwright$/, "Playwright E2E"]
|
|
245
245
|
];
|
|
246
|
+
var FIELD_MAX = 1900;
|
|
247
|
+
function clampField(s) {
|
|
248
|
+
return s.length > FIELD_MAX ? `${s.slice(0, FIELD_MAX - 1)}\u2026` : s;
|
|
249
|
+
}
|
|
246
250
|
async function extractRepo(cwd) {
|
|
247
251
|
const pkg = await readJson(join(cwd, "package.json"));
|
|
248
252
|
const name = pkg?.name || basename(cwd);
|
|
249
253
|
const description = pkg?.description || await firstReadmeParagraph(cwd) || "";
|
|
250
254
|
return {
|
|
251
|
-
name,
|
|
252
|
-
description,
|
|
253
|
-
techStack: detectStack(pkg),
|
|
254
|
-
tree: await buildTree(cwd),
|
|
255
|
-
commands:
|
|
256
|
-
conventions: await readExistingConfig(cwd)
|
|
255
|
+
name: clampField(name),
|
|
256
|
+
description: clampField(description),
|
|
257
|
+
techStack: clampField(await detectStack(cwd, pkg)),
|
|
258
|
+
tree: clampField(await buildTree(cwd)),
|
|
259
|
+
commands: clampField(await detectCommands(cwd, pkg)),
|
|
260
|
+
conventions: clampField(await readExistingConfig(cwd))
|
|
257
261
|
};
|
|
258
262
|
}
|
|
259
263
|
async function readJson(path) {
|
|
@@ -263,22 +267,96 @@ async function readJson(path) {
|
|
|
263
267
|
return null;
|
|
264
268
|
}
|
|
265
269
|
}
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
270
|
+
async function readFileSafe(path) {
|
|
271
|
+
try {
|
|
272
|
+
return await readFile(path, "utf8");
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function dirHasEntry(cwd, match) {
|
|
278
|
+
try {
|
|
279
|
+
const entries = await readdir(cwd);
|
|
280
|
+
return entries.some(match);
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function detectStack(cwd, pkg) {
|
|
273
286
|
const found = /* @__PURE__ */ new Set();
|
|
274
|
-
|
|
275
|
-
|
|
287
|
+
if (pkg) {
|
|
288
|
+
const deps = {
|
|
289
|
+
...pkg.dependencies ?? {},
|
|
290
|
+
...pkg.devDependencies ?? {}
|
|
291
|
+
};
|
|
292
|
+
const names = Object.keys(deps);
|
|
293
|
+
for (const [pattern, label] of STACK_HINTS) {
|
|
294
|
+
if (names.some((n) => pattern.test(n))) found.add(label);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const swiftPkg = await readFileSafe(join(cwd, "Package.swift"));
|
|
298
|
+
const hasXcode = await dirHasEntry(
|
|
299
|
+
cwd,
|
|
300
|
+
(n) => n.endsWith(".xcodeproj") || n.endsWith(".xcworkspace")
|
|
301
|
+
);
|
|
302
|
+
if (swiftPkg !== null || hasXcode) {
|
|
303
|
+
found.add("Swift");
|
|
304
|
+
if (swiftPkg && /supabase/i.test(swiftPkg)) found.add("Supabase");
|
|
305
|
+
}
|
|
306
|
+
const pyproject = await readFileSafe(join(cwd, "pyproject.toml"));
|
|
307
|
+
const requirements = await readFileSafe(join(cwd, "requirements.txt"));
|
|
308
|
+
const hasSetupPy = await dirHasEntry(cwd, (n) => n === "setup.py");
|
|
309
|
+
if (pyproject !== null || requirements !== null || hasSetupPy) {
|
|
310
|
+
found.add("Python");
|
|
311
|
+
const py = `${pyproject ?? ""}
|
|
312
|
+
${requirements ?? ""}`;
|
|
313
|
+
if (/fastapi/i.test(py)) found.add("FastAPI");
|
|
314
|
+
if (/\bdjango\b/i.test(py)) found.add("Django");
|
|
315
|
+
if (/\bflask\b/i.test(py)) found.add("Flask");
|
|
316
|
+
if (/sqlalchemy/i.test(py)) found.add("SQLAlchemy");
|
|
317
|
+
}
|
|
318
|
+
const gomod = await readFileSafe(join(cwd, "go.mod"));
|
|
319
|
+
if (gomod !== null) found.add("Go");
|
|
320
|
+
const cargo = await readFileSafe(join(cwd, "Cargo.toml"));
|
|
321
|
+
if (cargo !== null) {
|
|
322
|
+
found.add("Rust");
|
|
323
|
+
if (/\baxum\b/i.test(cargo)) found.add("Axum");
|
|
324
|
+
if (/actix-web/i.test(cargo)) found.add("Actix");
|
|
325
|
+
}
|
|
326
|
+
const gemfile = await readFileSafe(join(cwd, "Gemfile"));
|
|
327
|
+
if (gemfile !== null) {
|
|
328
|
+
found.add("Ruby");
|
|
329
|
+
if (/\brails\b/i.test(gemfile)) found.add("Rails");
|
|
276
330
|
}
|
|
277
331
|
return [...found].join(", ");
|
|
278
332
|
}
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
333
|
+
async function detectCommands(cwd, pkg) {
|
|
334
|
+
const parts = [];
|
|
335
|
+
const scripts = pkg?.scripts;
|
|
336
|
+
if (scripts && typeof scripts === "object") {
|
|
337
|
+
parts.push(
|
|
338
|
+
Object.entries(scripts).map(([k, v]) => `npm run ${k}: ${v}`).join("\n")
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const make = await readFileSafe(join(cwd, "Makefile"));
|
|
342
|
+
if (make) {
|
|
343
|
+
const targets = parseTargets(make);
|
|
344
|
+
if (targets.length) parts.push(targets.map((t) => `make ${t}`).join("\n"));
|
|
345
|
+
}
|
|
346
|
+
const just = await readFileSafe(join(cwd, "justfile")) ?? await readFileSafe(join(cwd, "Justfile"));
|
|
347
|
+
if (just) {
|
|
348
|
+
const recipes = parseTargets(just);
|
|
349
|
+
if (recipes.length) parts.push(recipes.map((r) => `just ${r}`).join("\n"));
|
|
350
|
+
}
|
|
351
|
+
return parts.filter(Boolean).join("\n");
|
|
352
|
+
}
|
|
353
|
+
function parseTargets(text) {
|
|
354
|
+
const out = [];
|
|
355
|
+
for (const line of text.split("\n")) {
|
|
356
|
+
const m = /^([a-zA-Z][\w-]*)\s*:/.exec(line);
|
|
357
|
+
if (m && m[1] !== ".PHONY") out.push(m[1]);
|
|
358
|
+
}
|
|
359
|
+
return [...new Set(out)].slice(0, 20);
|
|
282
360
|
}
|
|
283
361
|
async function buildTree(cwd) {
|
|
284
362
|
const lines = [];
|
|
@@ -348,7 +426,31 @@ function resolveApiKey(cfg) {
|
|
|
348
426
|
return process.env.PROMPTARCH_API_KEY || cfg.apiKey;
|
|
349
427
|
}
|
|
350
428
|
|
|
429
|
+
// src/prompt.ts
|
|
430
|
+
import { createInterface } from "readline";
|
|
431
|
+
function prompt(question) {
|
|
432
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
433
|
+
return new Promise(
|
|
434
|
+
(resolve2) => rl.question(question, (answer) => {
|
|
435
|
+
rl.close();
|
|
436
|
+
resolve2(answer);
|
|
437
|
+
})
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
async function confirm(question) {
|
|
441
|
+
const ans = (await prompt(`${question} `)).trim().toLowerCase();
|
|
442
|
+
return ans === "y" || ans === "yes";
|
|
443
|
+
}
|
|
444
|
+
|
|
351
445
|
// src/commands/init.ts
|
|
446
|
+
async function fileExists(path) {
|
|
447
|
+
try {
|
|
448
|
+
await access(path);
|
|
449
|
+
return true;
|
|
450
|
+
} catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
352
454
|
async function initCmd(opts) {
|
|
353
455
|
const cwd = process.cwd();
|
|
354
456
|
const info = await extractRepo(cwd);
|
|
@@ -406,7 +508,10 @@ async function initCmd(opts) {
|
|
|
406
508
|
return;
|
|
407
509
|
}
|
|
408
510
|
if (!res.ok) {
|
|
409
|
-
|
|
511
|
+
const detail = await res.json().catch(() => null);
|
|
512
|
+
console.error(
|
|
513
|
+
`Build failed (HTTP ${res.status})${detail?.error ? `: ${detail.error}` : ""}.`
|
|
514
|
+
);
|
|
410
515
|
process.exitCode = 1;
|
|
411
516
|
return;
|
|
412
517
|
}
|
|
@@ -419,6 +524,31 @@ async function initCmd(opts) {
|
|
|
419
524
|
const pack = parseContextModel(body.prompt);
|
|
420
525
|
const files = exportPack(pack, ALL_EXPORT_FORMATS);
|
|
421
526
|
const outDir = opts.out ? resolve(cwd, opts.out) : cwd;
|
|
527
|
+
if (!opts.force) {
|
|
528
|
+
const existing = [];
|
|
529
|
+
for (const f of files) {
|
|
530
|
+
if (await fileExists(join3(outDir, f.path))) existing.push(f.path);
|
|
531
|
+
}
|
|
532
|
+
if (existing.length > 0) {
|
|
533
|
+
console.log(
|
|
534
|
+
`
|
|
535
|
+
These file(s) already exist in ${outDir}:
|
|
536
|
+
${existing.join("\n ")}`
|
|
537
|
+
);
|
|
538
|
+
if (!process.stdin.isTTY) {
|
|
539
|
+
console.error(
|
|
540
|
+
"\nAborted (non-interactive). Re-run with --force to overwrite, or --out <dir> to write elsewhere."
|
|
541
|
+
);
|
|
542
|
+
process.exitCode = 1;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const ok = await confirm("\nOverwrite them? [y/N]");
|
|
546
|
+
if (!ok) {
|
|
547
|
+
console.log("Aborted. No files written.");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
422
552
|
for (const f of files) {
|
|
423
553
|
const dest = join3(outDir, f.path);
|
|
424
554
|
await mkdir2(dirname(dest), { recursive: true });
|
|
@@ -914,7 +1044,6 @@ ${file} - grade ${report.grade} (${report.objectiveScore}/100, ${report.format})
|
|
|
914
1044
|
}
|
|
915
1045
|
|
|
916
1046
|
// src/commands/login.ts
|
|
917
|
-
import { createInterface } from "readline";
|
|
918
1047
|
async function login(opts) {
|
|
919
1048
|
let key = opts.key;
|
|
920
1049
|
if (!key) {
|
|
@@ -927,28 +1056,18 @@ async function login(opts) {
|
|
|
927
1056
|
}
|
|
928
1057
|
const cfg = await loadConfig();
|
|
929
1058
|
cfg.apiKey = key;
|
|
930
|
-
if (opts.url) cfg.apiUrl = opts.url;
|
|
931
1059
|
await saveConfig(cfg);
|
|
932
1060
|
console.log(`Saved credentials to ${CONFIG_PATH}`);
|
|
933
1061
|
}
|
|
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
1062
|
|
|
944
1063
|
// src/index.ts
|
|
945
1064
|
var program = new Command();
|
|
946
1065
|
program.name("promptarch").description(
|
|
947
1066
|
"Generate and lint AI coding-agent context files (CLAUDE.md, AGENTS.md, Cursor rules) from your repo."
|
|
948
|
-
).version("1.0.
|
|
949
|
-
program.command("login").description("Store a PromptArch API key (pk_...) in ~/.config/promptarch").option("--key <key>", "API key (non-interactive; for CI)").
|
|
1067
|
+
).version("1.0.3");
|
|
1068
|
+
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
1069
|
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);
|
|
1070
|
+
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
1071
|
program.parseAsync().catch((err) => {
|
|
953
1072
|
console.error(err instanceof Error ? err.message : String(err));
|
|
954
1073
|
process.exitCode = 1;
|