@rockclaver/sandcastle 0.7.0 → 0.8.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/main.js CHANGED
@@ -18410,6 +18410,106 @@ 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 detectRepositoryProfiles = (repoDir) => Effect_exports.gen(function* () {
18482
+ const detected = [];
18483
+ const hasJsSignal = (yield* hasFile(repoDir, "package.json")) || (yield* Effect_exports.all(
18484
+ LOCKFILES.map(([file4]) => hasFile(repoDir, file4)),
18485
+ {
18486
+ concurrency: "unbounded"
18487
+ }
18488
+ )).some(Boolean);
18489
+ if (hasJsSignal) {
18490
+ detected.push(getProfile("js-ts"));
18491
+ }
18492
+ const pubspec = yield* readOptionalFile(repoDir, "pubspec.yaml");
18493
+ if (pubspec !== void 0) {
18494
+ detected.push(
18495
+ getProfile(hasFlutterMarkers(pubspec) ? "flutter" : "dart")
18496
+ );
18497
+ }
18498
+ if (yield* hasFile(repoDir, "go.mod")) {
18499
+ detected.push(getProfile("go"));
18500
+ }
18501
+ return { profiles: detected };
18502
+ });
18503
+ var getProfileMismatchWarning = (selectedProfiles, detection) => {
18504
+ if (detection.profiles.length === 0) return void 0;
18505
+ const detectedNames = new Set(detection.profiles.map((p3) => p3.name));
18506
+ const mismatches = selectedProfiles.filter((p3) => !detectedNames.has(p3.name));
18507
+ if (mismatches.length === 0) return void 0;
18508
+ const selected = selectedProfiles.map((p3) => p3.name).join(", ");
18509
+ const detected = detection.profiles.map((p3) => p3.name).join(", ");
18510
+ const mismatchNames = mismatches.map((p3) => p3.name).join(", ");
18511
+ 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}.`;
18512
+ };
18413
18513
  function getNextStepsLines(template, mainFilename, issueTracker, agent, packageManager) {
18414
18514
  if (issueTracker.name === "custom") {
18415
18515
  return [
@@ -18530,6 +18630,61 @@ var rewritePromptFiles = (configDir) => Effect_exports.gen(function* () {
18530
18630
  { concurrency: "unbounded" }
18531
18631
  );
18532
18632
  });
18633
+ var isPromptFile = (filename) => filename.endsWith("prompt.md");
18634
+ var buildProfilesPromptSection = (profiles) => {
18635
+ const entries = profiles.map((p3) => `- ${p3.label} \u2014 \`.sandcastle/${profileGuidancePath(p3.name)}\``).join("\n");
18636
+ return `# Project profiles
18637
+
18638
+ 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:
18639
+
18640
+ ${entries}
18641
+ `;
18642
+ };
18643
+ var buildProfileSetupHookCommand = (profile, setupCommand) => {
18644
+ const bin = setupCommand.split(" ")[0];
18645
+ const guidance = `.sandcastle/${profileGuidancePath(profile.name)}`;
18646
+ return `command -v ${bin} >/dev/null 2>&1 && ${setupCommand} || echo 'Sandcastle: ${bin} not found in sandbox \u2014 add it to .sandcastle/Dockerfile (see ${guidance})'`;
18647
+ };
18648
+ var rewriteProfileReferences = (configDir, profiles, mainFilename) => Effect_exports.gen(function* () {
18649
+ const fs = yield* FileSystem_exports.FileSystem;
18650
+ const files = yield* fs.readDirectory(configDir).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18651
+ const promptSection = buildProfilesPromptSection(profiles);
18652
+ const promptFiles = files.filter(isPromptFile);
18653
+ yield* Effect_exports.all(
18654
+ promptFiles.map(
18655
+ (f) => Effect_exports.gen(function* () {
18656
+ const filePath = join(configDir, f);
18657
+ const content = yield* fs.readFileString(filePath).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18658
+ let updated = content.replace(
18659
+ /`npm run typecheck` and `npm run test`/g,
18660
+ "the validation commands listed in your project profile guidance under `.sandcastle/profiles/`"
18661
+ );
18662
+ updated = `${updated.trimEnd()}
18663
+
18664
+ ${promptSection}`;
18665
+ yield* fs.writeFileString(filePath, updated).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18666
+ })
18667
+ ),
18668
+ { concurrency: "unbounded" }
18669
+ );
18670
+ const hasJsProfile = profiles.some((p3) => p3.name === DEFAULT_PROFILE_NAME);
18671
+ const primary = profiles[0];
18672
+ const setupCommand = primary?.setupCommands[0];
18673
+ if (!hasJsProfile && primary && setupCommand) {
18674
+ const mainPath = join(configDir, mainFilename);
18675
+ const mainExists = yield* fs.exists(mainPath).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18676
+ if (mainExists) {
18677
+ const mainContent = yield* fs.readFileString(mainPath).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18678
+ const rewritten = mainContent.replace(
18679
+ /command: "npm install"/g,
18680
+ `command: ${JSON.stringify(buildProfileSetupHookCommand(primary, setupCommand))}`
18681
+ );
18682
+ if (rewritten !== mainContent) {
18683
+ yield* fs.writeFileString(mainPath, rewritten).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18684
+ }
18685
+ }
18686
+ }
18687
+ });
18533
18688
  var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
