@hivecommons/promptargs 0.6.0 → 0.8.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 CHANGED
@@ -1,6 +1,12 @@
1
+ <p align="center">
2
+ <img src="docs/assets/promptargs-logo.svg" alt="Promptargs" width="120" height="120">
3
+ </p>
4
+
1
5
  # promptargs 🏴‍☠️
2
6
 
3
- **Template arguments for AI prompts.**
7
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
8
+
9
+ Template arguments for AI prompts.
4
10
 
5
11
  You write a prompt with blanks (`{{like_this}}`), and promptargs fills them in.
6
12
 
@@ -22,6 +28,8 @@ Works as a **CLI** and as a **skill** in Claude Code, Copilot, Goose, and Bob.
22
28
 
23
29
  ## Install
24
30
 
31
+ Requires Node.js 22 or newer.
32
+
25
33
  ### CLI
26
34
 
27
35
  ```bash
@@ -34,9 +42,29 @@ Or just run it without installing:
34
42
  npx @hivecommons/promptargs help
35
43
  ```
36
44
 
37
- ### As a Slash Command / Skill
45
+ ### As an Agent Skill
46
+
47
+ promptargs ships an [Agent Skill](https://agentskills.io/specification) at [`skills/promptargs/SKILL.md`](skills/promptargs/SKILL.md), loaded by GitHub Copilot (CLI, cloud agent, VS Code agent mode) and Claude Code.
48
+
49
+ **With the GitHub CLI** (2.90.0 or later):
50
+ ```bash
51
+ gh skill install hivecommons/promptargs promptargs
52
+ ```
53
+
54
+ **Manually** (copy the `skills/promptargs/` directory):
55
+ ```bash
56
+ # Copilot
57
+ mkdir -p ~/.copilot/skills
58
+ cp -r skills/promptargs ~/.copilot/skills/
59
+
60
+ # Claude Code
61
+ mkdir -p ~/.claude/skills
62
+ cp -r skills/promptargs ~/.claude/skills/
63
+ ```
64
+
65
+ Use `.github/skills/` or `.claude/skills/` inside a project instead to share it with your team.
38
66
 
39
- This repo ships skill files for multiple AI coding tools. Install the one(s) you use:
67
+ ### As a Slash Command (other tools)
40
68
 
41
69
  **Claude Code:**
42
70
  ```bash
@@ -59,8 +87,6 @@ curl -o ~/.bob/skills/promptargs.md \
59
87
  https://raw.githubusercontent.com/hivecommons/promptargs/main/.bob/skills/promptargs.md
60
88
  ```
61
89
 
62
- **Copilot:** Automatically detected from `.github/copilot-instructions.md` when you clone this repo.
63
-
64
90
  ---
65
91
 
66
92
  ## Quick Start (3 steps)
@@ -116,7 +142,15 @@ A blank looks like this: `{{name}}`
116
142
 
117
143
  A blank with a default looks like this: `{{name=hello}}` (promptargs extension)
118
144
 
119
- That's the basics. You also get the full Mustache spec — comments (`{{! ignore me}}`), sections, partials, and more.
145
+ Padding inside the braces is fine too — `{{ name }}` and `{{ name=hello }}` mean the same thing.
146
+
147
+ Names may use letters, digits, `_` and `-` (`{{file-name}}`). Dotted names such as `{{repo.owner}}` are rejected with an error — use `{{repo_owner}}`; the switch names `no-interactive`, `cross`, `json` and `status` are also rejected, as they are reserved for CLI switches. Likewise, the template file names `help`, `env`, `ui`, `init`, `list` and `show` are reserved for the subcommands of the same name; such a template is listed as unreachable by `promptargs list` (and `show` warns), because the subcommand always wins.
148
+
149
+ A default may appear on any occurrence of a blank (`{{name}} … {{name=hello}}`); if two occurrences give different defaults, the first one wins.
150
+
151
+ That's the basics. Mustache comments (`{{! ignore me}}`) work as documented and are safe to use.
152
+
153
+ Mustache **sections** (`{{#flag}}...{{/flag}}`) and **partials** (`{{>name}}`) are parsed too, but aren't practically usable yet: promptargs only resolves plain `{{name}}` / `{{name=default}}` tags from `--flag=value`, auto-detected context, defaults, or interactive fill — a section's condition name is never filled this way (so its content is always dropped, or always kept for an inverted `{{^name}}` section), and partial content is never supplied, so `{{>name}}` always expands to nothing. Stick to plain blanks and comments; avoid sections and partials for now.
120
154
 
121
155
  ### Red means "you need to fill this in"
122
156
 
@@ -212,6 +246,34 @@ promptargs "Review {{file}} for {{focus}}" \
212
246
  ✅ [3/3]: file=db.go focus=correctness
213
247
  ```
214
248
 
249
+ When arrays have different lengths, the number of runs is the length of the longest array, and shorter arrays repeat (cycle around):
250
+
251
+ ```bash
252
+ promptargs "Review {{file}} for {{focus}}" \
253
+ --file=api.go,auth.go,db.go \
254
+ --focus=security
255
+ ```
256
+
257
+ ```
258
+ ✅ [1/3]: file=api.go focus=security
259
+ ✅ [2/3]: file=auth.go focus=security
260
+ ✅ [3/3]: file=db.go focus=security
261
+ ```
262
+
263
+ Or with two unequal arrays:
264
+
265
+ ```bash
266
+ promptargs "Review {{file}} for {{focus}}" \
267
+ --file=a.go,b.go,c.go \
268
+ --focus=x,y
269
+ ```
270
+
271
+ ```
272
+ ✅ [1/3]: file=a.go focus=x
273
+ ✅ [2/3]: file=b.go focus=y
274
+ ✅ [3/3]: file=c.go focus=x
275
+ ```
276
+
215
277
  **Cross-product mode (`--cross`):** every combination of every value:
216
278
 
217
279
  ```bash
@@ -246,12 +308,20 @@ promptargs "Review {{file}} for {{focus}}" \
246
308
 
247
309
  Project templates override personal ones with the same name.
248
310
 
311
+ Symlinked `*.md` entries are followed only when their real target is inside the same `.prompts/` directory (a sub-directory is fine); a symlink whose target escapes the directory is listed with an error and refused when run, so a cloned repo cannot make `promptargs` read files elsewhere on your machine. Symlinks to directories and dangling symlinks are skipped.
312
+
249
313
  ---
250
314
 
251
315
  ## Auto-Magic Variables ✨
252
316
 
253
317
  promptargs discovers variables from two sources:
254
318
 
319
+ Run `promptargs env` any time to preview exactly what's auto-detected in your current shell before you run a template:
320
+
321
+ ```bash
322
+ promptargs env
323
+ ```
324
+
255
325
  ### 1. Git context (always detected)
256
326
 
257
327
  These variable names fill themselves automatically when you're in a git repo:
@@ -277,16 +347,20 @@ promptargs "Connect to {{DATABASE_URL}} in {{AWS_REGION}}"
277
347
 
278
348
  In the **Builder UI** (`promptargs ui`), click "Show terminal env" to see all available env vars from your shell. Click any one to insert it at the cursor.
279
349
 
280
- Common useful env vars: `NODE_ENV`, `AWS_REGION`, `DATABASE_URL`, `GITHUB_TOKEN`, `CI`, `USER`, `EDITOR`, `PATH`.
350
+ Common useful env vars: `NODE_ENV`, `AWS_REGION`, `CI`, `USER`, `EDITOR`, `PATH`, `SHELL`.
351
+
352
+ Credential-looking names are intentionally **excluded** from auto-detection: anything containing `TOKEN`, `SECRET`, `PASSWORD`/`PASSWD`/`PASSPHRASE`, `CREDENTIAL`, `API_KEY`/`ACCESS_KEY`/`PRIVATE_KEY`, `BEARER`, `COOKIE`, `WEBHOOK`, `JWT`, `HMAC`, `OAUTH`, `CONNECTION_STRING`, or the `_`-delimited words `AUTH`, `KEY`/`KEYS`, `PWD`, `PAT`, `PASS`, `DSN` (so `GITHUB_TOKEN`, `SIGNING_KEY`, `MYSQL_PWD`, `GITHUB_PAT` and `SLACK_WEBHOOK_URL` are all refused, while `PATH`, `PWD`, `KEYBOARD` and `AUTHOR` still work). Credential-**shaped** values are excluded whatever the name: URLs with a password in them (`postgres://user:pass@host/db`), PEM blocks, JWTs, and well-known token prefixes (`ghp_`, `github_pat_`, `glpat-`, `sk-`, `xoxb-`, `AKIA…`, `npm_…`, …). A repo-controlled template can therefore never expand `{{GITHUB_TOKEN}}` or `{{DATABASE_URL}}` into a secret, even though an explicit `--flag=value` can still pass any value you choose. This also applies to the Builder UI's "Show terminal env" list.
281
353
 
282
354
  ### Priority
283
355
 
284
356
  You can always override any auto-detected value with a flag. The order is:
285
357
 
286
358
  1. `--flag=value` (explicit, always wins)
287
- 2. Interactive prompt (if no flag and not `--no-interactive`)
288
- 3. Auto-detect from environment
289
- 4. Template default (`{{name=fallback}}`)
359
+ 2. Auto-detect from environment (git context and terminal env vars)
360
+ 3. Template default (`{{name=fallback}}`)
361
+ 4. Interactive prompt — only for variables still unfilled, and only without `--no-interactive`
362
+
363
+ A variable that auto-detects or has a default is never prompted for; pass `--name=value` to override it.
290
364
 
291
365
  ---
292
366
 
@@ -343,6 +417,41 @@ promptargs ui --port=4000
343
417
 
344
418
  ---
345
419
 
420
+ ## Privacy & Diagnostics
421
+
422
+ promptargs does not collect or export telemetry. There is no telemetry SDK, metrics endpoint, or observability backend configured anywhere in this package — nothing you run ever phones home.
423
+
424
+ All runtime diagnostics are human-readable console output (CLI status/errors, UI startup/port messages). The **Builder UI** (`promptargs ui`) binds to loopback only (`127.0.0.1`) and rejects requests with a non-local `Host` header, so the page — including the auto-detected Git and terminal environment values it embeds (see [Auto-Magic Variables](#auto-magic-variables-)) — is never reachable from other machines on your network.
425
+
426
+ **Opt-in structured diagnostics:** set `PROMPTARGS_LOG=json` to make diagnostics (CLI errors and iteration progress, UI Host rejections, asset failures, unreadable prompts directories, and one line per UI request) print as JSON lines on stderr with a fixed field set: `event`, plus `status`, `duration_ms`, `error_class` (`usage`, `not_found`, `io`, `port_in_use`, `unknown`), `count`, `current`, `total` where relevant. Template contents, argument values, paths and error messages are never logged. When unset, output is unchanged. Nothing is sent over the network.
427
+
428
+ If observability is ever added in the future, it will be explicit opt-in, bounded to local diagnostics, and will exclude environment values, template/prompt content, file paths, diffs, and user identifiers. No network exporter will be added unless you explicitly configure a backend yourself.
429
+
430
+ ---
431
+
432
+ ## Use as a Library
433
+
434
+ The package also exports its building blocks as an ES module (Node.js 22+). Install it first with `npm install @hivecommons/promptargs`.
435
+
436
+ ```js
437
+ import { parseVars, expand } from '@hivecommons/promptargs';
438
+
439
+ const template = 'Review {{file}} for {{focus=correctness}} issues.';
440
+
441
+ parseVars(template);
442
+ // [{ name: 'file', ... }, { name: 'focus', defaultValue: 'correctness', ... }]
443
+
444
+ expand(template, { file: 'src/app.ts', focus: 'security' });
445
+ // 'Review src/app.ts for security issues.'
446
+
447
+ expand(template, { file: 'src/app.ts' });
448
+ // 'Review src/app.ts for correctness issues.' (defaults fill missing values)
449
+ ```
450
+
451
+ Variables with no value and no default are left as `{{name}}` in the output. Other exports: `autodetect`, `autodetectAll`, `AUTODETECT_VARS`, `loadTemplates`, `findTemplate`, `resolve` and `renderStatus`.
452
+
453
+ ---
454
+
346
455
  ## Write Your Own Template
347
456
 
348
457
  Create a file in `.prompts/` with any name ending in `.md`:
@@ -379,13 +488,13 @@ promptargs review --file=src/main.ts --no-interactive | claude -p "do this revie
379
488
  # Goose
380
489
  promptargs review --file=src/main.ts --no-interactive | goose run
381
490
 
382
- # Copilot
383
- promptargs review --file=src/main.ts --no-interactive | gh copilot explain
491
+ # Copilot CLI (needs the standalone `copilot` CLI, installed and signed in)
492
+ copilot -p "$(promptargs review --file=src/main.ts --no-interactive)"
384
493
  ```
385
494
 
386
495
  ### As a skill inside your AI tool
387
496
 
388
- If you installed the skill (see [Install](#as-a-skill)), use it directly inside your session:
497
+ If you installed the skill (see [Install](#as-an-agent-skill)), use it directly inside your session:
389
498
 
390
499
  **Claude Code:**
391
500
  ```
@@ -394,7 +503,7 @@ If you installed the skill (see [Install](#as-a-skill)), use it directly inside
394
503
 
395
504
  **Goose / Bob:** The skill is automatically available after install — ask your agent to "use promptargs" or run a template.
396
505
 
397
- **Copilot:** The instructions in `.github/copilot-instructions.md` teach Copilot about promptargs when you're in a repo that has it.
506
+ **Copilot:** Installed skills load automatically; ask Copilot to use promptargs.
398
507
 
399
508
  ---
400
509
 
@@ -409,6 +518,7 @@ If you installed the skill (see [Install](#as-a-skill)), use it directly inside
409
518
  | `promptargs review --file=x` | Run template with flags |
410
519
  | `promptargs review` | Run template interactively |
411
520
  | `promptargs "inline {{var}}"` | Use inline template |
521
+ | `promptargs env` | Show auto-detected variable values |
412
522
  | `--no-interactive` | Skip questions, use defaults only |
413
523
  | `--cross` | Cross-product mode (all combinations) |
414
524
  | `--json` | Output as JSON |
@@ -416,8 +526,23 @@ If you installed the skill (see [Install](#as-a-skill)), use it directly inside
416
526
  | `... \| claude -p "do this"` | Pipe to Claude |
417
527
  | `... \| goose run` | Pipe to Goose |
418
528
 
529
+ The switch names `no-interactive`, `cross`, `json` and `status` are reserved and cannot be used as template variable names.
530
+
531
+ ---
532
+
533
+ ## Contributing & Community
534
+
535
+ promptargs is part of [Hive Commons](https://hivecommons.dev), a family of tools for working with AI agents.
536
+
537
+ - Want to contribute? See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, checks, and pull request guidelines.
538
+ - Found a bug or have an idea? Open an [issue](https://github.com/hivecommons/promptargs/issues).
539
+ - Reporting a security vulnerability? Follow [SECURITY.md](SECURITY.md) to report it privately, not through public issues.
540
+ - Please follow the [Hive Commons Code of Conduct](https://github.com/hivecommons/.github/blob/main/CODE_OF_CONDUCT.md).
541
+
419
542
  ---
420
543
 
421
544
  ## License
422
545
 
423
546
  Apache-2.0
547
+
548
+ Pirate flag icon: [Twemoji](https://twemoji.twitter.com/), [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/).
@@ -2,5 +2,7 @@
2
2
  * Auto-detect variable values from the current environment.
3
3
  */
4
4
  export declare const AUTODETECT_VARS: string[];
5
+ export declare function isSensitiveEnvName(name: string): boolean;
6
+ export declare function isSensitiveEnvValue(value: string): boolean;
5
7
  export declare function autodetect(varName: string): string | undefined;
6
8
  export declare function autodetectAll(varNames: string[]): Record<string, string>;
@@ -2,9 +2,24 @@
2
2
  * Auto-detect variable values from the current environment.
3
3
  */
4
4
  import { execSync } from 'node:child_process';
5
+ // gh is the only network call on the UI startup path; bound it so a stalled gh
6
+ // (hung proxy, expired auth) cannot hang startup.
7
+ const GH_TIMEOUT_MS = 5000;
8
+ // execSync's default 1 MiB maxBuffer makes `git diff` on a large staged change
9
+ // throw ENOBUFS, which the catch below turns into a silent "nothing detected"
10
+ // (#281). Raise it so large diffs are returned rather than dropped.
11
+ const GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
5
12
  function git(cmd) {
6
13
  try {
7
- return execSync(`git ${cmd}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
14
+ const out = execSync(`git ${cmd}`, {
15
+ encoding: 'utf8',
16
+ stdio: ['pipe', 'pipe', 'pipe'],
17
+ maxBuffer: GIT_MAX_BUFFER_BYTES,
18
+ }).trim();
19
+ // Success with empty output (detached HEAD for `branch --show-current`,
20
+ // clean tree for `diff`) is "nothing detected", not an empty value —
21
+ // otherwise it would override the template default and skip the prompt.
22
+ return out || undefined;
8
23
  }
9
24
  catch {
10
25
  return undefined;
@@ -32,6 +47,7 @@ const DETECTORS = {
32
47
  const out = execSync('gh pr view --json number --jq .number', {
33
48
  encoding: 'utf8',
34
49
  stdio: ['pipe', 'pipe', 'pipe'],
50
+ timeout: GH_TIMEOUT_MS,
35
51
  }).trim();
36
52
  return out || undefined;
37
53
  }
@@ -40,15 +56,64 @@ const DETECTORS = {
40
56
  }
41
57
  },
42
58
  user: () => git('config user.name'),
43
- date: () => new Date().toISOString().split('T')[0],
59
+ date: () => new Date().toLocaleDateString('sv-SE'),
44
60
  };
45
61
  export const AUTODETECT_VARS = Object.keys(DETECTORS);
62
+ // Env var names that look like credentials. The implicit process.env fallback
63
+ // must never expand these into prompt output (templates are repo-controlled),
64
+ // and the builder UI must never embed them in the served page.
65
+ //
66
+ // Word-bounded alternatives use (^|_)…(_|$) so PATH, KEYBOARD, PWD (the shell
67
+ // cwd) and AUTHOR stay detectable while GITHUB_PAT, SIGNING_KEY, MYSQL_PWD and
68
+ // AUTH_HEADER do not.
69
+ const SENSITIVE_ENV_PATTERN = new RegExp([
70
+ 'TOKEN', 'SECRET', 'PASSWORD', 'PASSWD', 'PASSPHRASE', 'CREDENTIAL',
71
+ 'API_?KEY', 'ACCESS_KEY', 'PRIVATE_KEY', 'BEARER', 'COOKIE', 'WEBHOOK',
72
+ 'JWT', 'HMAC', 'OAUTH', 'AUTHORI[SZ]ATION', 'CONN(?:ECTION)?_?STR(?:ING)?',
73
+ '(?:^|_)AUTH(?:_|$)', '(?:^|_)KEYS?(?:_|$)', '_PWD(?:_|$)',
74
+ '(?:^|_)PAT(?:_|$)', '(?:^|_)PASS(?:_|$)', '(?:^|_)DSN(?:_|$)',
75
+ ].join('|'), 'i');
76
+ export function isSensitiveEnvName(name) {
77
+ return SENSITIVE_ENV_PATTERN.test(name);
78
+ }
79
+ // Values that are credentials regardless of what the variable is called:
80
+ // a URL carrying a password in its userinfo (DATABASE_URL, REDIS_URL,
81
+ // MONGO_URI, SENTRY_DSN), PEM blocks, JWTs, HTTP Authorization header values
82
+ // (Bearer/Basic), and the fixed prefixes vendors stamp on their tokens so
83
+ // scanners can recognise them.
84
+ const SENSITIVE_ENV_VALUE_PATTERN = new RegExp([
85
+ '^[a-z][a-z0-9+.-]*://[^/\\s:@]*:[^/\\s@]+@',
86
+ '-----BEGIN ',
87
+ '^eyJ[\\w-]+\\.eyJ[\\w-]+\\.',
88
+ '^(?:Bearer|Basic)\\s+\\S+',
89
+ '^(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]{20,}',
90
+ '^glpat-[\\w-]{20,}',
91
+ '^sk-[A-Za-z0-9_-]{20,}',
92
+ '^sk_(?:live|test)_[A-Za-z0-9]{10,}',
93
+ '^xox[abprs]-[\\w-]{10,}',
94
+ '^AKIA[0-9A-Z]{16}$',
95
+ '^AIza[\\w-]{35}$',
96
+ '^npm_[A-Za-z0-9]{36}$',
97
+ '^hvs\\.[\\w-]{20,}',
98
+ ].join('|'), 'i');
99
+ export function isSensitiveEnvValue(value) {
100
+ return SENSITIVE_ENV_VALUE_PATTERN.test(value);
101
+ }
46
102
  export function autodetect(varName) {
47
- const detector = DETECTORS[varName];
48
- if (detector)
49
- return detector();
50
- // Fall back to terminal environment variables
51
- return process.env[varName] ?? undefined;
103
+ // Own-property check: {{constructor}} / {{toString}} must not resolve to
104
+ // Object.prototype members.
105
+ if (Object.hasOwn(DETECTORS, varName))
106
+ return DETECTORS[varName]();
107
+ // Fall back to terminal environment variables — but never implicitly expand
108
+ // credential-looking names ({{GITHUB_TOKEN}} in a repo template must not
109
+ // exfiltrate secrets into the generated prompt) or credential-shaped values
110
+ // under innocuous names ({{DATABASE_URL}}). Explicit --flags still work.
111
+ if (isSensitiveEnvName(varName) || !Object.hasOwn(process.env, varName))
112
+ return undefined;
113
+ const value = process.env[varName];
114
+ if (value === undefined || isSensitiveEnvValue(value))
115
+ return undefined;
116
+ return value;
52
117
  }
53
118
  export function autodetectAll(varNames) {
54
119
  const result = {};
@@ -1 +1 @@
1
- {"version":3,"file":"autodetect.js","sourceRoot":"","sources":["../src/autodetect.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,SAAS,GAAG,CAAC,GAAW;IACtB,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,OAAO,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,SAAS,GAA6C;IAC1D,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,uBAAuB,CAAC;IAC1C,IAAI,EAAE,GAAG,EAAE;QACT,MAAM,MAAM,GAAG,GAAG,CAAC,uBAAuB,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;QACpD,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,GAAG,EAAE,GAAG,EAAE;QACR,MAAM,MAAM,GAAG,GAAG,CAAC,uBAAuB,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACjE,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,IAAI,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC;IAC/C,EAAE,EAAE,GAAG,EAAE;QACP,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,QAAQ,CAAC,uCAAuC,EAAE;gBAC5D,QAAQ,EAAE,MAAM;gBAChB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;aAChC,CAAC,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,GAAG,IAAI,SAAS,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,IAAI,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,kBAAkB,CAAC;IACnC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACnD,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAEtD,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,QAAQ;QAAE,OAAO,QAAQ,EAAE,CAAC;IAChC,8CAA8C;IAC9C,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,SAAS,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAkB;IAC9C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IAChD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"autodetect.js","sourceRoot":"","sources":["../src/autodetect.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -9,14 +9,15 @@
9
9
  * promptargs init # create .prompts/ with examples
10
10
  * promptargs show <template> # show template with vars highlighted
11
11
  */
12
- import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
13
- import { join } from 'node:path';
14
12
  import { parseVars, expand } from './parser.js';
15
- import { findTemplate, loadTemplates } from './loader.js';
13
+ import { findTemplate } from './loader.js';
16
14
  import { resolve } from './resolver.js';
17
15
  import { renderStatus } from './status.js';
18
- import { autodetect, AUTODETECT_VARS } from './autodetect.js';
19
16
  import { startUI } from './ui.js';
17
+ import { UsageError, assertNoReservedVars, parseFlags, parsePort, SUBCOMMANDS } from './flags.js';
18
+ import { diag, classifyError } from './log.js';
19
+ import { doInit, doList, doShow, printEnv } from './commands.js';
20
+ import { sanitizeForTerminal } from './sanitize.js';
20
21
  const HELP = `
21
22
  promptargs — Template arguments for AI prompts 🏴‍☠️
22
23
 
@@ -33,9 +34,9 @@ Usage:
33
34
  Flags:
34
35
  --no-interactive Skip interactive prompts (use defaults/auto only)
35
36
  --cross Cross-product mode: all combinations of array values
36
- --parallel Run array iterations in parallel (print all at once)
37
37
  --json Output as JSON instead of plain text
38
38
  --status Print status line only (for IDE integration)
39
+ --var value Same as --var=value (switches above take no value)
39
40
 
40
41
  Array values:
41
42
  --file=a.go,b.go Comma-separated → one run per value
@@ -43,54 +44,48 @@ Array values:
43
44
  --file=@list.txt File → one value per line
44
45
 
45
46
  Iteration modes:
46
- Default (zip): --file=a,b --focus=x,y → 2 runs (a+x, b+y)
47
+ Default (zip): --file=a,b --focus=x,y → 2 runs (a+x, b+y); shorter arrays repeat
47
48
  Cross-product: --file=a,b --focus=x,y --cross → 4 runs (a+x, a+y, b+x, b+y)
48
49
 
49
50
  Templates live in:
50
51
  .prompts/ Project-level (checked into repo)
51
52
  ~/.prompts/ User-level (personal templates)
52
53
  `;
53
- const EXAMPLE_REVIEW = `Review {{file}} for {{focus=correctness}} issues.
54
- Be {{tone=concise}} in your feedback.
55
- Focus on real bugs, not style nitpicks.
56
- `;
57
- const EXAMPLE_EXPLAIN = `Explain what {{file}} does in {{style=simple}} terms.
58
- Assume the reader is a {{audience=junior developer}}.
59
- `;
60
- const EXAMPLE_FIX = `Fix the {{issue}} in {{file}}.
61
- The expected behavior is: {{expected}}
62
- The actual behavior is: {{actual}}
63
- `;
54
+ // The shared set supplies the runtime gate and its names supply the key union.
55
+ // The Record makes adding a subcommand without a handler (or vice versa) a type error.
56
+ const SUBCOMMAND_HANDLERS = {
57
+ help: () => {
58
+ console.log(HELP);
59
+ printEnv();
60
+ },
61
+ env: () => printEnv(),
62
+ ui: (args) => {
63
+ const flags = parseFlags(args);
64
+ startUI(parsePort(flags));
65
+ },
66
+ init: () => doInit(),
67
+ list: () => doList(),
68
+ show: (args) => doShow(args[0]),
69
+ };
70
+ function isSubcommand(value) {
71
+ return SUBCOMMANDS.has(value);
72
+ }
64
73
  async function main() {
65
74
  const args = process.argv.slice(2);
66
- if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
75
+ if (args.length === 0 || args[0] === '--help') {
67
76
  console.log(HELP);
68
77
  printEnv();
69
78
  return;
70
79
  }
71
- if (args[0] === 'env') {
72
- printEnv();
80
+ const command = args[0];
81
+ if (isSubcommand(command)) {
82
+ await SUBCOMMAND_HANDLERS[command](args.slice(1));
73
83
  return;
74
84
  }
75
- if (args[0] === 'ui') {
76
- const flags = parseFlags(args.slice(1));
77
- const port = flags['port'] ? Number(flags['port']) : undefined;
78
- startUI(port);
79
- return;
80
- }
81
- if (args[0] === 'init') {
82
- return doInit();
83
- }
84
- if (args[0] === 'list') {
85
- return doList();
86
- }
87
- if (args[0] === 'show') {
88
- return doShow(args[1]);
89
- }
90
85
  // Run a template
91
86
  const templateName = args[0];
92
87
  const flags = parseFlags(args.slice(1));
93
- const interactive = !flags['no-interactive'];
88
+ const interactive = !flags['no-interactive'] && process.stdin.isTTY === true;
94
89
  const cross = 'cross' in flags;
95
90
  const asJson = 'json' in flags;
96
91
  const statusOnly = 'status' in flags;
@@ -104,24 +99,31 @@ async function main() {
104
99
  else {
105
100
  const tpl = findTemplate(templateName);
106
101
  if (!tpl) {
107
- console.error(`Template "${templateName}" not found.`);
108
- console.error('Run "promptargs list" to see available templates.');
109
- console.error('Run "promptargs init" to create example templates.');
110
- process.exit(1);
111
- return;
102
+ throw new UsageError(`Template "${templateName}" not found.\n` +
103
+ 'Run "promptargs list" to see available templates.\n' +
104
+ 'Run "promptargs init" to create example templates.');
112
105
  }
113
106
  content = tpl.content;
114
107
  name = tpl.name;
115
108
  }
116
109
  const vars = parseVars(content);
110
+ assertNoReservedVars(vars);
117
111
  if (vars.length === 0) {
118
- // No variables — just output the template as-is
119
- console.log(content);
112
+ // No variables — skip expand() so Mustache-style content that is not a promptargs blank stays untouched
113
+ if (statusOnly) {
114
+ console.log(renderStatus(name, vars, {}, undefined, new Set()));
115
+ }
116
+ else if (asJson) {
117
+ console.log(JSON.stringify(content, null, 2));
118
+ }
119
+ else {
120
+ console.log(content);
121
+ }
120
122
  return;
121
123
  }
122
- const { iterations } = await resolve(vars, flags, interactive, cross);
124
+ const { iterations, defaulted } = await resolve(vars, flags, interactive, cross);
123
125
  if (statusOnly) {
124
- const status = renderStatus(name, vars, iterations[0]);
126
+ const status = renderStatus(name, vars, iterations[0], undefined, defaulted);
125
127
  console.log(status);
126
128
  return;
127
129
  }
@@ -133,8 +135,8 @@ async function main() {
133
135
  const status = renderStatus(name, vars, values, {
134
136
  current: i + 1,
135
137
  total: iterations.length,
136
- });
137
- console.error(status);
138
+ }, defaulted);
139
+ diag('cli_iteration', { current: i + 1, total: iterations.length }, status);
138
140
  }
139
141
  results.push(expanded);
140
142
  }
@@ -145,101 +147,8 @@ async function main() {
145
147
  console.log(results.join('\n---\n'));
146
148
  }
147
149
  }
148
- function parseFlags(args) {
149
- const flags = {};
150
- for (const arg of args) {
151
- if (arg.startsWith('--')) {
152
- const eq = arg.indexOf('=');
153
- if (eq > 0) {
154
- flags[arg.slice(2, eq)] = arg.slice(eq + 1);
155
- }
156
- else {
157
- flags[arg.slice(2)] = 'true';
158
- }
159
- }
160
- }
161
- return flags;
162
- }
163
- function doInit() {
164
- const dir = join(process.cwd(), '.prompts');
165
- if (existsSync(dir)) {
166
- console.log('.prompts/ already exists!');
167
- return;
168
- }
169
- mkdirSync(dir, { recursive: true });
170
- writeFileSync(join(dir, 'review.md'), EXAMPLE_REVIEW);
171
- writeFileSync(join(dir, 'explain.md'), EXAMPLE_EXPLAIN);
172
- writeFileSync(join(dir, 'fix.md'), EXAMPLE_FIX);
173
- console.log('Created .prompts/ with 3 example templates:');
174
- console.log(' review.md — Code review with focus area');
175
- console.log(' explain.md — Explain code simply');
176
- console.log(' fix.md — Bug fix template');
177
- console.log('');
178
- console.log('Try it: promptargs review --file=src/main.ts');
179
- }
180
- function doList() {
181
- const templates = loadTemplates();
182
- if (templates.length === 0) {
183
- console.log('No templates found.');
184
- console.log('Run "promptargs init" to create example templates.');
185
- return;
186
- }
187
- console.log('Available templates:\n');
188
- for (const t of templates) {
189
- const vars = parseVars(t.content);
190
- const varNames = vars.map(v => {
191
- if (v.defaultValue !== undefined)
192
- return `${v.name}=${v.defaultValue}`;
193
- return v.name;
194
- });
195
- const source = t.source === 'user' ? ' (user)' : '';
196
- console.log(` ${t.name}${source}`);
197
- console.log(` vars: {{${varNames.join('}} {{')}}}`);
198
- console.log('');
199
- }
200
- }
201
- function doShow(templateName) {
202
- if (!templateName) {
203
- console.error('Usage: promptargs show <template>');
204
- process.exit(1);
205
- return;
206
- }
207
- const tpl = findTemplate(templateName);
208
- if (!tpl) {
209
- console.error(`Template "${templateName}" not found.`);
210
- process.exit(1);
211
- return;
212
- }
213
- const highlighted = tpl.content.replace(/\{\{(\w+)(?:=([^}]*))?\}\}/g, (_m, varName, defaultVal) => {
214
- if (defaultVal !== undefined) {
215
- return `\x1b[33m{{${varName}=${defaultVal}}}\x1b[0m`;
216
- }
217
- return `\x1b[31m{{${varName}}}\x1b[0m`;
218
- });
219
- console.log(`Template: ${tpl.name} (${tpl.path})\n`);
220
- console.log(highlighted);
221
- console.log('\n\x1b[31mred\x1b[0m = required \x1b[33myellow\x1b[0m = has default');
222
- }
223
- const DIFF_PREVIEW_MAX_CHARS = 60;
224
- function printEnv() {
225
- console.log('Auto-detected variables:\n');
226
- for (const name of AUTODETECT_VARS) {
227
- const value = autodetect(name);
228
- if (value !== undefined) {
229
- let display = value;
230
- if (name === 'diff' && display.length > DIFF_PREVIEW_MAX_CHARS) {
231
- display = display.slice(0, DIFF_PREVIEW_MAX_CHARS) + '...';
232
- }
233
- console.log(` \x1b[32m{{${name}}}\x1b[0m = ${display}`);
234
- }
235
- else {
236
- console.log(` \x1b[90m{{${name}}}\x1b[0m = (not detected)`);
237
- }
238
- }
239
- console.log('');
240
- }
241
150
  main().catch(err => {
242
- console.error(err.message);
151
+ diag('cli_error', { error_class: classifyError(err) }, sanitizeForTerminal(String(err.message)));
243
152
  process.exit(1);
244
153
  });
245
154
  //# sourceMappingURL=cli.js.map