@rockclaver/sandcastle 0.7.0 → 0.8.1

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/main.js CHANGED
@@ -18410,6 +18410,122 @@ var SANDBOX_PROVIDER_REGISTRY = [
18410
18410
  ];
18411
18411
  var listSandboxProviders = () => SANDBOX_PROVIDER_REGISTRY;
18412
18412
  var getSandboxProvider = (name) => SANDBOX_PROVIDER_REGISTRY.find((p3) => p3.name === name);
18413
+ var DEFAULT_PROFILE_NAME = "js-ts";
18414
+ var PROFILE_REGISTRY = [
18415
+ {
18416
+ name: "js-ts",
18417
+ label: "JavaScript / TypeScript",
18418
+ guidance: "JavaScript or TypeScript project managed with npm, pnpm, yarn, or bun. Install dependencies before validating, and prefer the project's existing scripts (typecheck, lint, test) over ad-hoc commands.",
18419
+ setupCommands: ["npm install"],
18420
+ validationCommands: [
18421
+ "npm install",
18422
+ "npm run typecheck",
18423
+ "npm run lint",
18424
+ "npm test"
18425
+ ]
18426
+ },
18427
+ {
18428
+ name: "flutter",
18429
+ label: "Flutter",
18430
+ guidance: "Flutter application using the Dart SDK. Fetch packages with `flutter pub get`, then analyze and test through the Flutter toolchain. Sandcastle does not install or pin the Flutter SDK \u2014 assume it is available in the sandbox.",
18431
+ setupCommands: ["flutter pub get"],
18432
+ validationCommands: ["flutter pub get", "flutter analyze", "flutter test"]
18433
+ },
18434
+ {
18435
+ name: "dart",
18436
+ label: "Dart",
18437
+ guidance: "Standalone Dart package (no Flutter). Fetch packages with `dart pub get`, then analyze and test with the Dart toolchain. Sandcastle does not install or pin the Dart SDK \u2014 assume it is available in the sandbox.",
18438
+ setupCommands: ["dart pub get"],
18439
+ validationCommands: ["dart pub get", "dart analyze", "dart test"]
18440
+ },
18441
+ {
18442
+ name: "go",
18443
+ label: "Go",
18444
+ guidance: "Go module defined by `go.mod`. Build, vet, and test through the Go toolchain. Sandcastle does not install or pin the Go SDK \u2014 assume it is available in the sandbox.",
18445
+ setupCommands: ["go mod download"],
18446
+ validationCommands: ["go build ./...", "go vet ./...", "go test ./..."]
18447
+ }
18448
+ ];
18449
+ var listProfiles = () => PROFILE_REGISTRY;
18450
+ var getProfile = (name) => PROFILE_REGISTRY.find((p3) => p3.name === name);
18451
+ var resolveProfileEntries = (profileNames) => {
18452
+ const names = profileNames.length === 0 ? [DEFAULT_PROFILE_NAME] : profileNames;
18453
+ const seen = /* @__PURE__ */ new Set();
18454
+ const resolved = [];
18455
+ for (const name of names) {
18456
+ if (seen.has(name)) continue;
18457
+ const entry = PROFILE_REGISTRY.find((p3) => p3.name === name);
18458
+ if (!entry) {
18459
+ const valid = PROFILE_REGISTRY.map((p3) => p3.name).join(", ");
18460
+ throw new Error(
18461
+ `Unknown profile "${name}". Available profiles: ${valid}`
18462
+ );
18463
+ }
18464
+ seen.add(name);
18465
+ resolved.push(entry);
18466
+ }
18467
+ return resolved;
18468
+ };
18469
+ var hasFile = (repoDir, file4) => Effect_exports.gen(function* () {
18470
+ const fs = yield* FileSystem_exports.FileSystem;
18471
+ return yield* fs.exists(join(repoDir, file4)).pipe(Effect_exports.orElseSucceed(() => false));
18472
+ });
18473
+ var readOptionalFile = (repoDir, file4) => Effect_exports.gen(function* () {
18474
+ const fs = yield* FileSystem_exports.FileSystem;
18475
+ const path2 = join(repoDir, file4);
18476
+ const exists = yield* fs.exists(path2).pipe(Effect_exports.orElseSucceed(() => false));
18477
+ if (!exists) return void 0;
18478
+ return yield* fs.readFileString(path2).pipe(Effect_exports.orElseSucceed(() => void 0));
18479
+ });
18480
+ var hasFlutterMarkers = (pubspec) => /^\s*sdk:\s*flutter\s*$/m.test(pubspec) || /^flutter:\s*$/m.test(pubspec);
18481
+ var parseSubmodulePaths = (gitmodules) => {
18482
+ const paths = [];
18483
+ for (const line4 of gitmodules.split("\n")) {
18484
+ const match6 = /^\s*path\s*=\s*(.+?)\s*$/.exec(line4);
18485
+ if (match6?.[1]) paths.push(match6[1]);
18486
+ }
18487
+ return paths;
18488
+ };
18489
+ var detectProfilesInDir = (dir) => Effect_exports.gen(function* () {
18490
+ const names = [];
18491
+ const hasJsSignal = (yield* hasFile(dir, "package.json")) || (yield* Effect_exports.all(
18492
+ LOCKFILES.map(([file4]) => hasFile(dir, file4)),
18493
+ { concurrency: "unbounded" }
18494
+ )).some(Boolean);
18495
+ if (hasJsSignal) names.push("js-ts");
18496
+ const pubspec = yield* readOptionalFile(dir, "pubspec.yaml");
18497
+ if (pubspec !== void 0) {
18498
+ names.push(hasFlutterMarkers(pubspec) ? "flutter" : "dart");
18499
+ }
18500
+ if (yield* hasFile(dir, "go.mod")) names.push("go");
18501
+ return names;
18502
+ });
18503
+ var detectRepositoryProfiles = (repoDir) => Effect_exports.gen(function* () {
18504
+ const gitmodules = yield* readOptionalFile(repoDir, ".gitmodules");
18505
+ const submodulePaths = gitmodules === void 0 ? [] : parseSubmodulePaths(gitmodules);
18506
+ const searchDirs = [
18507
+ repoDir,
18508
+ ...submodulePaths.map((p3) => join(repoDir, p3))
18509
+ ];
18510
+ const found = /* @__PURE__ */ new Set();
18511
+ for (const dir of searchDirs) {
18512
+ for (const name of yield* detectProfilesInDir(dir)) {
18513
+ found.add(name);
18514
+ }
18515
+ }
18516
+ const profiles = listProfiles().filter((p3) => found.has(p3.name));
18517
+ return { profiles };
18518
+ });
18519
+ var getProfileMismatchWarning = (selectedProfiles, detection) => {
18520
+ if (detection.profiles.length === 0) return void 0;
18521
+ const detectedNames = new Set(detection.profiles.map((p3) => p3.name));
18522
+ const mismatches = selectedProfiles.filter((p3) => !detectedNames.has(p3.name));
18523
+ if (mismatches.length === 0) return void 0;
18524
+ const selected = selectedProfiles.map((p3) => p3.name).join(", ");
18525
+ const detected = detection.profiles.map((p3) => p3.name).join(", ");
18526
+ const mismatchNames = mismatches.map((p3) => p3.name).join(", ");
18527
+ return `Selected profile${mismatches.length === 1 ? "" : "s"} ${mismatchNames} did not match detected repository profile${detection.profiles.length === 1 ? "" : "s"} (${detected}). Continuing because custom layouts and monorepos may still be valid. Selected: ${selected}.`;
18528
+ };
18413
18529
  function getNextStepsLines(template, mainFilename, issueTracker, agent, packageManager) {
18414
18530
  if (issueTracker.name === "custom") {
18415
18531
  return [
@@ -18530,6 +18646,61 @@ var rewritePromptFiles = (configDir) => Effect_exports.gen(function* () {
18530
18646
  { concurrency: "unbounded" }
18531
18647
  );
18532
18648
  });
18649
+ var isPromptFile = (filename) => filename.endsWith("prompt.md");
18650
+ var buildProfilesPromptSection = (profiles) => {
18651
+ const entries = profiles.map((p3) => `- ${p3.label} \u2014 \`.sandcastle/${profileGuidancePath(p3.name)}\``).join("\n");
18652
+ return `# Project profiles
18653
+
18654
+ This project was scaffolded for the profile(s) below. Read each guidance file and follow its setup and validation commands instead of assuming a specific toolchain:
18655
+
18656
+ ${entries}
18657
+ `;
18658
+ };
18659
+ var buildProfileSetupHookCommand = (profile, setupCommand) => {
18660
+ const bin = setupCommand.split(" ")[0];
18661
+ const guidance = `.sandcastle/${profileGuidancePath(profile.name)}`;
18662
+ return `command -v ${bin} >/dev/null 2>&1 && ${setupCommand} || echo 'Sandcastle: ${bin} not found in sandbox \u2014 add it to .sandcastle/Dockerfile (see ${guidance})'`;
18663
+ };
18664
+ var rewriteProfileReferences = (configDir, profiles, mainFilename) => Effect_exports.gen(function* () {
18665
+ const fs = yield* FileSystem_exports.FileSystem;
18666
+ const files = yield* fs.readDirectory(configDir).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18667
+ const promptSection = buildProfilesPromptSection(profiles);
18668
+ const promptFiles = files.filter(isPromptFile);
18669
+ yield* Effect_exports.all(
18670
+ promptFiles.map(
18671
+ (f) => Effect_exports.gen(function* () {
18672
+ const filePath = join(configDir, f);
18673
+ const content = yield* fs.readFileString(filePath).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18674
+ let updated = content.replace(
18675
+ /`npm run typecheck` and `npm run test`/g,
18676
+ "the validation commands listed in your project profile guidance under `.sandcastle/profiles/`"
18677
+ );
18678
+ updated = `${updated.trimEnd()}
18679
+
18680
+ ${promptSection}`;
18681
+ yield* fs.writeFileString(filePath, updated).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18682
+ })
18683
+ ),
18684
+ { concurrency: "unbounded" }
18685
+ );
18686
+ const hasJsProfile = profiles.some((p3) => p3.name === DEFAULT_PROFILE_NAME);
18687
+ const primary = profiles[0];
18688
+ const setupCommand = primary?.setupCommands[0];
18689
+ if (!hasJsProfile && primary && setupCommand) {
18690
+ const mainPath = join(configDir, mainFilename);
18691
+ const mainExists = yield* fs.exists(mainPath).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18692
+ if (mainExists) {
18693
+ const mainContent = yield* fs.readFileString(mainPath).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18694
+ const rewritten = mainContent.replace(
18695
+ /command: "npm install"/g,
18696
+ `command: ${JSON.stringify(buildProfileSetupHookCommand(primary, setupCommand))}`
18697
+ );
18698
+ if (rewritten !== mainContent) {
18699
+ yield* fs.writeFileString(mainPath, rewritten).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18700
+ }
18701
+ }
18702
+ }
18703
+ });
18533
18704
  var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
18534
18705
  ".md",
18535
18706
  ".txt",
@@ -18625,6 +18796,54 @@ sandcastle ${cliNamespace} build-image
18625
18796
 
18626
18797
  Run your **list** command inside the built image and confirm it returns the open tasks as JSON. If it errors, fix the command or the auth and rebuild.
18627
18798
  `;
18799
+ var PROFILES_DIR = "profiles";
18800
+ var PROFILES_METADATA_FILE = `${PROFILES_DIR}/profiles.json`;
18801
+ var profileGuidancePath = (profileName) => `${PROFILES_DIR}/${profileName}.md`;
18802
+ var buildProfileGuidanceDoc = (profile) => {
18803
+ const setup = profile.setupCommands.map((c) => `- \`${c}\``).join("\n");
18804
+ const commands = profile.validationCommands.map((c) => `- \`${c}\``).join("\n");
18805
+ return `# ${profile.label} profile
18806
+
18807
+ ${profile.guidance}
18808
+
18809
+ ## Suggested setup commands
18810
+
18811
+ ${setup}
18812
+
18813
+ ## Suggested validation commands
18814
+
18815
+ ${commands}
18816
+
18817
+ > These commands are guidance, not a contract. Sandcastle does not install or pin the SDK for this profile \u2014 adapt them to the project's actual scripts and tooling.
18818
+ `;
18819
+ };
18820
+ var scaffoldProfiles = (configDir, profiles) => Effect_exports.gen(function* () {
18821
+ const fs = yield* FileSystem_exports.FileSystem;
18822
+ const profilesDir = join(configDir, PROFILES_DIR);
18823
+ yield* fs.makeDirectory(profilesDir, { recursive: true }).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18824
+ const metadata = {
18825
+ profiles: profiles.map((p3) => ({
18826
+ name: p3.name,
18827
+ label: p3.label,
18828
+ guidance: profileGuidancePath(p3.name)
18829
+ }))
18830
+ };
18831
+ yield* Effect_exports.all(
18832
+ [
18833
+ fs.writeFileString(
18834
+ join(configDir, PROFILES_METADATA_FILE),
18835
+ JSON.stringify(metadata, null, 2) + "\n"
18836
+ ).pipe(Effect_exports.mapError((e) => new Error(e.message))),
18837
+ ...profiles.map(
18838
+ (profile) => fs.writeFileString(
18839
+ join(configDir, profileGuidancePath(profile.name)),
18840
+ buildProfileGuidanceDoc(profile)
18841
+ ).pipe(Effect_exports.mapError((e) => new Error(e.message)))
18842
+ )
18843
+ ],
18844
+ { concurrency: "unbounded" }
18845
+ );
18846
+ });
18628
18847
  var detectMainFilename = (repoDir) => Effect_exports.gen(function* () {
18629
18848
  const fs = yield* FileSystem_exports.FileSystem;
18630
18849
  const pkgPath = join(repoDir, "package.json");
@@ -18646,9 +18865,13 @@ var scaffold = (repoDir, options3) => Effect_exports.gen(function* () {
18646
18865
  createLabel = true,
18647
18866
  issueTracker = ISSUE_TRACKER_REGISTRY[0],
18648
18867
  // default: github-issues
18649
- sandboxProvider = SANDBOX_PROVIDER_REGISTRY[0]
18868
+ sandboxProvider = SANDBOX_PROVIDER_REGISTRY[0],
18650
18869
  // default: docker
18870
+ profiles
18651
18871
  } = options3;
18872
+ const selectedProfiles = resolveProfileEntries(
18873
+ (profiles ?? []).map((p3) => p3.name)
18874
+ );
18652
18875
  if (agents.length === 0) {
18653
18876
  yield* Effect_exports.fail(new Error("At least one agent must be selected."));
18654
18877
  }
@@ -18698,9 +18921,11 @@ var scaffold = (repoDir, options3) => Effect_exports.gen(function* () {
18698
18921
  buildSetupIssueTrackerDoc(sandboxProvider.cliNamespace)
18699
18922
  ).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18700
18923
  }
18924
+ yield* scaffoldProfiles(configDir, selectedProfiles);
18925
+ yield* rewriteProfileReferences(configDir, selectedProfiles, mainFilename);
18701
18926
  return { mainFilename };
18702
18927
  });
18703
- var VERSION = "0.7.0" ;
18928
+ var VERSION = "0.8.1" ;
18704
18929
 
18705
18930
  // src/cli.ts
18706
18931
  var imageNameOption = Options_exports.text("image-name").pipe(
@@ -18754,6 +18979,12 @@ var issueTrackerOption = Options_exports.text("issue-tracker").pipe(
18754
18979
  ),
18755
18980
  Options_exports.optional
18756
18981
  );
18982
+ var profileOption = Options_exports.text("profile").pipe(
18983
+ Options_exports.withDescription(
18984
+ "Comma-separated project profiles to scaffold (e.g. js-ts,go)"
18985
+ ),
18986
+ Options_exports.optional
18987
+ );
18757
18988
  var createLabelOption = Options_exports.choice("create-label", [
18758
18989
  "true",
18759
18990
  "false"
@@ -18779,6 +19010,24 @@ var installTemplateDepsOption = Options_exports.choice("install-template-deps",
18779
19010
  Options_exports.optional
18780
19011
  );
18781
19012
  var choiceToTriBool = (opt) => opt._tag === "Some" ? Option_exports.some(opt.value === "true") : Option_exports.none();
19013
+ var parseCommaSeparatedNames = (value2) => value2.split(",").map((n) => n.trim()).filter((n) => n.length > 0);
19014
+ var resolveProfileFlagEntries = (value2) => {
19015
+ const names = parseCommaSeparatedNames(value2);
19016
+ if (names.length === 0) {
19017
+ throw new Error("--profile must name at least one profile.");
19018
+ }
19019
+ return resolveProfileEntries(names);
19020
+ };
19021
+ var promptForProfiles = async (profiles = listProfiles()) => clack.multiselect({
19022
+ message: "Select one or more project profiles:",
19023
+ initialValues: [DEFAULT_PROFILE_NAME],
19024
+ required: true,
19025
+ options: profiles.map((p3) => ({
19026
+ value: p3.name,
19027
+ label: p3.label,
19028
+ hint: p3.validationCommands.join(", ")
19029
+ }))
19030
+ });
18782
19031
  var initCommand = Command_exports.make(
18783
19032
  "init",
18784
19033
  {
@@ -18788,6 +19037,7 @@ var initCommand = Command_exports.make(
18788
19037
  model: initModelOption,
18789
19038
  sandbox: sandboxOption,
18790
19039
  issueTracker: issueTrackerOption,
19040
+ profile: profileOption,
18791
19041
  createLabel: createLabelOption,
18792
19042
  buildImage: buildImageOption,
18793
19043
  installTemplateDeps: installTemplateDepsOption
@@ -18799,6 +19049,7 @@ var initCommand = Command_exports.make(
18799
19049
  model: modelFlag,
18800
19050
  sandbox: sandboxFlag,
18801
19051
  issueTracker: issueTrackerFlag,
19052
+ profile: profileFlag,
18802
19053
  createLabel: createLabelFlag,
18803
19054
  buildImage: buildImageFlag,
18804
19055
  installTemplateDeps: installTemplateDepsFlag
@@ -18840,6 +19091,17 @@ var initCommand = Command_exports.make(
18840
19091
  );
18841
19092
  }
18842
19093
  }
19094
+ if (profileFlag._tag === "Some") {
19095
+ try {
19096
+ resolveProfileFlagEntries(profileFlag.value);
19097
+ } catch (e) {
19098
+ yield* Effect_exports.fail(
19099
+ new InitError({
19100
+ message: `${e instanceof Error ? e.message : e}`
19101
+ })
19102
+ );
19103
+ }
19104
+ }
18843
19105
  const createLabelChoice = choiceToTriBool(createLabelFlag);
18844
19106
  const buildImageChoice = choiceToTriBool(buildImageFlag);
18845
19107
  const installTemplateDepsChoice = choiceToTriBool(
@@ -18920,6 +19182,31 @@ var initCommand = Command_exports.make(
18920
19182
  }
18921
19183
  const defaultAgent = selectedAgents[0];
18922
19184
  const modelOverride = modelFlag._tag === "Some" ? modelFlag.value : void 0;
19185
+ const profiles = listProfiles();
19186
+ let selectedProfiles;
19187
+ if (profileFlag._tag === "Some") {
19188
+ selectedProfiles = resolveProfileFlagEntries(profileFlag.value);
19189
+ } else if (isInteractive) {
19190
+ const selected = yield* Effect_exports.promise(
19191
+ () => promptForProfiles(profiles)
19192
+ );
19193
+ if (clack.isCancel(selected)) {
19194
+ yield* Effect_exports.fail(
19195
+ new InitError({ message: "Profile selection cancelled." })
19196
+ );
19197
+ }
19198
+ selectedProfiles = selected.map((n) => getProfile(n));
19199
+ } else {
19200
+ selectedProfiles = resolveProfileEntries([]);
19201
+ }
19202
+ const repositoryProfileDetection = yield* detectRepositoryProfiles(cwd);
19203
+ const profileMismatchWarning = getProfileMismatchWarning(
19204
+ selectedProfiles,
19205
+ repositoryProfileDetection
19206
+ );
19207
+ if (profileMismatchWarning) {
19208
+ yield* d.status(profileMismatchWarning, "warn");
19209
+ }
18923
19210
  const sandboxProviders = listSandboxProviders();
18924
19211
  let selectedSandboxProvider;
18925
19212
  if (sandboxFlag._tag === "Some") {
@@ -19024,7 +19311,8 @@ var initCommand = Command_exports.make(
19024
19311
  templateName: selectedTemplate,
19025
19312
  createLabel: shouldCreateLabel,
19026
19313
  issueTracker: selectedIssueTracker,
19027
- sandboxProvider: selectedSandboxProvider
19314
+ sandboxProvider: selectedSandboxProvider,
19315
+ profiles: selectedProfiles
19028
19316
  }).pipe(
19029
19317
  Effect_exports.mapError(
19030
19318
  (e) => new InitError({