@ghl-ai/aw 0.1.80 → 0.1.82
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 +150 -25
- 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
|
}
|
|
@@ -526,32 +572,91 @@ async function getProjectSourceRepo(projectRoot) {
|
|
|
526
572
|
return `local/${basename(projectRoot)}`;
|
|
527
573
|
}
|
|
528
574
|
|
|
529
|
-
|
|
530
|
-
|
|
575
|
+
// Network git ops against the docs repo always disable interactive credential
|
|
576
|
+
// prompts (so a missing/incorrect credential fails fast instead of hanging
|
|
577
|
+
// forever) and are time-bounded (so a stalled connection aborts instead of
|
|
578
|
+
// blocking the whole publish). Override the caps with AW_DOCS_GIT_TIMEOUT_MS.
|
|
579
|
+
const AW_DOCS_GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' };
|
|
580
|
+
const AW_DOCS_CLONE_TIMEOUT_MS = Number(process.env.AW_DOCS_GIT_TIMEOUT_MS) || 180000;
|
|
581
|
+
const AW_DOCS_NET_TIMEOUT_MS = Number(process.env.AW_DOCS_GIT_TIMEOUT_MS) || 120000;
|
|
531
582
|
|
|
532
|
-
|
|
533
|
-
|
|
583
|
+
async function awDocsSparseEnabled(cloneDir) {
|
|
584
|
+
try {
|
|
585
|
+
const { stdout } = await execFile('git', ['config', '--get', 'core.sparseCheckout'], {
|
|
586
|
+
cwd: cloneDir,
|
|
534
587
|
encoding: 'utf8',
|
|
535
588
|
});
|
|
589
|
+
return stdout.trim() === 'true';
|
|
590
|
+
} catch {
|
|
591
|
+
return false;
|
|
536
592
|
}
|
|
593
|
+
}
|
|
537
594
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
595
|
+
async function ensureAwDocsRepoClone(home, publishConfig, sparseIncludePaths = []) {
|
|
596
|
+
const cloneDir = process.env.AW_DOCS_WORKTREE || join(home, '.aw-ghl-aw-docs');
|
|
597
|
+
const wantSparse = sparseIncludePaths.length > 0;
|
|
598
|
+
const freshClone = !existsSync(join(cloneDir, '.git'));
|
|
599
|
+
|
|
600
|
+
if (freshClone) {
|
|
601
|
+
// A full clone of ghl-aw-docs pulls the entire org's published docs
|
|
602
|
+
// (hundreds of MB / ~170k objects) and is the dominant cause of
|
|
603
|
+
// first-publish timeouts. Clone blobless (no file contents up front) and,
|
|
604
|
+
// when we know which subtree this publish touches, defer checkout and
|
|
605
|
+
// sparse-checkout only that path — turning hundreds of MB into a few MB.
|
|
606
|
+
// Degrades gracefully: if the remote does not support partial clone git
|
|
607
|
+
// ignores the filter, and if sparse-checkout is unavailable the branch
|
|
608
|
+
// checkout below still materialises the full tree.
|
|
609
|
+
const cloneArgs = ['clone', '--filter=blob:none'];
|
|
610
|
+
if (wantSparse) cloneArgs.push('--no-checkout');
|
|
611
|
+
cloneArgs.push(publishConfig.repoUrl, cloneDir);
|
|
612
|
+
await execFile('git', cloneArgs, {
|
|
613
|
+
encoding: 'utf8',
|
|
614
|
+
env: AW_DOCS_GIT_ENV,
|
|
615
|
+
timeout: AW_DOCS_CLONE_TIMEOUT_MS,
|
|
616
|
+
});
|
|
617
|
+
if (wantSparse) {
|
|
618
|
+
await execFile('git', ['sparse-checkout', 'set', '--cone', ...sparseIncludePaths], {
|
|
619
|
+
cwd: cloneDir,
|
|
620
|
+
encoding: 'utf8',
|
|
621
|
+
}).catch(() => { /* sparse unsupported → full checkout below */ });
|
|
622
|
+
}
|
|
541
623
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
624
|
+
|
|
625
|
+
// Only validate cleanliness for a REUSED clone. A fresh blobless
|
|
626
|
+
// --no-checkout clone intentionally has no working tree yet (it is
|
|
627
|
+
// materialised by the branch checkout below), which would otherwise
|
|
628
|
+
// register as "dirty".
|
|
629
|
+
if (!freshClone) {
|
|
630
|
+
let status = await getGitStatus(cloneDir);
|
|
631
|
+
if (status.trim() && await selfHealManagedAwCacheDirtiness(cloneDir, status)) {
|
|
632
|
+
status = await getGitStatus(cloneDir);
|
|
633
|
+
}
|
|
634
|
+
if (status.trim()) {
|
|
635
|
+
throw new Error([
|
|
636
|
+
`AW docs repo worktree is dirty: ${cloneDir}`,
|
|
637
|
+
status.trim(),
|
|
638
|
+
'Clean the cached docs repo or set AW_DOCS_WORKTREE to a clean clone before publishing.',
|
|
639
|
+
].join('\n'));
|
|
640
|
+
}
|
|
548
641
|
}
|
|
549
642
|
|
|
550
643
|
await execFile('git', ['fetch', 'origin'], {
|
|
551
644
|
cwd: cloneDir,
|
|
552
645
|
encoding: 'utf8',
|
|
646
|
+
env: AW_DOCS_GIT_ENV,
|
|
647
|
+
timeout: AW_DOCS_NET_TIMEOUT_MS,
|
|
553
648
|
});
|
|
554
649
|
|
|
650
|
+
// Keep the current publish's subtree inside the sparse cone. Only applies to
|
|
651
|
+
// clones that already use sparse-checkout (i.e. the blobless clones we
|
|
652
|
+
// created) — legacy full clones are left untouched so there is no regression.
|
|
653
|
+
if (wantSparse && await awDocsSparseEnabled(cloneDir)) {
|
|
654
|
+
await execFile('git', ['sparse-checkout', 'add', ...sparseIncludePaths], {
|
|
655
|
+
cwd: cloneDir,
|
|
656
|
+
encoding: 'utf8',
|
|
657
|
+
}).catch(() => { /* best effort — checkout below still works */ });
|
|
658
|
+
}
|
|
659
|
+
|
|
555
660
|
const hasPublishBranch = await remoteBranchExists(cloneDir, publishConfig.branch);
|
|
556
661
|
const checkoutStart = hasPublishBranch
|
|
557
662
|
? `origin/${publishConfig.branch}`
|
|
@@ -562,9 +667,13 @@ async function ensureAwDocsRepoClone(home, publishConfig) {
|
|
|
562
667
|
}
|
|
563
668
|
|
|
564
669
|
try {
|
|
670
|
+
// With a blobless clone, checkout lazily fetches the missing blobs for the
|
|
671
|
+
// sparse cone — a network op, so bound it like the other remote calls.
|
|
565
672
|
await execFile('git', ['checkout', '-B', publishConfig.branch, checkoutStart], {
|
|
566
673
|
cwd: cloneDir,
|
|
567
674
|
encoding: 'utf8',
|
|
675
|
+
env: AW_DOCS_GIT_ENV,
|
|
676
|
+
timeout: AW_DOCS_NET_TIMEOUT_MS,
|
|
568
677
|
});
|
|
569
678
|
} catch (e) {
|
|
570
679
|
const healedPath = isCheckoutOverwrittenByAwPathError(e)
|
|
@@ -576,6 +685,8 @@ async function ensureAwDocsRepoClone(home, publishConfig) {
|
|
|
576
685
|
await execFile('git', ['checkout', '-B', publishConfig.branch, checkoutStart], {
|
|
577
686
|
cwd: cloneDir,
|
|
578
687
|
encoding: 'utf8',
|
|
688
|
+
env: AW_DOCS_GIT_ENV,
|
|
689
|
+
timeout: AW_DOCS_NET_TIMEOUT_MS,
|
|
579
690
|
});
|
|
580
691
|
}
|
|
581
692
|
|
|
@@ -583,6 +694,8 @@ async function ensureAwDocsRepoClone(home, publishConfig) {
|
|
|
583
694
|
await execFile('git', ['pull', '--ff-only', 'origin', publishConfig.branch], {
|
|
584
695
|
cwd: cloneDir,
|
|
585
696
|
encoding: 'utf8',
|
|
697
|
+
env: AW_DOCS_GIT_ENV,
|
|
698
|
+
timeout: AW_DOCS_NET_TIMEOUT_MS,
|
|
586
699
|
});
|
|
587
700
|
}
|
|
588
701
|
await removeTrackedAwSymlink(cloneDir);
|
|
@@ -683,6 +796,8 @@ async function commitAndPushAwDocsRepo(docsRepoDir, { message, branch }) {
|
|
|
683
796
|
await execFile('git', ['push', '-u', 'origin', `HEAD:${branch}`], {
|
|
684
797
|
cwd: docsRepoDir,
|
|
685
798
|
encoding: 'utf8',
|
|
799
|
+
env: AW_DOCS_GIT_ENV,
|
|
800
|
+
timeout: AW_DOCS_NET_TIMEOUT_MS,
|
|
686
801
|
});
|
|
687
802
|
}
|
|
688
803
|
|
|
@@ -733,6 +848,7 @@ function updateAwDocsManifest(docsRepoDir, { repoSlug, sourceRepo, githubUsernam
|
|
|
733
848
|
}
|
|
734
849
|
|
|
735
850
|
async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
|
|
851
|
+
ensureHeadlessGitEnv();
|
|
736
852
|
const { projectRoot, files } = collectProjectAwDocs(cwd, home, scope);
|
|
737
853
|
if (files.length === 0) return { hasDocs: false, publishedPaths: [] };
|
|
738
854
|
|
|
@@ -776,7 +892,12 @@ async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
|
|
|
776
892
|
const s = fmt.spinner();
|
|
777
893
|
s.start(`Publishing ${files.length} AW doc${files.length > 1 ? 's' : ''} to ${publishConfig.repo}...`);
|
|
778
894
|
try {
|
|
779
|
-
|
|
895
|
+
// Scope the (blobless) docs clone to only the subtree this publish writes,
|
|
896
|
+
// so a fresh clone fetches a few MB instead of the whole org's docs.
|
|
897
|
+
const sparseIncludePaths = isRootScope
|
|
898
|
+
? [`${publishConfig.dest}/${scope.relPrefix}`]
|
|
899
|
+
: [`${publishConfig.dest}/${repoSlug}/${githubUsername}`];
|
|
900
|
+
const docsRepoDir = await ensureAwDocsRepoClone(home, publishConfig, sparseIncludePaths);
|
|
780
901
|
const deleteTarget = isRootScope
|
|
781
902
|
? join(docsRepoDir, publishConfig.dest, scope.relPrefix)
|
|
782
903
|
: scope?.relPrefix
|
|
@@ -1326,6 +1447,8 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
|
|
|
1326
1447
|
// ── Main command ─────────────────────────────────────────────────────
|
|
1327
1448
|
|
|
1328
1449
|
export async function pushCommand(args) {
|
|
1450
|
+
configureInteractionMode(args);
|
|
1451
|
+
|
|
1329
1452
|
const input = args._positional?.[0];
|
|
1330
1453
|
const dryRun = args['--dry-run'] === true;
|
|
1331
1454
|
const docsOnly = args['--aw-docs-only'] === true || args['--docs-only'] === true;
|
|
@@ -1684,6 +1807,8 @@ export const __test__ = {
|
|
|
1684
1807
|
featureScopeFromInput,
|
|
1685
1808
|
awDocsFeatureScope,
|
|
1686
1809
|
awDocsRootScope,
|
|
1810
|
+
matchRootPrefix,
|
|
1811
|
+
normalizeAwDocsRootInput,
|
|
1687
1812
|
resolveAwDocsScope,
|
|
1688
1813
|
assertDocsOnlyScopeOrAll,
|
|
1689
1814
|
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
|
|