18534
18689
  ".md",
18535
18690
  ".txt",
@@ -18625,6 +18780,54 @@ sandcastle ${cliNamespace} build-image
18625
18780
 
18626
18781
  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
18782
  `;
18783
+ var PROFILES_DIR = "profiles";
18784
+ var PROFILES_METADATA_FILE = `${PROFILES_DIR}/profiles.json`;
18785
+ var profileGuidancePath = (profileName) => `${PROFILES_DIR}/${profileName}.md`;
18786
+ var buildProfileGuidanceDoc = (profile) => {
18787
+ const setup = profile.setupCommands.map((c) => `- \`${c}\``).join("\n");
18788
+ const commands = profile.validationCommands.map((c) => `- \`${c}\``).join("\n");
18789
+ return `# ${profile.label} profile
18790
+
18791
+ ${profile.guidance}
18792
+
18793
+ ## Suggested setup commands
18794
+
18795
+ ${setup}
18796
+
18797
+ ## Suggested validation commands
18798
+
18799
+ ${commands}
18800
+
18801
+ > 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.
18802
+ `;
18803
+ };
18804
+ var scaffoldProfiles = (configDir, profiles) => Effect_exports.gen(function* () {
18805
+ const fs = yield* FileSystem_exports.FileSystem;
18806
+ const profilesDir = join(configDir, PROFILES_DIR);
18807
+ yield* fs.makeDirectory(profilesDir, { recursive: true }).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18808
+ const metadata = {
18809
+ profiles: profiles.map((p3) => ({
18810
+ name: p3.name,
18811
+ label: p3.label,
18812
+ guidance: profileGuidancePath(p3.name)
18813
+ }))
18814
+ };
18815
+ yield* Effect_exports.all(
18816
+ [
18817
+ fs.writeFileString(
18818
+ join(configDir, PROFILES_METADATA_FILE),
18819
+ JSON.stringify(metadata, null, 2) + "\n"
18820
+ ).pipe(Effect_exports.mapError((e) => new Error(e.message))),
18821
+ ...profiles.map(
18822
+ (profile) => fs.writeFileString(
18823
+ join(configDir, profileGuidancePath(profile.name)),
18824
+ buildProfileGuidanceDoc(profile)
18825
+ ).pipe(Effect_exports.mapError((e) => new Error(e.message)))
18826
+ )
18827
+ ],
18828
+ { concurrency: "unbounded" }
18829
+ );
18830
+ });
18628
18831
  var detectMainFilename = (repoDir) => Effect_exports.gen(function* () {
18629
18832
  const fs = yield* FileSystem_exports.FileSystem;
18630
18833
  const pkgPath = join(repoDir, "package.json");
@@ -18646,9 +18849,13 @@ var scaffold = (repoDir, options3) => Effect_exports.gen(function* () {
18646
18849
  createLabel = true,
18647
18850
  issueTracker = ISSUE_TRACKER_REGISTRY[0],
18648
18851
  // default: github-issues
18649
- sandboxProvider = SANDBOX_PROVIDER_REGISTRY[0]
18852
+ sandboxProvider = SANDBOX_PROVIDER_REGISTRY[0],
18650
18853
  // default: docker
18854
+ profiles
18651
18855
  } = options3;
18856
+ const selectedProfiles = resolveProfileEntries(
18857
+ (profiles ?? []).map((p3) => p3.name)
18858
+ );
18652
18859
  if (agents.length === 0) {
18653
18860
  yield* Effect_exports.fail(new Error("At least one agent must be selected."));
18654
18861
  }
@@ -18698,9 +18905,11 @@ var scaffold = (repoDir, options3) => Effect_exports.gen(function* () {
18698
18905
  buildSetupIssueTrackerDoc(sandboxProvider.cliNamespace)
18699
18906
  ).pipe(Effect_exports.mapError((e) => new Error(e.message)));
18700
18907
  }
18908
+ yield* scaffoldProfiles(configDir, selectedProfiles);
18909
+ yield* rewriteProfileReferences(configDir, selectedProfiles, mainFilename);
18701
18910
  return { mainFilename };
18702
18911
  });
18703
- var VERSION = "0.7.0" ;
18912
+ var VERSION = "0.8.0" ;
18704
18913
 
18705
18914
  // src/cli.ts
18706
18915
  var imageNameOption = Options_exports.text("image-name").pipe(
@@ -18754,6 +18963,12 @@ var issueTrackerOption = Options_exports.text("issue-tracker").pipe(
18754
18963
  ),
18755
18964
  Options_exports.optional
18756
18965
  );
18966
+ var profileOption = Options_exports.text("profile").pipe(
18967
+ Options_exports.withDescription(
18968
+ "Comma-separated project profiles to scaffold (e.g. js-ts,go)"
18969
+ ),
18970
+ Options_exports.optional
18971
+ );
18757
18972
  var createLabelOption = Options_exports.choice("create-label", [
18758
18973
  "true",
18759
18974
  "false"
@@ -18779,6 +18994,24 @@ var installTemplateDepsOption = Options_exports.choice("install-template-deps",
18779
18994
  Options_exports.optional
18780
18995
  );
18781
18996
  var choiceToTriBool = (opt) => opt._tag === "Some" ? Option_exports.some(opt.value === "true") : Option_exports.none();
18997
+ var parseCommaSeparatedNames = (value2) => value2.split(",").map((n) => n.trim()).filter((n) => n.length > 0);
18998
+ var resolveProfileFlagEntries = (value2) => {
18999
+ const names = parseCommaSeparatedNames(value2);
19000
+ if (names.length === 0) {
19001
+ throw new Error("--profile must name at least one profile.");
19002
+ }
19003
+ return resolveProfileEntries(names);
19004
+ };
19005
+ var promptForProfiles = async (profiles = listProfiles()) => clack.multiselect({
19006
+ message: "Select one or more project profiles:",
19007
+ initialValues: [DEFAULT_PROFILE_NAME],
19008
+ required: true,
19009
+ options: profiles.map((p3) => ({
19010
+ value: p3.name,
19011
+ label: p3.label,
19012
+ hint: p3.validationCommands.join(", ")
19013
+ }))
19014
+ });
18782
19015
  var initCommand = Command_exports.make(
18783
19016
  "init",
18784
19017
  {
@@ -18788,6 +19021,7 @@ var initCommand = Command_exports.make(
18788
19021
  model: initModelOption,
18789
19022
  sandbox: sandboxOption,
18790
19023
  issueTracker: issueTrackerOption,
19024
+ profile: profileOption,
18791
19025
  createLabel: createLabelOption,
18792
19026
  buildImage: buildImageOption,
18793
19027
  installTemplateDeps: installTemplateDepsOption
@@ -18799,6 +19033,7 @@ var initCommand = Command_exports.make(
18799
19033
  model: modelFlag,
18800
19034
  sandbox: sandboxFlag,
18801
19035
  issueTracker: issueTrackerFlag,
19036
+ profile: profileFlag,
18802
19037
  createLabel: createLabelFlag,
18803
19038
  buildImage: buildImageFlag,
18804
19039
  installTemplateDeps: installTemplateDepsFlag
@@ -18840,6 +19075,17 @@ var initCommand = Command_exports.make(
18840
19075
  );
18841
19076
  }
18842
19077
  }
19078
+ if (profileFlag._tag === "Some") {
19079
+ try {
19080
+ resolveProfileFlagEntries(profileFlag.value);
19081
+ } catch (e) {
19082
+ yield* Effect_exports.fail(
19083
+ new InitError({
19084
+ message: `${e instanceof Error ? e.message : e}`
19085
+ })
19086
+ );
19087
+ }
19088
+ }
18843
19089
  const createLabelChoice = choiceToTriBool(createLabelFlag);
18844
19090
  const buildImageChoice = choiceToTriBool(buildImageFlag);
18845
19091
  const installTemplateDepsChoice = choiceToTriBool(
@@ -18920,6 +19166,31 @@ var initCommand = Command_exports.make(
18920
19166
  }
18921
19167
  const defaultAgent = selectedAgents[0];
18922
19168
  const modelOverride = modelFlag._tag === "Some" ? modelFlag.value : void 0;
19169
+ const profiles = listProfiles();
19170
+ let selectedProfiles;
19171
+ if (profileFlag._tag === "Some") {
19172
+ selectedProfiles = resolveProfileFlagEntries(profileFlag.value);
19173
+ } else if (isInteractive) {
19174
+ const selected = yield* Effect_exports.promise(
19175
+ () => promptForProfiles(profiles)
19176
+ );
19177
+ if (clack.isCancel(selected)) {
19178
+ yield* Effect_exports.fail(
19179
+ new InitError({ message: "Profile selection cancelled." })
19180
+ );
19181
+ }
19182
+ selectedProfiles = selected.map((n) => getProfile(n));
19183
+ } else {
19184
+ selectedProfiles = resolveProfileEntries([]);
19185
+ }
19186
+ const repositoryProfileDetection = yield* detectRepositoryProfiles(cwd);
19187
+ const profileMismatchWarning = getProfileMismatchWarning(
19188
+ selectedProfiles,
19189
+ repositoryProfileDetection
19190
+ );
19191
+ if (profileMismatchWarning) {
19192
+ yield* d.status(profileMismatchWarning, "warn");
19193
+ }
18923
19194
  const sandboxProviders = listSandboxProviders();
18924
19195
  let selectedSandboxProvider;
18925
19196
  if (sandboxFlag._tag === "Some") {
@@ -19024,7 +19295,8 @@ var initCommand = Command_exports.make(
19024
19295
  templateName: selectedTemplate,
19025
19296
  createLabel: shouldCreateLabel,
19026
19297
  issueTracker: selectedIssueTracker,
19027
- sandboxProvider: selectedSandboxProvider
19298
+ sandboxProvider: selectedSandboxProvider,
19299
+ profiles: selectedProfiles
19028
19300
  }).pipe(
19029
19301
  Effect_exports.mapError(
19030
19302
  (e) => new InitError({