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.
Files changed (3) hide show
  1. package/README.md +8 -3
  2. package/dist/index.js +153 -34
  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
@@ -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: formatScripts(pkg?.scripts),
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 detectStack(pkg) {
267
- if (!pkg) return "";
268
- const deps = {
269
- ...pkg.dependencies ?? {},
270
- ...pkg.devDependencies ?? {}
271
- };
272
- const names = Object.keys(deps);
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
- for (const [pattern, label] of STACK_HINTS) {
275
- if (names.some((n) => pattern.test(n))) found.add(label);
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 formatScripts(scripts) {
280
- if (!scripts || typeof scripts !== "object") return "";
281
- return Object.entries(scripts).map(([k, v]) => `npm run ${k}: ${v}`).join("\n");
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
- console.error(`Build failed (HTTP ${res.status}).`);
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.1");
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);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptarch",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",