@ghl-ai/aw 0.1.80 → 0.1.81

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/cli.mjs CHANGED
@@ -63,6 +63,9 @@ function parseArgs(argv) {
63
63
  } else if (arg === '-y') {
64
64
  args['-y'] = true;
65
65
  i++;
66
+ } else if (arg === '--yes' || arg === '--non-interactive' || arg === '--headless') {
67
+ args[arg] = true;
68
+ i++;
66
69
  } else if (arg === '--version') {
67
70
  args['--version'] = true;
68
71
  i++;
@@ -154,6 +157,7 @@ function printHelp() {
154
157
  sec('Upload'),
155
158
  cmd('aw push', 'Push all modified files (creates one PR)'),
156
159
  cmd('aw push --aw-docs-only --feature <slug>', 'Publish one .aw_docs feature folder and print share links'),
160
+ cmd('aw push --aw-docs-only --yes', 'Publish in CI/headless mode (no TTY spinner; auto when CI=1)'),
157
161
  cmd('aw push --aw-docs-only --all', 'Publish ALL .aw_docs feature folders (explicit opt-in; otherwise scope with --feature)'),
158
162
  cmd('aw push <path>', 'Push file, folder, or namespace to registry'),
159
163
  cmd('aw push-rules [path]', 'Push platform rules to platform-docs'),
package/commands/push.mjs CHANGED
@@ -23,7 +23,7 @@ import { createHash } from 'node:crypto';
23
23
  const exec = promisify(execCb);
24
24
  const execFile = promisify(execFileCb);
25
25
  import * as fmt from '../fmt.mjs';
26
- import { chalk } from '../fmt.mjs';
26
+ import { chalk, configureInteractionMode } from '../fmt.mjs';
27
27
  import {
28
28
  REGISTRY_REPO,
29
29
  REGISTRY_URL,
@@ -158,18 +158,64 @@ function normalizeRelPath(value) {
158
158
  // and full nested folders. Use for shared, repo-keyed doc trees such as PR
159
159
  // reviews (pr-reviews/<owner>/<repo>/pr-<n>/<run>) so they do not land under the
160
160
  // reviewer's workspace namespace as one mangled flat slug.
161
- const AW_DOCS_ROOT_NAMESPACES = ['pr-reviews'];
161
+ const AW_DOCS_ROOT_NAMESPACES = ['pr-reviews', 'security-audits'];
162
+
163
+ // Compound root prefixes (multi-segment). Longest match wins in matchRootPrefix().
164
+ const AW_DOCS_ROOT_PREFIX_RULES = [
165
+ { prefix: 'security/audit', minSegments: 4 },
166
+ { prefix: 'pr-reviews', minSegments: 4 },
167
+ { prefix: 'security-audits', minSegments: 3 },
168
+ ];
169
+
170
+ // Minimum nested depth per root namespace. A root scope's delete-then-copy
171
+ // target is the resolved subtree, so a shallow path (e.g. `pr-reviews` or
172
+ // `security-audits/<repo>`) must be rejected — otherwise it could resolve to a
173
+ // broad shared subtree and wipe unrelated runs on the remote.
174
+ // pr-reviews/<repo>/pr-<number>/<run> → 4 segments
175
+ // security-audits/<repo>/<date> → 3 segments
176
+ // security/audit/<repo>/<run-id> → 4 segments
177
+ const AW_DOCS_ROOT_MIN_SEGMENTS = {
178
+ 'pr-reviews': 4,
179
+ 'security-audits': 3,
180
+ };
181
+
182
+ function normalizeAwDocsRootInput(stripped) {
183
+ if (stripped.startsWith('security/audit/')) {
184
+ return stripped.replace(/^security\/audit\//, 'security-audits/');
185
+ }
186
+ return stripped;
187
+ }
188
+
189
+ function ensureHeadlessGitEnv() {
190
+ if (!fmt.isHeadless()) return;
191
+ if (process.env.GIT_TERMINAL_PROMPT === undefined) process.env.GIT_TERMINAL_PROMPT = '0';
192
+ if (process.env.GCM_INTERACTIVE === undefined) process.env.GCM_INTERACTIVE = 'never';
193
+ }
194
+
195
+ function matchRootPrefix(stripped) {
196
+ const value = normalizeRelPath(stripped);
197
+ const rules = [...AW_DOCS_ROOT_PREFIX_RULES].sort((a, b) => b.prefix.length - a.prefix.length);
198
+ for (const rule of rules) {
199
+ if (value === rule.prefix || value.startsWith(`${rule.prefix}/`)) {
200
+ return rule;
201
+ }
202
+ }
203
+ const first = value.split('/')[0];
204
+ if (AW_DOCS_ROOT_NAMESPACES.includes(first)) {
205
+ return { prefix: first, minSegments: AW_DOCS_ROOT_MIN_SEGMENTS[first] };
206
+ }
207
+ return null;
208
+ }
162
209
 
163
210
  function awDocsRootScope(relPath) {
164
211
  const value = normalizeRelPath(relPath);
165
212
  const segments = value.split('/');
166
- // A root scope's delete-then-copy target is the resolved subtree. Require the
167
- // full pr-reviews/<repo>/pr-<number>/<run> depth so a shallow path (e.g.
168
- // `pr-reviews` or `pr-reviews/<repo>`) can never resolve to a broad shared
169
- // subtree and wipe unrelated review runs on the remote.
170
- if (segments[0] !== 'pr-reviews' || segments.length < 4) {
213
+ const rule = matchRootPrefix(value);
214
+ if (!rule || segments.length < rule.minSegments) {
171
215
  throw new Error(
172
- `Root-scope docs publish must target pr-reviews/<repo>/pr-<number>/<run>, got "${relPath}".`
216
+ 'Root-scope docs publish must target pr-reviews/<repo>/pr-<number>/<run>, ' +
217
+ 'security-audits/<repo>/<date>, or security/audit/<repo>/<run-id>, ' +
218
+ `got "${relPath}".`
173
219
  );
174
220
  }
175
221
  for (const seg of segments) {
@@ -184,14 +230,14 @@ function featureScopeFromInput(input) {
184
230
  const value = normalizeRelPath(input);
185
231
  if (!value) return null;
186
232
 
187
- const stripped = value.replace(/^\.aw_docs\//, '');
188
- if (AW_DOCS_ROOT_NAMESPACES.includes(stripped.split('/')[0])) {
233
+ const stripped = normalizeAwDocsRootInput(value.replace(/^\.aw_docs\//, ''));
234
+ if (matchRootPrefix(stripped)) {
189
235
  return awDocsRootScope(stripped);
190
236
  }
191
237
 
192
238
  const match = value.match(/^(?:\.aw_docs\/)?features\/([^/]+)$/);
193
239
  if (!match) {
194
- throw new Error('Docs-only publish path must be .aw_docs/features/<feature-slug>, .aw_docs/pr-reviews/<...>, or use --feature <feature-slug>.');
240
+ throw new Error('Docs-only publish path must be .aw_docs/features/<feature-slug>, .aw_docs/pr-reviews/<...>, .aw_docs/security-audits/<...> (or legacy .aw_docs/security/audit/<...>), or use --feature <feature-slug>.');
195
241
  }
196
242
  return awDocsFeatureScope(match[1]);
197
243
  }
@@ -733,6 +779,7 @@ function updateAwDocsManifest(docsRepoDir, { repoSlug, sourceRepo, githubUsernam
733
779
  }
734
780
 
735
781
  async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
782
+ ensureHeadlessGitEnv();
736
783
  const { projectRoot, files } = collectProjectAwDocs(cwd, home, scope);
737
784
  if (files.length === 0) return { hasDocs: false, publishedPaths: [] };
738
785
 
@@ -1326,6 +1373,8 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
1326
1373
  // ── Main command ─────────────────────────────────────────────────────
1327
1374
 
1328
1375
  export async function pushCommand(args) {
1376
+ configureInteractionMode(args);
1377
+
1329
1378
  const input = args._positional?.[0];
1330
1379
  const dryRun = args['--dry-run'] === true;
1331
1380
  const docsOnly = args['--aw-docs-only'] === true || args['--docs-only'] === true;
@@ -1684,6 +1733,8 @@ export const __test__ = {
1684
1733
  featureScopeFromInput,
1685
1734
  awDocsFeatureScope,
1686
1735
  awDocsRootScope,
1736
+ matchRootPrefix,
1737
+ normalizeAwDocsRootInput,
1687
1738
  resolveAwDocsScope,
1688
1739
  assertDocsOnlyScopeOrAll,
1689
1740
  getGitHubUser,
package/fmt.mjs CHANGED
@@ -13,6 +13,38 @@ let _silent = false;
13
13
  export function setSilent(v) { _silent = !!v; }
14
14
  export function isSilent() { return _silent; }
15
15
 
16
+ // ─── Headless mode ───
17
+ // CI / non-TTY / --yes: skip clack TTY UI (intro, spinner, p.cancel) but keep log output.
18
+ let _headless = false;
19
+ export function setHeadless(v) { _headless = !!v; }
20
+ export function isHeadless() { return _headless; }
21
+
22
+ function envTruthy(name) {
23
+ const value = String(process.env[name] || '').trim().toLowerCase();
24
+ return value === '1' || value === 'true' || value === 'yes';
25
+ }
26
+
27
+ /** Configure silent/headless interaction before command output. */
28
+ export function configureInteractionMode(args = {}) {
29
+ if (args['--silent'] === true) {
30
+ setSilent(true);
31
+ setHeadless(true);
32
+ return { silent: true, headless: true };
33
+ }
34
+
35
+ const forceHeadless = args['--yes'] === true
36
+ || args['-y'] === true
37
+ || args['--non-interactive'] === true
38
+ || args['--headless'] === true;
39
+ const headless = forceHeadless
40
+ || envTruthy('CI')
41
+ || envTruthy('AW_NON_INTERACTIVE')
42
+ || !process.stdout.isTTY;
43
+
44
+ setHeadless(headless);
45
+ return { silent: false, headless };
46
+ }
47
+
16
48
  // ─── Banner ───
17
49
 
18
50
  // Big ASCII art icons — same height as ANSI Shadow font (6 lines)
@@ -73,14 +105,27 @@ export function banner(text, opts = {}) {
73
105
 
74
106
  // ─── Clack wrappers ───
75
107
 
76
- export const intro = (msg) => { if (!_silent) p.intro(chalk.bgHex('#FF6B35').black(` ⟁ ${msg} `)); };
77
- export const outro = (msg) => { if (!_silent) p.outro(chalk.green(msg)); };
108
+ export const intro = (msg) => { if (!_silent && !_headless) p.intro(chalk.bgHex('#FF6B35').black(` ⟁ ${msg} `)); };
109
+ export const outro = (msg) => {
110
+ if (_silent) return;
111
+ if (_headless) { console.log(msg); return; }
112
+ p.outro(chalk.green(msg));
113
+ };
78
114
  export const select = p.select;
79
115
  export const isCancel = p.isCancel;
80
116
 
81
- // Returns a real spinner when not silent, or a no-op stub when silent.
82
- const _noopSpinner = { start() {}, stop() {}, message() {} };
83
- export const spinner = () => _silent ? _noopSpinner : p.spinner();
117
+ // Returns a real spinner when interactive, or a stub in silent/headless mode.
118
+ const _silentSpinner = { start() {}, stop() {}, message() {} };
119
+ const _headlessSpinner = {
120
+ start() {},
121
+ stop(msg) { if (msg) console.log(msg); },
122
+ message() {},
123
+ };
124
+ export const spinner = () => {
125
+ if (_silent) return _silentSpinner;
126
+ if (_headless) return _headlessSpinner;
127
+ return p.spinner();
128
+ };
84
129
 
85
130
  export class CancelError extends Error {
86
131
  constructor(message, { exitCode = 1 } = {}) {
@@ -91,13 +136,15 @@ export class CancelError extends Error {
91
136
  }
92
137
 
93
138
  export function cancel(msg) {
94
- p.cancel(msg);
139
+ if (_headless) console.error(msg);
140
+ else p.cancel(msg);
95
141
  throw new CancelError(msg);
96
142
  }
97
143
 
98
144
  /** Hard exit — for use in process exception handlers where throwing is unsafe */
99
145
  export function cancelAndExit(msg) {
100
- p.cancel(msg);
146
+ if (_headless) console.error(msg);
147
+ else p.cancel(msg);
101
148
  process.exit(1);
102
149
  }
103
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.80",
3
+ "version": "0.1.81",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {