@imdeadpool/guardex 7.0.43 → 7.1.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 +26 -0
- package/package.json +2 -1
- package/skills/gx-act/SKILL.md +82 -0
- package/src/agents/inspect.js +17 -4
- package/src/agents/launch.js +10 -1
- package/src/agents/status.js +9 -6
- package/src/budget/index.js +2 -1
- package/src/cli/args.js +52 -2
- package/src/cli/commands/agents.js +364 -0
- package/src/cli/commands/bootstrap.js +92 -0
- package/src/cli/commands/branch.js +127 -0
- package/src/cli/commands/claude.js +674 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/finish.js +26 -0
- package/src/cli/commands/mcp.js +122 -0
- package/src/cli/commands/misc.js +304 -0
- package/src/cli/commands/pr.js +439 -0
- package/src/cli/commands/prompt.js +92 -0
- package/src/cli/commands/release.js +305 -0
- package/src/cli/commands/report.js +244 -0
- package/src/cli/commands/review.js +32 -0
- package/src/cli/commands/setup.js +242 -0
- package/src/cli/commands/status.js +338 -0
- package/src/cli/commands/watch.js +234 -0
- package/src/cli/main.js +68 -3726
- package/src/cli/shared/repo-env.js +161 -0
- package/src/cli/shared/sandbox.js +417 -0
- package/src/cli/shared/scaffolding.js +535 -0
- package/src/cli/shared/toolchain-shims.js +420 -0
- package/src/context.js +229 -11
- package/src/core/runtime.js +6 -1
- package/src/doctor/index.js +42 -13
- package/src/finish/index.js +147 -5
- package/src/finish/preflight.js +177 -0
- package/src/finish/review-gate.js +182 -0
- package/src/git/index.js +446 -4
- package/src/hooks/index.js +0 -64
- package/src/mcp/collect.js +370 -0
- package/src/mcp/server.js +157 -0
- package/src/output/index.js +67 -1
- package/src/pr-review.js +23 -0
- package/src/pr.js +381 -0
- package/src/sandbox/index.js +13 -2
- package/src/scaffold/agent-worktree-prep.js +213 -0
- package/src/scaffold/index.js +108 -10
- package/src/speckit/index.js +226 -0
- package/src/terminal/index.js +1 -76
- package/src/terminal/tmux.js +0 -1
- package/src/toolchain/index.js +20 -0
- package/templates/AGENTS.monorepo-apps.md +26 -0
- package/templates/AGENTS.multiagent-safety.md +61 -347
- package/templates/AGENTS.multiagent-safety.min.md +11 -0
- package/templates/codex/skills/gx-act/SKILL.md +82 -0
- package/templates/githooks/pre-commit +22 -19
- package/templates/scripts/agent-branch-finish.sh +8 -30
- package/templates/scripts/agent-branch-merge.sh +4 -1
- package/templates/scripts/agent-branch-start.sh +88 -3
- package/templates/scripts/agent-preflight.sh +31 -5
- package/templates/scripts/agent-worktree-prune.sh +1 -1
- package/templates/scripts/codex-agent.sh +0 -91
- package/src/agents/detect.js +0 -160
- package/src/cockpit/keybindings.js +0 -224
- package/src/cockpit/layout.js +0 -224
package/src/scaffold/index.js
CHANGED
|
@@ -14,6 +14,8 @@ const {
|
|
|
14
14
|
USER_LEVEL_SKILL_ASSETS,
|
|
15
15
|
AGENTS_MARKER_START,
|
|
16
16
|
AGENTS_MARKER_END,
|
|
17
|
+
MONOREPO_MARKER_START,
|
|
18
|
+
MONOREPO_MARKER_END,
|
|
17
19
|
GITIGNORE_MARKER_START,
|
|
18
20
|
GITIGNORE_MARKER_END,
|
|
19
21
|
SHARED_VSCODE_SETTINGS_RELATIVE,
|
|
@@ -500,31 +502,54 @@ function removeLegacyManagedRepoFile(repoRoot, relativePath, options = {}) {
|
|
|
500
502
|
return { status: dryRun ? 'would-remove' : 'removed', file: relativePath };
|
|
501
503
|
}
|
|
502
504
|
|
|
503
|
-
|
|
505
|
+
// A managed block longer than this many non-blank lines is treated as the full
|
|
506
|
+
// contract. The minimal block is ~8 lines and the full contract is ~171, so any
|
|
507
|
+
// threshold between the two is safe; 40 leaves a wide margin on both sides.
|
|
508
|
+
const FULL_BLOCK_LINE_THRESHOLD = 40;
|
|
509
|
+
|
|
510
|
+
function countNonBlankLines(text) {
|
|
511
|
+
return text.split('\n').filter((line) => line.trim().length > 0).length;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Default install ships the minimal block; the full 171-line contract is opt-in
|
|
515
|
+
// via `options.contract` (--contract / --full). Once a repo has the full block,
|
|
516
|
+
// it is never silently downgraded: an existing managed block over the line
|
|
517
|
+
// threshold keeps refreshing from the full template even without the flag.
|
|
518
|
+
function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
|
|
504
519
|
const agentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
505
|
-
const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
|
|
506
520
|
const managedRegex = new RegExp(
|
|
507
521
|
`${AGENTS_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${AGENTS_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
508
522
|
'm',
|
|
509
523
|
);
|
|
510
524
|
|
|
511
|
-
|
|
525
|
+
const existing = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, 'utf8') : null;
|
|
526
|
+
const existingBlock = existing ? existing.match(managedRegex) : null;
|
|
527
|
+
const existingIsFull = Boolean(existingBlock)
|
|
528
|
+
&& countNonBlankLines(existingBlock[0]) > FULL_BLOCK_LINE_THRESHOLD;
|
|
529
|
+
const wantFull = Boolean(options.contract) || existingIsFull;
|
|
530
|
+
|
|
531
|
+
const templateFile = wantFull
|
|
532
|
+
? 'AGENTS.multiagent-safety.md'
|
|
533
|
+
: 'AGENTS.multiagent-safety.min.md';
|
|
534
|
+
const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, templateFile), 'utf8').trimEnd();
|
|
535
|
+
const variant = wantFull ? 'full contract block' : 'minimal block';
|
|
536
|
+
|
|
537
|
+
if (existing == null) {
|
|
512
538
|
if (!dryRun) {
|
|
513
539
|
fs.writeFileSync(agentsPath, `# AGENTS\n\n${snippet}\n`, 'utf8');
|
|
514
540
|
}
|
|
515
|
-
return { status: 'created', file: 'AGENTS.md' };
|
|
541
|
+
return { status: 'created', file: 'AGENTS.md', note: variant };
|
|
516
542
|
}
|
|
517
543
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const next = existing.replace(managedRegex, snippet);
|
|
544
|
+
if (existingBlock) {
|
|
545
|
+
const next = existing.replace(managedRegex, () => snippet);
|
|
521
546
|
if (next === existing) {
|
|
522
|
-
return { status: 'unchanged', file: 'AGENTS.md' };
|
|
547
|
+
return { status: 'unchanged', file: 'AGENTS.md', note: variant };
|
|
523
548
|
}
|
|
524
549
|
if (!dryRun) {
|
|
525
550
|
fs.writeFileSync(agentsPath, next, 'utf8');
|
|
526
551
|
}
|
|
527
|
-
return { status: 'updated', file: 'AGENTS.md', note:
|
|
552
|
+
return { status: 'updated', file: 'AGENTS.md', note: `refreshed gitguardex-managed block (${variant})` };
|
|
528
553
|
}
|
|
529
554
|
|
|
530
555
|
if (existing.includes(AGENTS_MARKER_START)) {
|
|
@@ -536,7 +561,7 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
|
|
|
536
561
|
fs.writeFileSync(agentsPath, `${existing}${separator}${snippet}\n`, 'utf8');
|
|
537
562
|
}
|
|
538
563
|
|
|
539
|
-
return { status: 'updated', file: 'AGENTS.md' };
|
|
564
|
+
return { status: 'updated', file: 'AGENTS.md', note: variant };
|
|
540
565
|
}
|
|
541
566
|
|
|
542
567
|
function ensureClaudeAgentsLink(repoRoot, dryRun) {
|
|
@@ -557,6 +582,77 @@ function ensureClaudeAgentsLink(repoRoot, dryRun) {
|
|
|
557
582
|
return { status: dryRun ? 'would-create' : 'created', file: 'CLAUDE.md', note: 'symlink to AGENTS.md' };
|
|
558
583
|
}
|
|
559
584
|
|
|
585
|
+
function detectMonorepoApps(repoRoot) {
|
|
586
|
+
const appsDir = path.join(repoRoot, 'apps');
|
|
587
|
+
let stat;
|
|
588
|
+
try {
|
|
589
|
+
stat = fs.statSync(appsDir);
|
|
590
|
+
} catch {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
if (!stat.isDirectory()) return false;
|
|
594
|
+
let entries;
|
|
595
|
+
try {
|
|
596
|
+
entries = fs.readdirSync(appsDir, { withFileTypes: true });
|
|
597
|
+
} catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
return entries.some(
|
|
601
|
+
(entry) =>
|
|
602
|
+
entry.isDirectory() &&
|
|
603
|
+
fs.existsSync(path.join(appsDir, entry.name, 'package.json')),
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function ensureMonorepoAppsSnippet(repoRoot, dryRun) {
|
|
608
|
+
const agentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
609
|
+
if (!detectMonorepoApps(repoRoot)) {
|
|
610
|
+
return {
|
|
611
|
+
status: 'skipped',
|
|
612
|
+
file: 'AGENTS.md',
|
|
613
|
+
note: 'no apps/<pkg>/package.json detected — monorepo block not needed',
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
const snippet = fs
|
|
617
|
+
.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.monorepo-apps.md'), 'utf8')
|
|
618
|
+
.trimEnd();
|
|
619
|
+
const managedRegex = new RegExp(
|
|
620
|
+
`${MONOREPO_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${MONOREPO_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
621
|
+
'm',
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
// Ensure AGENTS.md exists first (created by ensureAgentsSnippet upstream).
|
|
625
|
+
if (!fs.existsSync(agentsPath)) {
|
|
626
|
+
if (!dryRun) {
|
|
627
|
+
fs.writeFileSync(agentsPath, `# AGENTS\n\n${snippet}\n`, 'utf8');
|
|
628
|
+
}
|
|
629
|
+
return { status: 'created', file: 'AGENTS.md', note: 'monorepo-apps block' };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const existing = fs.readFileSync(agentsPath, 'utf8');
|
|
633
|
+
if (managedRegex.test(existing)) {
|
|
634
|
+
const next = existing.replace(managedRegex, snippet);
|
|
635
|
+
if (next === existing) {
|
|
636
|
+
return { status: 'unchanged', file: 'AGENTS.md', note: 'monorepo-apps block' };
|
|
637
|
+
}
|
|
638
|
+
if (!dryRun) {
|
|
639
|
+
fs.writeFileSync(agentsPath, next, 'utf8');
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
status: 'updated',
|
|
643
|
+
file: 'AGENTS.md',
|
|
644
|
+
note: 'refreshed monorepo-apps block',
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
649
|
+
if (!dryRun) {
|
|
650
|
+
fs.writeFileSync(agentsPath, `${existing}${separator}${snippet}\n`, 'utf8');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return { status: 'updated', file: 'AGENTS.md', note: 'appended monorepo-apps block' };
|
|
654
|
+
}
|
|
655
|
+
|
|
560
656
|
function ensureManagedGitignore(repoRoot, dryRun) {
|
|
561
657
|
const gitignorePath = path.join(repoRoot, '.gitignore');
|
|
562
658
|
const managedBlock = [
|
|
@@ -782,6 +878,8 @@ module.exports = {
|
|
|
782
878
|
removeLegacyManagedRepoFile,
|
|
783
879
|
ensureAgentsSnippet,
|
|
784
880
|
ensureClaudeAgentsLink,
|
|
881
|
+
ensureMonorepoAppsSnippet,
|
|
882
|
+
detectMonorepoApps,
|
|
785
883
|
ensureManagedGitignore,
|
|
786
884
|
parseJsonObjectLikeFile,
|
|
787
885
|
buildRepoVscodeSettings,
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const cp = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const { TOOL_NAME, SHORT_TOOL_NAME } = require('../context');
|
|
8
|
+
|
|
9
|
+
const SPECIFY_BIN = 'specify';
|
|
10
|
+
const SPECKIT_INSTALL_HINT =
|
|
11
|
+
'uv tool install specify-cli --from git+https://github.com/github/spec-kit.git';
|
|
12
|
+
|
|
13
|
+
function whichSpecify() {
|
|
14
|
+
const result = cp.spawnSync(process.platform === 'win32' ? 'where' : 'which', [SPECIFY_BIN], {
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
});
|
|
17
|
+
if (result.status === 0 && result.stdout && result.stdout.trim()) {
|
|
18
|
+
return result.stdout.trim().split(/\r?\n/)[0];
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function specifyVersion(specifyPath) {
|
|
24
|
+
const result = cp.spawnSync(specifyPath, ['--version'], { encoding: 'utf8' });
|
|
25
|
+
if (result.status === 0 && result.stdout) {
|
|
26
|
+
return result.stdout.trim();
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isGitRepo(target) {
|
|
32
|
+
try {
|
|
33
|
+
const result = cp.spawnSync('git', ['-C', target, 'rev-parse', '--git-dir'], {
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
});
|
|
36
|
+
return result.status === 0;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function listSpecKitOpenSpecScaffolds(target) {
|
|
43
|
+
const planRoot = path.join(target, 'openspec', 'plan');
|
|
44
|
+
const changesRoot = path.join(target, 'openspec', 'changes');
|
|
45
|
+
const result = { planDirs: [], specsDirs: [] };
|
|
46
|
+
if (fs.existsSync(planRoot) && fs.statSync(planRoot).isDirectory()) {
|
|
47
|
+
for (const entry of fs.readdirSync(planRoot)) {
|
|
48
|
+
if (/^agent-.*-masterplan-setup-spec-kit-/i.test(entry)) {
|
|
49
|
+
result.planDirs.push(path.join(planRoot, entry));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (fs.existsSync(changesRoot) && fs.statSync(changesRoot).isDirectory()) {
|
|
54
|
+
for (const entry of fs.readdirSync(changesRoot)) {
|
|
55
|
+
if (/^agent-.*-setup-spec-kit-/i.test(entry)) {
|
|
56
|
+
const specsDir = path.join(changesRoot, entry, 'specs');
|
|
57
|
+
if (fs.existsSync(specsDir)) result.specsDirs.push(specsDir);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pruneSpecKitScaffolds(target, { dryRun, logger }) {
|
|
65
|
+
const found = listSpecKitOpenSpecScaffolds(target);
|
|
66
|
+
const removed = [];
|
|
67
|
+
for (const dir of [...found.planDirs, ...found.specsDirs]) {
|
|
68
|
+
if (dryRun) {
|
|
69
|
+
logger(`[${TOOL_NAME}] dry-run: would prune ${path.relative(target, dir)}`);
|
|
70
|
+
} else {
|
|
71
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
72
|
+
logger(`[${TOOL_NAME}] pruned ${path.relative(target, dir)}`);
|
|
73
|
+
}
|
|
74
|
+
removed.push(dir);
|
|
75
|
+
}
|
|
76
|
+
return removed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function runSpecifyInit(target, { dryRun, logger }) {
|
|
80
|
+
const args = ['init', '--here', '--ai', 'claude', '--force', '--ignore-agent-tools'];
|
|
81
|
+
if (dryRun) {
|
|
82
|
+
logger(`[${TOOL_NAME}] dry-run: would run \`${SPECIFY_BIN} ${args.join(' ')}\` in ${target}`);
|
|
83
|
+
return { status: 'dry-run' };
|
|
84
|
+
}
|
|
85
|
+
const result = cp.spawnSync(SPECIFY_BIN, args, {
|
|
86
|
+
cwd: target,
|
|
87
|
+
stdio: 'inherit',
|
|
88
|
+
});
|
|
89
|
+
if (result.status !== 0) {
|
|
90
|
+
throw new Error(`${SPECIFY_BIN} init exited with status ${result.status}`);
|
|
91
|
+
}
|
|
92
|
+
return { status: 'ok' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isSpecKitAlreadyInstalled(target) {
|
|
96
|
+
return fs.existsSync(path.join(target, '.specify', 'integration.json'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function installSpeckit({
|
|
100
|
+
target = process.cwd(),
|
|
101
|
+
dryRun = false,
|
|
102
|
+
prune = true,
|
|
103
|
+
force = false,
|
|
104
|
+
silent = false,
|
|
105
|
+
logger = console.log,
|
|
106
|
+
}) {
|
|
107
|
+
const resolved = path.resolve(target);
|
|
108
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
|
|
109
|
+
if (silent) {
|
|
110
|
+
logger(`[${TOOL_NAME}] ⚠️ speckit: target ${resolved} does not exist; skipping.`);
|
|
111
|
+
return { status: 'skipped', reason: 'no-target' };
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Target directory does not exist: ${resolved}`);
|
|
114
|
+
}
|
|
115
|
+
if (!isGitRepo(resolved)) {
|
|
116
|
+
logger(
|
|
117
|
+
`[${TOOL_NAME}] ⚠️ ${resolved} is not a git repo. Spec Kit will scaffold without git extension wiring.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (!force && isSpecKitAlreadyInstalled(resolved)) {
|
|
121
|
+
logger(`[${TOOL_NAME}] ✅ Spec Kit already installed at ${resolved}/.specify (use --speckit-force to reinstall).`);
|
|
122
|
+
return { status: 'already-installed', target: resolved };
|
|
123
|
+
}
|
|
124
|
+
const specifyPath = whichSpecify();
|
|
125
|
+
if (!specifyPath) {
|
|
126
|
+
if (silent) {
|
|
127
|
+
logger(
|
|
128
|
+
`[${TOOL_NAME}] ⚠️ speckit: \`${SPECIFY_BIN}\` not on PATH; skipping speckit install. ` +
|
|
129
|
+
`Install with: ${SPECKIT_INSTALL_HINT}`,
|
|
130
|
+
);
|
|
131
|
+
return { status: 'skipped', reason: 'specify-missing' };
|
|
132
|
+
}
|
|
133
|
+
throw new Error(
|
|
134
|
+
`${SPECIFY_BIN} CLI not found on PATH. Install with:\n ${SPECKIT_INSTALL_HINT}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const version = specifyVersion(specifyPath);
|
|
138
|
+
logger(`[${TOOL_NAME}] specify-cli: ${specifyPath}${version ? ` (${version})` : ''}`);
|
|
139
|
+
logger(`[${TOOL_NAME}] Running \`${SPECIFY_BIN} init --here --ai claude --force\` in ${resolved}`);
|
|
140
|
+
|
|
141
|
+
const initResult = runSpecifyInit(resolved, { dryRun, logger });
|
|
142
|
+
|
|
143
|
+
let pruned = [];
|
|
144
|
+
if (prune && (initResult.status === 'ok' || dryRun)) {
|
|
145
|
+
pruned = pruneSpecKitScaffolds(resolved, { dryRun, logger });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
logger('');
|
|
149
|
+
logger(`[${TOOL_NAME}] ✅ Spec Kit installed. Next:`);
|
|
150
|
+
logger(` - Start a fresh Claude session at ${resolved}`);
|
|
151
|
+
logger(` - Use slash skills: /speckit-constitution, /speckit-specify, /speckit-plan, /speckit-tasks, /speckit-implement`);
|
|
152
|
+
logger(` - Agent worktree flow is unchanged — run \`${SHORT_TOOL_NAME} pivot "<task>" "claude"\` to start work.`);
|
|
153
|
+
|
|
154
|
+
return { status: 'installed', specifyPath, version, dryRun, prunedScaffolds: pruned, target: resolved };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function printSpeckitHelp() {
|
|
158
|
+
const lines = [
|
|
159
|
+
`Usage: ${SHORT_TOOL_NAME} speckit [options]`,
|
|
160
|
+
'',
|
|
161
|
+
' Install Spec Kit (specify-cli) SDD slash skills into the current repo.',
|
|
162
|
+
' Runs `specify init --here --ai claude --force --ignore-agent-tools` and',
|
|
163
|
+
' prunes the heavy auto-generated openspec/plan + specs/ scaffolds the',
|
|
164
|
+
' specify-cli emits, so it composes cleanly with the existing gx workflow.',
|
|
165
|
+
'',
|
|
166
|
+
'Options:',
|
|
167
|
+
' --target <path> Run in <path> instead of cwd',
|
|
168
|
+
' --no-prune Keep the auto-generated openspec/plan + specs scaffolds',
|
|
169
|
+
' --dry-run Print actions without modifying files',
|
|
170
|
+
' -h, --help Show this help',
|
|
171
|
+
'',
|
|
172
|
+
'Prerequisite:',
|
|
173
|
+
` ${SPECIFY_BIN} CLI on PATH. Install with:`,
|
|
174
|
+
` ${SPECKIT_INSTALL_HINT}`,
|
|
175
|
+
];
|
|
176
|
+
console.log(lines.join('\n'));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function runSpeckitCommand(rawArgs) {
|
|
180
|
+
const args = Array.isArray(rawArgs) ? [...rawArgs] : [];
|
|
181
|
+
let target = process.cwd();
|
|
182
|
+
let prune = true;
|
|
183
|
+
let dryRun = false;
|
|
184
|
+
let force = false;
|
|
185
|
+
|
|
186
|
+
while (args.length > 0) {
|
|
187
|
+
const arg = args.shift();
|
|
188
|
+
if (arg === '-h' || arg === '--help' || arg === 'help') {
|
|
189
|
+
printSpeckitHelp();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (arg === '--target') {
|
|
193
|
+
const next = args.shift();
|
|
194
|
+
if (!next) throw new Error('--target requires a path value');
|
|
195
|
+
target = next;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (arg === '--no-prune') {
|
|
199
|
+
prune = false;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (arg === '--prune') {
|
|
203
|
+
prune = true;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (arg === '--dry-run') {
|
|
207
|
+
dryRun = true;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (arg === '--force' || arg === '--reinstall') {
|
|
211
|
+
force = true;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
installSpeckit({ target, prune, dryRun, force });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
runSpeckitCommand,
|
|
222
|
+
installSpeckit,
|
|
223
|
+
pruneSpecKitScaffolds,
|
|
224
|
+
whichSpecify,
|
|
225
|
+
isSpecKitAlreadyInstalled,
|
|
226
|
+
};
|
package/src/terminal/index.js
CHANGED
|
@@ -14,6 +14,7 @@ function normalizeBackendName(value, fallback = DEFAULT_BACKEND) {
|
|
|
14
14
|
return normalized;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Internal: build the kitty/tmux backend pair consumed by selectTerminalBackend.
|
|
17
18
|
function createBackends(options = {}) {
|
|
18
19
|
return {
|
|
19
20
|
kitty: options.kittyBackend || kitty.createBackend(options.kitty || {}),
|
|
@@ -21,73 +22,6 @@ function createBackends(options = {}) {
|
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
function firstText(...values) {
|
|
25
|
-
for (const value of values) {
|
|
26
|
-
if (typeof value === 'string' && value.trim().length > 0) return value.trim();
|
|
27
|
-
}
|
|
28
|
-
return '';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function metadataOf(target = {}) {
|
|
32
|
-
return target.metadata && typeof target.metadata === 'object' ? target.metadata : {};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function terminalOf(target = {}) {
|
|
36
|
-
return target.terminal && typeof target.terminal === 'object' ? target.terminal : {};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function tmuxOf(target = {}) {
|
|
40
|
-
return target.tmux && typeof target.tmux === 'object' ? target.tmux : {};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function kittyOf(target = {}) {
|
|
44
|
-
return target.kitty && typeof target.kitty === 'object' ? target.kitty : {};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function resolveTargetBackendName(target = {}, fallback = '') {
|
|
48
|
-
const metadata = metadataOf(target);
|
|
49
|
-
const terminal = terminalOf(target);
|
|
50
|
-
const explicit = firstText(
|
|
51
|
-
target.terminalBackend,
|
|
52
|
-
target.backend,
|
|
53
|
-
terminal.backend,
|
|
54
|
-
metadata.terminalBackend,
|
|
55
|
-
metadata['terminal.backend'],
|
|
56
|
-
);
|
|
57
|
-
if (explicit) return normalizeBackendName(explicit);
|
|
58
|
-
|
|
59
|
-
const tmux = tmuxOf(target);
|
|
60
|
-
if (firstText(target.paneId, target.tmuxPaneId, target.tmuxTarget, tmux.paneId, tmux.target, metadata.tmuxPaneId, metadata['tmux.paneId'])) {
|
|
61
|
-
return 'tmux';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const kittyTarget = kittyOf(target);
|
|
65
|
-
if (firstText(
|
|
66
|
-
target.kittyMatch,
|
|
67
|
-
target.match,
|
|
68
|
-
target.kittyWindowId,
|
|
69
|
-
target.windowId,
|
|
70
|
-
target.kittyTitle,
|
|
71
|
-
target.windowTitle,
|
|
72
|
-
terminal.match,
|
|
73
|
-
terminal.windowId,
|
|
74
|
-
terminal.title,
|
|
75
|
-
kittyTarget.match,
|
|
76
|
-
kittyTarget.windowId,
|
|
77
|
-
kittyTarget.title,
|
|
78
|
-
metadata.kittyMatch,
|
|
79
|
-
metadata['kitty.match'],
|
|
80
|
-
metadata.kittyWindowId,
|
|
81
|
-
metadata['kitty.windowId'],
|
|
82
|
-
metadata.kittyTitle,
|
|
83
|
-
metadata['kitty.title'],
|
|
84
|
-
)) {
|
|
85
|
-
return 'kitty';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return fallback ? normalizeBackendName(fallback) : '';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
25
|
function selectTerminalBackend(value = DEFAULT_BACKEND, options = {}) {
|
|
92
26
|
const name = normalizeBackendName(value);
|
|
93
27
|
const backends = createBackends(options);
|
|
@@ -102,19 +36,10 @@ function selectTerminalBackend(value = DEFAULT_BACKEND, options = {}) {
|
|
|
102
36
|
return backends[name];
|
|
103
37
|
}
|
|
104
38
|
|
|
105
|
-
function selectTerminalBackendForTarget(target = {}, options = {}) {
|
|
106
|
-
const name = resolveTargetBackendName(target, options.defaultBackend);
|
|
107
|
-
if (!name) return null;
|
|
108
|
-
return selectTerminalBackend(name, options);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
39
|
module.exports = {
|
|
112
40
|
DEFAULT_BACKEND,
|
|
113
41
|
normalizeBackendName,
|
|
114
|
-
resolveTargetBackendName,
|
|
115
42
|
selectTerminalBackend,
|
|
116
|
-
selectTerminalBackendForTarget,
|
|
117
|
-
createBackends,
|
|
118
43
|
kitty,
|
|
119
44
|
tmux,
|
|
120
45
|
};
|
package/src/terminal/tmux.js
CHANGED
package/src/toolchain/index.js
CHANGED
|
@@ -293,7 +293,24 @@ function buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools)
|
|
|
293
293
|
return `${dependencyPrefix}Install missing companion tools now? (${installCommands.join(' && ')})`;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
// Process-scoped memo for the slow `npm list -g` probe (~1.6-2.4s). Detection is
|
|
297
|
+
// invariant within one gx invocation, but the bare-`gx`/`gx status` path queries
|
|
298
|
+
// it twice (self-update check + status snapshot). Busted after a global install.
|
|
299
|
+
let globalToolchainDetectionCache = null;
|
|
300
|
+
|
|
301
|
+
function resetGlobalToolchainDetectionCache() {
|
|
302
|
+
globalToolchainDetectionCache = null;
|
|
303
|
+
}
|
|
304
|
+
|
|
296
305
|
function detectGlobalToolchainPackages() {
|
|
306
|
+
if (globalToolchainDetectionCache) {
|
|
307
|
+
return globalToolchainDetectionCache;
|
|
308
|
+
}
|
|
309
|
+
globalToolchainDetectionCache = computeGlobalToolchainPackages();
|
|
310
|
+
return globalToolchainDetectionCache;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function computeGlobalToolchainPackages() {
|
|
297
314
|
const result = run(NPM_BIN, ['list', '-g', '--depth=0', '--json']);
|
|
298
315
|
if (result.status !== 0) {
|
|
299
316
|
const stderr = (result.stderr || '').trim();
|
|
@@ -563,6 +580,8 @@ function performCompanionInstall(missingPackages, missingLocalTools) {
|
|
|
563
580
|
};
|
|
564
581
|
}
|
|
565
582
|
installed.push(...missingPackages);
|
|
583
|
+
// Global package set changed; drop the memo so any later detection re-probes.
|
|
584
|
+
resetGlobalToolchainDetectionCache();
|
|
566
585
|
}
|
|
567
586
|
|
|
568
587
|
for (const tool of missingLocalTools) {
|
|
@@ -599,6 +618,7 @@ module.exports = {
|
|
|
599
618
|
describeCompanionInstallCommands,
|
|
600
619
|
buildMissingCompanionInstallPrompt,
|
|
601
620
|
detectGlobalToolchainPackages,
|
|
621
|
+
resetGlobalToolchainDetectionCache,
|
|
602
622
|
detectRequiredSystemTools,
|
|
603
623
|
detectOptionalLocalCompanionTools,
|
|
604
624
|
askGlobalInstallForMissing,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!-- monorepo-apps:START -->
|
|
2
|
+
## Monorepo workflow (`apps/*`)
|
|
3
|
+
|
|
4
|
+
This repo has `apps/*` (storefront, backend, etc.). The **root worktree is kept on the protected base branch** so the user can keep `pnpm <app>:dev` running there and see merged-to-main state in real time. Never edit or commit on the root worktree.
|
|
5
|
+
|
|
6
|
+
### Per-task loop
|
|
7
|
+
|
|
8
|
+
1. **Start in a sibling worktree.** Run `gx pivot` (auto) or `gx branch start --type <kind> --task <slug>` — both spawn a worktree under `.omx/agent-worktrees/` on a fresh `agent/*` branch.
|
|
9
|
+
2. **Run scoped dev servers from your worktree**, e.g. `pnpm --filter storefront dev` from `.omx/agent-worktrees/<your>/`. Pick a non-conflicting port if the user is already running the root.
|
|
10
|
+
3. **Commit + push** as you go — the agent branch tracks `origin/agent/*`. The user can watch your branch live in their git client.
|
|
11
|
+
4. **Ship via PR.** When the user approves the work, run `gx ship` (alias for `gx finish --via-pr --wait-for-merge --cleanup`). This: opens a PR → auto-merges to the protected base → prunes the worktree + branch.
|
|
12
|
+
5. The user's root worktree is now showing the merged result on next pull.
|
|
13
|
+
|
|
14
|
+
### Cross-app guardrails
|
|
15
|
+
|
|
16
|
+
- Edits to **both** `apps/storefront` AND `apps/backend` in one branch → split into two PRs unless they must land atomically. Reviews stay clean, rollbacks stay surgical.
|
|
17
|
+
- Edits to root configs (`pnpm-workspace.yaml`, `turbo.json`, `package.json`) lock every other agent. Claim → change → release fast.
|
|
18
|
+
- Migrations under `apps/backend/src/migrations/*` require explicit user OK before commit — they're irreversible on prod.
|
|
19
|
+
- Don't `pnpm install` from the root unless the user asks; do it inside your worktree if you added a dep.
|
|
20
|
+
|
|
21
|
+
### What the user sees
|
|
22
|
+
|
|
23
|
+
- `git log --all --graph --oneline` shows every active agent branch in real time.
|
|
24
|
+
- `gx status` lists active worktrees + their branches.
|
|
25
|
+
- Each `gx ship` produces a PR — link goes in the user's GitHub notifications.
|
|
26
|
+
<!-- monorepo-apps:END -->
|