@basou/cli 0.33.0 → 0.35.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/dist/index.js +174 -80
- package/dist/index.js.map +1 -1
- package/dist/program.js +174 -80
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3616,11 +3616,104 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
3616
3616
|
}
|
|
3617
3617
|
}
|
|
3618
3618
|
|
|
3619
|
+
// src/commands/portfolio.ts
|
|
3620
|
+
import { existsSync, statSync } from "fs";
|
|
3621
|
+
import { join as join9 } from "path";
|
|
3622
|
+
function registerPortfolioCommand(program2) {
|
|
3623
|
+
program2.command("portfolio").description(
|
|
3624
|
+
"List the workspaces you orient across (read-only): every planning master registered in ~/.basou/portfolio.yaml, with its path and whether it exists / is initialized. The headless text/JSON counterpart to the `basou view --portfolio` GUI \u2014 for discovering where a sibling project lives without opening a browser"
|
|
3625
|
+
).argument("[action]", "optional literal `list` (the only, and default, action)").option("--json", "Output the result as JSON").option(
|
|
3626
|
+
"--check",
|
|
3627
|
+
"moved: the redundancy/footprint safety preflight is `basou view --portfolio --check` (this prints that pointer and exits)"
|
|
3628
|
+
).option("-v, --verbose", "Show error causes").action(async (action, opts) => {
|
|
3629
|
+
await runPortfolioCommand(action, opts);
|
|
3630
|
+
});
|
|
3631
|
+
}
|
|
3632
|
+
function isDirectory(path) {
|
|
3633
|
+
try {
|
|
3634
|
+
return statSync(path).isDirectory();
|
|
3635
|
+
} catch {
|
|
3636
|
+
return false;
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
async function runPortfolioCommand(action, options, ctx = {}) {
|
|
3640
|
+
if (options.check === true) {
|
|
3641
|
+
console.error(
|
|
3642
|
+
"`basou portfolio` is a read-only listing; it has no safety preflight.\nRun `basou view --portfolio --check` for the redundancy/footprint check."
|
|
3643
|
+
);
|
|
3644
|
+
process.exitCode = 1;
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
if (action !== void 0 && action !== "list") {
|
|
3648
|
+
console.error(
|
|
3649
|
+
`Unknown portfolio action '${action}'. Run \`basou portfolio\` or \`basou portfolio list\` to list the registered workspaces.`
|
|
3650
|
+
);
|
|
3651
|
+
process.exitCode = 1;
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
await runPortfolioList(options, ctx);
|
|
3655
|
+
}
|
|
3656
|
+
async function runPortfolioList(options, ctx = {}) {
|
|
3657
|
+
try {
|
|
3658
|
+
await doRunPortfolioList(options, ctx);
|
|
3659
|
+
} catch (error) {
|
|
3660
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
3661
|
+
process.exitCode = 1;
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
async function doRunPortfolioList(options, ctx) {
|
|
3665
|
+
const configPath = ctx.configPath ?? DEFAULT_PORTFOLIO_CONFIG_PATH;
|
|
3666
|
+
const pathExists2 = ctx.pathExists ?? ((p) => existsSync(p));
|
|
3667
|
+
const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join9(p, ".basou")));
|
|
3668
|
+
const workspaces = await loadPortfolioConfig(configPath);
|
|
3669
|
+
const result = {
|
|
3670
|
+
configPath,
|
|
3671
|
+
workspaces: workspaces.map((w) => {
|
|
3672
|
+
const exists = pathExists2(w.path);
|
|
3673
|
+
return {
|
|
3674
|
+
label: w.label ?? null,
|
|
3675
|
+
path: w.path,
|
|
3676
|
+
exists,
|
|
3677
|
+
initialized: exists && isInitialized(w.path)
|
|
3678
|
+
};
|
|
3679
|
+
})
|
|
3680
|
+
};
|
|
3681
|
+
if (options.json === true) {
|
|
3682
|
+
console.log(JSON.stringify(result));
|
|
3683
|
+
} else {
|
|
3684
|
+
console.log(renderPortfolioList(result));
|
|
3685
|
+
}
|
|
3686
|
+
return result;
|
|
3687
|
+
}
|
|
3688
|
+
var LABEL_COLUMN_CAP = 24;
|
|
3689
|
+
function renderPortfolioList(result) {
|
|
3690
|
+
const lines = [];
|
|
3691
|
+
const n = result.workspaces.length;
|
|
3692
|
+
lines.push("# Portfolio (workspaces you orient across)");
|
|
3693
|
+
lines.push("");
|
|
3694
|
+
lines.push(`${n} workspace${n === 1 ? "" : "s"} registered in ~/.basou/portfolio.yaml:`);
|
|
3695
|
+
lines.push("");
|
|
3696
|
+
const labelWidth = Math.min(
|
|
3697
|
+
LABEL_COLUMN_CAP,
|
|
3698
|
+
Math.max(0, ...result.workspaces.map((w) => (w.label ?? "(no label)").length))
|
|
3699
|
+
);
|
|
3700
|
+
for (const w of result.workspaces) {
|
|
3701
|
+
const label = (w.label ?? "(no label)").padEnd(labelWidth);
|
|
3702
|
+
const status = !w.exists ? "\u26A0 path not found" : w.initialized ? "\u2713 initialized" : "\u26A0 no .basou (not a planning master?)";
|
|
3703
|
+
lines.push(`- ${label} ${w.path} ${status}`);
|
|
3704
|
+
}
|
|
3705
|
+
lines.push("");
|
|
3706
|
+
lines.push(
|
|
3707
|
+
"Note: read-only listing of ~/.basou/portfolio.yaml. Run `basou view --portfolio` for the cross-workspace GUI, or `basou view --portfolio --check` for the redundancy/footprint safety preflight."
|
|
3708
|
+
);
|
|
3709
|
+
return lines.join("\n");
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3619
3712
|
// src/commands/project.ts
|
|
3620
3713
|
import {
|
|
3621
3714
|
closeSync,
|
|
3622
3715
|
copyFileSync,
|
|
3623
|
-
existsSync,
|
|
3716
|
+
existsSync as existsSync2,
|
|
3624
3717
|
constants as fsConstants,
|
|
3625
3718
|
ftruncateSync,
|
|
3626
3719
|
lstatSync,
|
|
@@ -3630,13 +3723,13 @@ import {
|
|
|
3630
3723
|
readFileSync,
|
|
3631
3724
|
readlinkSync,
|
|
3632
3725
|
realpathSync,
|
|
3633
|
-
statSync,
|
|
3726
|
+
statSync as statSync2,
|
|
3634
3727
|
symlinkSync,
|
|
3635
3728
|
unlinkSync,
|
|
3636
3729
|
writeFileSync,
|
|
3637
3730
|
writeSync
|
|
3638
3731
|
} from "fs";
|
|
3639
|
-
import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as
|
|
3732
|
+
import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join10, relative as relative2, resolve as resolve7 } from "path";
|
|
3640
3733
|
import {
|
|
3641
3734
|
appendBasouGitignore as appendBasouGitignore2,
|
|
3642
3735
|
basouPaths as basouPaths10,
|
|
@@ -4018,7 +4111,7 @@ function classifySourceRoot(repositoryRoot, declaredPath) {
|
|
|
4018
4111
|
} catch {
|
|
4019
4112
|
return { path: declaredPath, kind: "unresolved" };
|
|
4020
4113
|
}
|
|
4021
|
-
return { path: declaredPath, kind:
|
|
4114
|
+
return { path: declaredPath, kind: existsSync2(join10(real, ".git")) ? "repo" : "non-repo" };
|
|
4022
4115
|
}
|
|
4023
4116
|
async function doRunProjectAdopt(options, ctx) {
|
|
4024
4117
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -4122,7 +4215,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
4122
4215
|
} catch {
|
|
4123
4216
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
4124
4217
|
}
|
|
4125
|
-
if (!
|
|
4218
|
+
if (!existsSync2(join10(real, ".git"))) {
|
|
4126
4219
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
4127
4220
|
}
|
|
4128
4221
|
try {
|
|
@@ -4130,7 +4223,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
4130
4223
|
for (const name of INSTRUCTION_FILES) {
|
|
4131
4224
|
let present = true;
|
|
4132
4225
|
try {
|
|
4133
|
-
lstatSync(
|
|
4226
|
+
lstatSync(join10(real, name));
|
|
4134
4227
|
} catch {
|
|
4135
4228
|
present = false;
|
|
4136
4229
|
}
|
|
@@ -4241,10 +4334,10 @@ function gatherRepoGitignore(repositoryRoot, entry) {
|
|
|
4241
4334
|
} catch {
|
|
4242
4335
|
return { ...base, reachable: false, currentLines: [] };
|
|
4243
4336
|
}
|
|
4244
|
-
if (!
|
|
4337
|
+
if (!existsSync2(join10(real, ".git"))) {
|
|
4245
4338
|
return { ...base, reachable: false, currentLines: [] };
|
|
4246
4339
|
}
|
|
4247
|
-
return { ...base, reachable: true, currentLines: readGitignoreLines(
|
|
4340
|
+
return { ...base, reachable: true, currentLines: readGitignoreLines(join10(real, ".gitignore")) };
|
|
4248
4341
|
}
|
|
4249
4342
|
function hasErrorCode(error) {
|
|
4250
4343
|
return error instanceof Error && typeof error.code === "string";
|
|
@@ -4258,7 +4351,7 @@ function readGitignoreLines(file) {
|
|
|
4258
4351
|
}
|
|
4259
4352
|
}
|
|
4260
4353
|
function applyGitignorePlan(repositoryRoot, plan) {
|
|
4261
|
-
const file =
|
|
4354
|
+
const file = join10(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
|
|
4262
4355
|
let existing = "";
|
|
4263
4356
|
try {
|
|
4264
4357
|
existing = readFileSync(file, "utf8");
|
|
@@ -4384,7 +4477,7 @@ function anchorCanonicalState(filePath) {
|
|
|
4384
4477
|
if (hasErrorCode(error) && error.code === "ENOENT") return "absent";
|
|
4385
4478
|
return "broken";
|
|
4386
4479
|
}
|
|
4387
|
-
if (st.isSymbolicLink()) return
|
|
4480
|
+
if (st.isSymbolicLink()) return existsSync2(filePath) ? "usable" : "broken";
|
|
4388
4481
|
return st.isFile() ? "usable" : "broken";
|
|
4389
4482
|
}
|
|
4390
4483
|
function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
@@ -4398,7 +4491,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4398
4491
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
4399
4492
|
}
|
|
4400
4493
|
if (real === anchorReal) {
|
|
4401
|
-
const anchorCanonical =
|
|
4494
|
+
const anchorCanonical = join10(real, CANONICAL_FILE);
|
|
4402
4495
|
const anchorState = anchorCanonicalState(anchorCanonical);
|
|
4403
4496
|
if (anchorState === "absent") {
|
|
4404
4497
|
return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
|
|
@@ -4418,7 +4511,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4418
4511
|
anchorCanonical,
|
|
4419
4512
|
"self"
|
|
4420
4513
|
).map((spec) => {
|
|
4421
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4514
|
+
const { state, actualTarget } = inspectSymlink(join10(real, spec.name), spec.target);
|
|
4422
4515
|
return {
|
|
4423
4516
|
name: spec.name,
|
|
4424
4517
|
expectedTarget: spec.target,
|
|
@@ -4435,16 +4528,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4435
4528
|
files: anchorFiles
|
|
4436
4529
|
};
|
|
4437
4530
|
}
|
|
4438
|
-
if (!
|
|
4531
|
+
if (!existsSync2(join10(real, ".git"))) {
|
|
4439
4532
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
4440
4533
|
}
|
|
4441
|
-
const canonicalFile = isSelf ?
|
|
4442
|
-
if (!
|
|
4534
|
+
const canonicalFile = isSelf ? join10(real, CANONICAL_FILE) : join10(anchorReal, "agents", basename5(real), CANONICAL_FILE);
|
|
4535
|
+
if (!existsSync2(canonicalFile)) {
|
|
4443
4536
|
return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
|
|
4444
4537
|
}
|
|
4445
4538
|
const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
|
|
4446
4539
|
(spec) => {
|
|
4447
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4540
|
+
const { state, actualTarget } = inspectSymlink(join10(real, spec.name), spec.target);
|
|
4448
4541
|
return {
|
|
4449
4542
|
name: spec.name,
|
|
4450
4543
|
expectedTarget: spec.target,
|
|
@@ -4473,7 +4566,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
|
|
|
4473
4566
|
const created = [];
|
|
4474
4567
|
const failed = [];
|
|
4475
4568
|
for (const { name, target } of plan.toCreate) {
|
|
4476
|
-
const filePath =
|
|
4569
|
+
const filePath = join10(real, name);
|
|
4477
4570
|
try {
|
|
4478
4571
|
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4479
4572
|
symlinkSync(target, filePath);
|
|
@@ -4504,10 +4597,10 @@ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
|
|
|
4504
4597
|
const collision = viewCanonicalCollision(repositoryRoot, roster, viewName);
|
|
4505
4598
|
if (collision !== void 0) return { kind: "collision", viewName, repoPath: collision };
|
|
4506
4599
|
const canonicalFile = canonicalFileFor(anchorReal, viewName);
|
|
4507
|
-
if (!
|
|
4600
|
+
if (!existsSync2(canonicalFile)) return { kind: "missing-canonical", viewName };
|
|
4508
4601
|
const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
|
|
4509
4602
|
(spec) => {
|
|
4510
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4603
|
+
const { state, actualTarget } = inspectSymlink(join10(viewDir, spec.name), spec.target);
|
|
4511
4604
|
return {
|
|
4512
4605
|
name: spec.name,
|
|
4513
4606
|
expectedTarget: spec.target,
|
|
@@ -4523,7 +4616,7 @@ function applyViewSymlinks(viewDir, files) {
|
|
|
4523
4616
|
const failed = [];
|
|
4524
4617
|
for (const f of files) {
|
|
4525
4618
|
if (f.state !== "missing") continue;
|
|
4526
|
-
const filePath =
|
|
4619
|
+
const filePath = join10(viewDir, f.name);
|
|
4527
4620
|
try {
|
|
4528
4621
|
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4529
4622
|
symlinkSync(f.expectedTarget, filePath);
|
|
@@ -4745,7 +4838,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
|
|
|
4745
4838
|
return realpathSync(abs);
|
|
4746
4839
|
} catch {
|
|
4747
4840
|
try {
|
|
4748
|
-
return
|
|
4841
|
+
return join10(realpathSync(dirname3(abs)), basename5(abs));
|
|
4749
4842
|
} catch {
|
|
4750
4843
|
return abs;
|
|
4751
4844
|
}
|
|
@@ -4763,7 +4856,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
|
4763
4856
|
return { path: entry.path, reachable: false };
|
|
4764
4857
|
}
|
|
4765
4858
|
const linkName = basename5(repoReal);
|
|
4766
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4859
|
+
const { state, actualTarget } = inspectSymlink(join10(viewDir, linkName), expectedTarget);
|
|
4767
4860
|
return {
|
|
4768
4861
|
path: entry.path,
|
|
4769
4862
|
reachable: true,
|
|
@@ -4777,7 +4870,7 @@ function applyViewPlan(viewDir, toCreate) {
|
|
|
4777
4870
|
const created = [];
|
|
4778
4871
|
const failed = [];
|
|
4779
4872
|
for (const { name, target } of toCreate) {
|
|
4780
|
-
const filePath =
|
|
4873
|
+
const filePath = join10(viewDir, name);
|
|
4781
4874
|
try {
|
|
4782
4875
|
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4783
4876
|
symlinkSync(target, filePath);
|
|
@@ -4792,7 +4885,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
|
|
|
4792
4885
|
INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
|
|
4793
4886
|
);
|
|
4794
4887
|
function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
4795
|
-
const filePath =
|
|
4888
|
+
const filePath = join10(viewDir, name);
|
|
4796
4889
|
let isLink;
|
|
4797
4890
|
try {
|
|
4798
4891
|
isLink = lstatSync(filePath).isSymbolicLink();
|
|
@@ -4814,14 +4907,14 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
|
4814
4907
|
if (isAbsolute3(target)) return { target, kind: "absolute" };
|
|
4815
4908
|
let isDir = false;
|
|
4816
4909
|
try {
|
|
4817
|
-
isDir =
|
|
4910
|
+
isDir = statSync2(resolved).isDirectory();
|
|
4818
4911
|
} catch {
|
|
4819
4912
|
isDir = false;
|
|
4820
4913
|
}
|
|
4821
4914
|
if (!isDir) {
|
|
4822
|
-
return { target, kind:
|
|
4915
|
+
return { target, kind: existsSync2(resolved) ? "non-repo" : "broken" };
|
|
4823
4916
|
}
|
|
4824
|
-
return { target, kind:
|
|
4917
|
+
return { target, kind: existsSync2(join10(resolved, ".git")) ? "repo" : "non-repo" };
|
|
4825
4918
|
}
|
|
4826
4919
|
function gatherExistingViewLinks(viewDir, rosterRealpaths) {
|
|
4827
4920
|
let names;
|
|
@@ -4846,7 +4939,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
|
4846
4939
|
const pruned = [];
|
|
4847
4940
|
const failed = [];
|
|
4848
4941
|
for (const { name } of toPrune) {
|
|
4849
|
-
const filePath =
|
|
4942
|
+
const filePath = join10(viewDir, name);
|
|
4850
4943
|
const c = classifyViewLink(viewDir, name, rosterRealpaths);
|
|
4851
4944
|
if (c === null || c.kind !== "repo") {
|
|
4852
4945
|
failed.push({
|
|
@@ -5059,10 +5152,10 @@ async function runProjectPreset(options, ctx = {}) {
|
|
|
5059
5152
|
}
|
|
5060
5153
|
}
|
|
5061
5154
|
function canonicalFileFor(anchorReal, canonicalName) {
|
|
5062
|
-
return
|
|
5155
|
+
return join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5063
5156
|
}
|
|
5064
5157
|
function canonicalLabelFor(canonicalName) {
|
|
5065
|
-
return
|
|
5158
|
+
return join10("agents", canonicalName, CANONICAL_FILE);
|
|
5066
5159
|
}
|
|
5067
5160
|
async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
5068
5161
|
const declared = {
|
|
@@ -5083,7 +5176,7 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
5083
5176
|
if (real === anchorReal) {
|
|
5084
5177
|
return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
|
|
5085
5178
|
}
|
|
5086
|
-
if (!
|
|
5179
|
+
if (!existsSync2(join10(real, ".git"))) {
|
|
5087
5180
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
5088
5181
|
}
|
|
5089
5182
|
const canonicalName = basename5(real);
|
|
@@ -5465,24 +5558,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
5465
5558
|
const instructionFiles = [];
|
|
5466
5559
|
for (const name of INSTRUCTION_FILES) {
|
|
5467
5560
|
try {
|
|
5468
|
-
lstatSync(
|
|
5561
|
+
lstatSync(join10(real, name));
|
|
5469
5562
|
instructionFiles.push(name);
|
|
5470
5563
|
} catch {
|
|
5471
5564
|
}
|
|
5472
5565
|
}
|
|
5473
5566
|
let ignored;
|
|
5474
5567
|
try {
|
|
5475
|
-
ignored = new Set(readGitignoreLines(
|
|
5568
|
+
ignored = new Set(readGitignoreLines(join10(real, ".gitignore")).map((l) => l.trim()));
|
|
5476
5569
|
} catch {
|
|
5477
5570
|
ignored = /* @__PURE__ */ new Set();
|
|
5478
5571
|
}
|
|
5479
5572
|
const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
|
|
5480
|
-
const canonical2 =
|
|
5573
|
+
const canonical2 = existsSync2(join10(anchorReal, "agents", canonicalName, CANONICAL_FILE));
|
|
5481
5574
|
let viewLink = false;
|
|
5482
5575
|
const viewPath = manifest.workspace.view;
|
|
5483
5576
|
if (viewPath !== void 0) {
|
|
5484
5577
|
try {
|
|
5485
|
-
lstatSync(
|
|
5578
|
+
lstatSync(join10(resolveViewDir(repositoryRoot, viewPath), canonicalName));
|
|
5486
5579
|
viewLink = true;
|
|
5487
5580
|
} catch {
|
|
5488
5581
|
}
|
|
@@ -5496,11 +5589,11 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
5496
5589
|
};
|
|
5497
5590
|
}
|
|
5498
5591
|
function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
|
|
5499
|
-
const canonicalFile =
|
|
5592
|
+
const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5500
5593
|
return expectedSymlinkTargets(repoReal, canonicalFile);
|
|
5501
5594
|
}
|
|
5502
5595
|
function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
5503
|
-
const filePath =
|
|
5596
|
+
const filePath = join10(viewDir, name);
|
|
5504
5597
|
try {
|
|
5505
5598
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
5506
5599
|
const target = readlinkSync(filePath);
|
|
@@ -5511,7 +5604,7 @@ function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
|
5511
5604
|
}
|
|
5512
5605
|
}
|
|
5513
5606
|
function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
|
|
5514
|
-
const filePath =
|
|
5607
|
+
const filePath = join10(viewDir, name);
|
|
5515
5608
|
try {
|
|
5516
5609
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
5517
5610
|
const target = readlinkSync(filePath);
|
|
@@ -5562,7 +5655,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5562
5655
|
if (!isAnchor) {
|
|
5563
5656
|
if (repoReal !== void 0) {
|
|
5564
5657
|
for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
|
|
5565
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5658
|
+
const { state, actualTarget } = inspectSymlink(join10(repoReal, spec.name), spec.target);
|
|
5566
5659
|
if (isSelf) {
|
|
5567
5660
|
if (state !== "missing")
|
|
5568
5661
|
items.push({
|
|
@@ -5599,7 +5692,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5599
5692
|
}
|
|
5600
5693
|
let ignored;
|
|
5601
5694
|
try {
|
|
5602
|
-
ignored = new Set(readGitignoreLines(
|
|
5695
|
+
ignored = new Set(readGitignoreLines(join10(repoReal, ".gitignore")).map((l) => l.trim()));
|
|
5603
5696
|
for (const p of INSTRUCTION_FILES) {
|
|
5604
5697
|
if (ignored.has(p) || ignored.has(`/${p}`)) {
|
|
5605
5698
|
items.push({
|
|
@@ -5622,7 +5715,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5622
5715
|
const viewPath = manifest.workspace.view;
|
|
5623
5716
|
if (viewPath !== void 0) {
|
|
5624
5717
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
5625
|
-
const linkPath =
|
|
5718
|
+
const linkPath = join10(viewDir, canonicalName);
|
|
5626
5719
|
let isLink = false;
|
|
5627
5720
|
try {
|
|
5628
5721
|
isLink = lstatSync(linkPath).isSymbolicLink();
|
|
@@ -5648,8 +5741,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5648
5741
|
else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
|
|
5649
5742
|
}
|
|
5650
5743
|
}
|
|
5651
|
-
const canonicalFile =
|
|
5652
|
-
const canonicalLabel =
|
|
5744
|
+
const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5745
|
+
const canonicalLabel = join10("agents", canonicalName, CANONICAL_FILE);
|
|
5653
5746
|
let canonicalIsLink = false;
|
|
5654
5747
|
try {
|
|
5655
5748
|
canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
|
|
@@ -5663,7 +5756,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5663
5756
|
state: "foreign",
|
|
5664
5757
|
note: "the canonical is a symlink (not generated)"
|
|
5665
5758
|
});
|
|
5666
|
-
} else if (
|
|
5759
|
+
} else if (existsSync2(canonicalFile)) {
|
|
5667
5760
|
let content;
|
|
5668
5761
|
try {
|
|
5669
5762
|
content = readFileSync(canonicalFile, "utf8");
|
|
@@ -5753,12 +5846,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5753
5846
|
);
|
|
5754
5847
|
for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
|
|
5755
5848
|
const expected = expectedByName.get(item.label);
|
|
5756
|
-
if (repoReal === null || expected === void 0 || inspectSymlink(
|
|
5849
|
+
if (repoReal === null || expected === void 0 || inspectSymlink(join10(repoReal, item.label), expected).state !== "correct") {
|
|
5757
5850
|
changed(item.label);
|
|
5758
5851
|
continue;
|
|
5759
5852
|
}
|
|
5760
5853
|
try {
|
|
5761
|
-
unlinkSync(
|
|
5854
|
+
unlinkSync(join10(repoReal, item.label));
|
|
5762
5855
|
removed.push(item.label);
|
|
5763
5856
|
} catch (error) {
|
|
5764
5857
|
failed.push({ label: item.label, message: failureReason(error) });
|
|
@@ -5777,7 +5870,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5777
5870
|
continue;
|
|
5778
5871
|
}
|
|
5779
5872
|
try {
|
|
5780
|
-
unlinkSync(
|
|
5873
|
+
unlinkSync(join10(viewDir, item.label));
|
|
5781
5874
|
removed.push(`view/${item.label}`);
|
|
5782
5875
|
} catch (error) {
|
|
5783
5876
|
failed.push({ label: `view/${item.label}`, message: failureReason(error) });
|
|
@@ -5785,7 +5878,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5785
5878
|
}
|
|
5786
5879
|
const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
5787
5880
|
for (const item of removable.filter((i) => i.kind === "canonical-block")) {
|
|
5788
|
-
const canonicalFile =
|
|
5881
|
+
const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5789
5882
|
try {
|
|
5790
5883
|
if (lstatSync(canonicalFile).isSymbolicLink()) {
|
|
5791
5884
|
changed(item.label);
|
|
@@ -6059,12 +6152,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
|
|
|
6059
6152
|
} catch {
|
|
6060
6153
|
return { canonicalDirOld: false, viewLinkOld: false };
|
|
6061
6154
|
}
|
|
6062
|
-
const canonicalDirOld =
|
|
6155
|
+
const canonicalDirOld = existsSync2(join10(anchorReal, "agents", oldBasename));
|
|
6063
6156
|
let viewLinkOld = false;
|
|
6064
6157
|
const viewPath = manifest.workspace.view;
|
|
6065
6158
|
if (viewPath !== void 0) {
|
|
6066
6159
|
try {
|
|
6067
|
-
lstatSync(
|
|
6160
|
+
lstatSync(join10(resolveViewDir(repositoryRoot, viewPath), oldBasename));
|
|
6068
6161
|
viewLinkOld = true;
|
|
6069
6162
|
} catch {
|
|
6070
6163
|
}
|
|
@@ -6262,7 +6355,7 @@ async function doRunProjectNew(repos, options, ctx) {
|
|
|
6262
6355
|
const viewPath = options.view === false ? null : options.view ?? `../${viewStem}-workspace`;
|
|
6263
6356
|
const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
|
|
6264
6357
|
const paths = basouPaths10(repositoryRoot);
|
|
6265
|
-
const existed =
|
|
6358
|
+
const existed = existsSync2(paths.files.manifest);
|
|
6266
6359
|
const manifest = createManifest2({
|
|
6267
6360
|
workspaceName,
|
|
6268
6361
|
sourceRoots,
|
|
@@ -6422,7 +6515,7 @@ async function doRunProjectSeedAnchor(options, ctx) {
|
|
|
6422
6515
|
console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
|
|
6423
6516
|
return;
|
|
6424
6517
|
}
|
|
6425
|
-
const anchorDoc =
|
|
6518
|
+
const anchorDoc = join10(repositoryRoot, CANONICAL_FILE);
|
|
6426
6519
|
if (pathPresent(anchorDoc)) {
|
|
6427
6520
|
console.log(
|
|
6428
6521
|
`\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
|
|
@@ -6484,7 +6577,7 @@ function regularFileSpokes(repoReal) {
|
|
|
6484
6577
|
const out = [];
|
|
6485
6578
|
for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
|
|
6486
6579
|
try {
|
|
6487
|
-
const st = lstatSync(
|
|
6580
|
+
const st = lstatSync(join10(repoReal, spoke));
|
|
6488
6581
|
if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
|
|
6489
6582
|
} catch {
|
|
6490
6583
|
}
|
|
@@ -6530,8 +6623,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
6530
6623
|
};
|
|
6531
6624
|
}
|
|
6532
6625
|
const isAnchor = argReal === anchorReal;
|
|
6533
|
-
const reachable =
|
|
6534
|
-
const canonicalFile =
|
|
6626
|
+
const reachable = existsSync2(join10(argReal, ".git"));
|
|
6627
|
+
const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6535
6628
|
return {
|
|
6536
6629
|
path,
|
|
6537
6630
|
declared,
|
|
@@ -6540,13 +6633,13 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
6540
6633
|
reachable,
|
|
6541
6634
|
canonicalName,
|
|
6542
6635
|
...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
|
|
6543
|
-
agentsState: inspectAgentsState(
|
|
6636
|
+
agentsState: inspectAgentsState(join10(argReal, CANONICAL_FILE)),
|
|
6544
6637
|
canonicalExists: pathPresent(canonicalFile),
|
|
6545
6638
|
regularSpokes: regularFileSpokes(argReal)
|
|
6546
6639
|
};
|
|
6547
6640
|
}
|
|
6548
6641
|
function relocateAgentsFile(repoReal, canonicalFile) {
|
|
6549
|
-
const agentsFile =
|
|
6642
|
+
const agentsFile = join10(repoReal, CANONICAL_FILE);
|
|
6550
6643
|
try {
|
|
6551
6644
|
mkdirSync(dirname3(canonicalFile), { recursive: true });
|
|
6552
6645
|
} catch (error) {
|
|
@@ -6663,7 +6756,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
6663
6756
|
let failure;
|
|
6664
6757
|
let partial = false;
|
|
6665
6758
|
if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
|
|
6666
|
-
const canonicalFile =
|
|
6759
|
+
const canonicalFile = join10(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
|
|
6667
6760
|
const res = relocateAgentsFile(argReal, canonicalFile);
|
|
6668
6761
|
if (res.ok) {
|
|
6669
6762
|
applied = true;
|
|
@@ -6868,7 +6961,7 @@ import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers3, readMarkdo
|
|
|
6868
6961
|
|
|
6869
6962
|
// src/lib/context-channel.ts
|
|
6870
6963
|
import { homedir as homedir7 } from "os";
|
|
6871
|
-
import { join as
|
|
6964
|
+
import { join as join11 } from "path";
|
|
6872
6965
|
import {
|
|
6873
6966
|
ORIENTATION_END,
|
|
6874
6967
|
ORIENTATION_START,
|
|
@@ -6876,7 +6969,7 @@ import {
|
|
|
6876
6969
|
readMarkdownFile as readMarkdownFile5,
|
|
6877
6970
|
removeMarkerSection as removeMarkerSection2
|
|
6878
6971
|
} from "@basou/core";
|
|
6879
|
-
var CODEX_TARGET_PATH =
|
|
6972
|
+
var CODEX_TARGET_PATH = join11(homedir7(), ".codex", "AGENTS.md");
|
|
6880
6973
|
var ORIENTATION_MARKERS = { start: ORIENTATION_START, end: ORIENTATION_END };
|
|
6881
6974
|
var ORIENTATION_MANAGED_NOTE = "<!-- Managed by basou: 'basou refresh' regenerates everything between the BASOU:ORIENTATION markers with the workspace's current position. This block is transient \u2014 it changes every refresh; do not edit it. -->";
|
|
6882
6975
|
function buildTargetBody(existing, block, markers) {
|
|
@@ -6980,15 +7073,15 @@ async function renderOrientationToCodexChannel(opts) {
|
|
|
6980
7073
|
|
|
6981
7074
|
// src/lib/protocols-config.ts
|
|
6982
7075
|
import { homedir as homedir8 } from "os";
|
|
6983
|
-
import { isAbsolute as isAbsolute4, join as
|
|
7076
|
+
import { isAbsolute as isAbsolute4, join as join12, resolve as resolve8 } from "path";
|
|
6984
7077
|
import { readYamlFile as readYamlFile5 } from "@basou/core";
|
|
6985
|
-
var DEFAULT_PROTOCOLS_CONFIG_PATH =
|
|
6986
|
-
var DEFAULT_TARGET_PATH =
|
|
7078
|
+
var DEFAULT_PROTOCOLS_CONFIG_PATH = join12(homedir8(), ".basou", "protocols.yaml");
|
|
7079
|
+
var DEFAULT_TARGET_PATH = join12(homedir8(), ".claude", "CLAUDE.md");
|
|
6987
7080
|
var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
|
|
6988
7081
|
var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
|
|
6989
7082
|
function expandTilde3(p) {
|
|
6990
7083
|
if (p === "~") return homedir8();
|
|
6991
|
-
if (p.startsWith("~/")) return
|
|
7084
|
+
if (p.startsWith("~/")) return join12(homedir8(), p.slice(2));
|
|
6992
7085
|
return p;
|
|
6993
7086
|
}
|
|
6994
7087
|
function isRecord3(value) {
|
|
@@ -7193,15 +7286,15 @@ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
|
7193
7286
|
// src/commands/refresh-watch.ts
|
|
7194
7287
|
import { readdir as readdir2, stat as stat5 } from "fs/promises";
|
|
7195
7288
|
import { homedir as homedir9 } from "os";
|
|
7196
|
-
import { join as
|
|
7289
|
+
import { join as join13 } from "path";
|
|
7197
7290
|
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
7198
7291
|
var DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
7199
7292
|
var MIN_WATCH_INTERVAL_SEC = 5;
|
|
7200
7293
|
var MAX_WATCH_INTERVAL_SEC = 86400;
|
|
7201
7294
|
function watchedRoots(ctx) {
|
|
7202
7295
|
return [
|
|
7203
|
-
ctx.codexSessionsDir ??
|
|
7204
|
-
ctx.claudeProjectsDir ??
|
|
7296
|
+
ctx.codexSessionsDir ?? join13(homedir9(), ".codex", "sessions"),
|
|
7297
|
+
ctx.claudeProjectsDir ?? join13(homedir9(), ".claude", "projects")
|
|
7205
7298
|
];
|
|
7206
7299
|
}
|
|
7207
7300
|
async function scanSourceLogs(roots) {
|
|
@@ -7215,7 +7308,7 @@ async function scanSourceLogs(roots) {
|
|
|
7215
7308
|
throw new Error("Failed to read a source log directory", { cause: error });
|
|
7216
7309
|
}
|
|
7217
7310
|
for (const entry of entries) {
|
|
7218
|
-
const full =
|
|
7311
|
+
const full = join13(dir, entry.name);
|
|
7219
7312
|
if (entry.isDirectory()) {
|
|
7220
7313
|
await walk(full);
|
|
7221
7314
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -7937,7 +8030,7 @@ function renderReviewGaps(summary) {
|
|
|
7937
8030
|
// src/commands/run.ts
|
|
7938
8031
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
7939
8032
|
import { homedir as homedir11 } from "os";
|
|
7940
|
-
import { join as
|
|
8033
|
+
import { join as join14 } from "path";
|
|
7941
8034
|
import {
|
|
7942
8035
|
acquireLock as acquireLock5,
|
|
7943
8036
|
assertBasouRootSafe as assertBasouRootSafe12,
|
|
@@ -8013,13 +8106,13 @@ async function runTrackedTool(args, options, ctx, adapter) {
|
|
|
8013
8106
|
await assertBasouRootSafe12(paths.root);
|
|
8014
8107
|
const manifest = await readManifest8(paths);
|
|
8015
8108
|
const sessionId = prefixedUlid4("ses");
|
|
8016
|
-
const sessionDir =
|
|
8109
|
+
const sessionDir = join14(paths.sessions, sessionId);
|
|
8017
8110
|
await mkdir2(sessionDir, { recursive: true });
|
|
8018
8111
|
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
8019
8112
|
await coreAppendChainedEvent2(paths, sessionId, event);
|
|
8020
8113
|
});
|
|
8021
8114
|
const startedAt = now().toISOString();
|
|
8022
|
-
const sessionYamlPath =
|
|
8115
|
+
const sessionYamlPath = join14(sessionDir, "session.yaml");
|
|
8023
8116
|
const session = buildInitialSession2({
|
|
8024
8117
|
id: sessionId,
|
|
8025
8118
|
command,
|
|
@@ -8381,7 +8474,7 @@ async function syncCodexOrientationChannelPreSpawn(cwd, ctx) {
|
|
|
8381
8474
|
|
|
8382
8475
|
// src/commands/session.ts
|
|
8383
8476
|
import { readFile as readFile6 } from "fs/promises";
|
|
8384
|
-
import { basename as basename6, isAbsolute as isAbsolute6, join as
|
|
8477
|
+
import { basename as basename6, isAbsolute as isAbsolute6, join as join15, relative as relative3 } from "path";
|
|
8385
8478
|
import {
|
|
8386
8479
|
acquireLock as acquireLock6,
|
|
8387
8480
|
appendEventToExistingSession as appendEventToExistingSession3,
|
|
@@ -8506,8 +8599,8 @@ async function doRunSessionShow(idInput, options, ctx) {
|
|
|
8506
8599
|
const paths = basouPaths16(repositoryRoot);
|
|
8507
8600
|
await assertWorkspaceInitialized11(paths.root);
|
|
8508
8601
|
const sessionId = await resolveSessionId3(paths, idInput);
|
|
8509
|
-
const sessionDir =
|
|
8510
|
-
const sessionYamlPath =
|
|
8602
|
+
const sessionDir = join15(paths.sessions, sessionId);
|
|
8603
|
+
const sessionYamlPath = join15(sessionDir, "session.yaml");
|
|
8511
8604
|
let session;
|
|
8512
8605
|
try {
|
|
8513
8606
|
const raw = await readYamlFile7(sessionYamlPath);
|
|
@@ -9269,7 +9362,7 @@ function formatVersionGateMessage(error) {
|
|
|
9269
9362
|
|
|
9270
9363
|
// src/commands/task.ts
|
|
9271
9364
|
import { readFile as readFile7 } from "fs/promises";
|
|
9272
|
-
import { join as
|
|
9365
|
+
import { join as join16 } from "path";
|
|
9273
9366
|
import {
|
|
9274
9367
|
archiveTask,
|
|
9275
9368
|
assertBasouRootSafe as assertBasouRootSafe16,
|
|
@@ -9595,7 +9688,7 @@ async function doRunTaskShow(idInput, options, ctx) {
|
|
|
9595
9688
|
const events = [];
|
|
9596
9689
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
9597
9690
|
for (const s of sessions) {
|
|
9598
|
-
const sessionDir =
|
|
9691
|
+
const sessionDir = join16(paths.sessions, s.sessionId);
|
|
9599
9692
|
try {
|
|
9600
9693
|
for await (const ev of replayEvents3(sessionDir, {
|
|
9601
9694
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -10501,7 +10594,7 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
|
10501
10594
|
// src/lib/portfolio-safety.ts
|
|
10502
10595
|
import { execFile } from "child_process";
|
|
10503
10596
|
import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
|
|
10504
|
-
import { isAbsolute as isAbsolute7, join as
|
|
10597
|
+
import { isAbsolute as isAbsolute7, join as join17, relative as relative4, resolve as resolve11 } from "path";
|
|
10505
10598
|
import { promisify } from "util";
|
|
10506
10599
|
import { readManifest as readManifest12 } from "@basou/core";
|
|
10507
10600
|
var execFileAsync = promisify(execFile);
|
|
@@ -10525,7 +10618,7 @@ function isBasouPath(p) {
|
|
|
10525
10618
|
async function inspectRepo(repoPath) {
|
|
10526
10619
|
let hasEntry = false;
|
|
10527
10620
|
try {
|
|
10528
|
-
await lstat2(
|
|
10621
|
+
await lstat2(join17(repoPath, ".basou"));
|
|
10529
10622
|
hasEntry = true;
|
|
10530
10623
|
} catch (error) {
|
|
10531
10624
|
if (errorCode(error) !== "ENOENT") {
|
|
@@ -10679,7 +10772,7 @@ function formatSafetyReport(result) {
|
|
|
10679
10772
|
|
|
10680
10773
|
// src/lib/view-server.ts
|
|
10681
10774
|
import { createServer } from "http";
|
|
10682
|
-
import { basename as basename7, join as
|
|
10775
|
+
import { basename as basename7, join as join18, resolve as resolve12 } from "path";
|
|
10683
10776
|
import {
|
|
10684
10777
|
computeWorkStats as computeWorkStats2,
|
|
10685
10778
|
enumerateApprovals as enumerateApprovals2,
|
|
@@ -11719,7 +11812,7 @@ async function sessionDetail(ws, sessionId) {
|
|
|
11719
11812
|
throw error;
|
|
11720
11813
|
}
|
|
11721
11814
|
try {
|
|
11722
|
-
const events = await readAllEvents2(
|
|
11815
|
+
const events = await readAllEvents2(join18(ws.paths.sessions, sessionId));
|
|
11723
11816
|
return { session, events };
|
|
11724
11817
|
} catch {
|
|
11725
11818
|
return { session, events: [], degraded: true };
|
|
@@ -12100,6 +12193,7 @@ function buildProgram() {
|
|
|
12100
12193
|
registerDecisionsCommand(program2);
|
|
12101
12194
|
registerReportCommand(program2);
|
|
12102
12195
|
registerOrientCommand(program2);
|
|
12196
|
+
registerPortfolioCommand(program2);
|
|
12103
12197
|
registerReviewCommand(program2);
|
|
12104
12198
|
registerReviewGapsCommand(program2);
|
|
12105
12199
|
registerProjectCommand(program2);
|