@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 +4 -0
- package/commands/push.mjs +62 -11
- package/fmt.mjs +54 -7
- package/package.json +1 -1
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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 (
|
|
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) => {
|
|
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
|
|
82
|
-
const
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
+
if (_headless) console.error(msg);
|
|
147
|
+
else p.cancel(msg);
|
|
101
148
|
process.exit(1);
|
|
102
149
|
}
|
|
103
150
|
|