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.
Files changed (3) hide show
  1. package/README.md +8 -3
  2. package/dist/index.js +141 -29
  3. 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
- | `--url <url>` | Override the API base URL (saved to config) |
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 (package manifest, scripts, 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).
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: formatScripts(pkg?.scripts),
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 detectStack(pkg) {
267
- if (!pkg) return "";
268
- const deps = {
269
- ...pkg.dependencies ?? {},
270
- ...pkg.devDependencies ?? {}
271
- };
272
- const names = Object.keys(deps);
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
- for (const [pattern, label] of STACK_HINTS) {
275
- if (names.some((n) => pattern.test(n))) found.add(label);
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 formatScripts(scripts) {
280
- if (!scripts || typeof scripts !== "object") return "";
281
- return Object.entries(scripts).map(([k, v]) => `npm run ${k}: ${v}`).join("\n");
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("0.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)").option("--url <url>", "Override the API base URL").action(login);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptarch",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Generate and lint AI coding-agent context files (CLAUDE.md, AGENTS.md, Cursor rules) from your repo.",
5
5
  "keywords": [
6
6
  "claude",