@shawaze/agentspace 0.3.2 → 0.3.5

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.
Files changed (3) hide show
  1. package/README.md +22 -0
  2. package/dist/cli.js +98 -28
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -89,6 +89,28 @@ npx @shawaze/agentspace init
89
89
  It refuses to write into a non-empty directory unless you pass `--force`, and
90
90
  `--dry-run` prints what it would write without touching disk.
91
91
 
92
+ **Non-interactive** (CI / reproducible scaffolding): skip the wizard and pass a
93
+ JSON config.
94
+
95
+ ```bash
96
+ npx @shawaze/agentspace init --config workspace.json
97
+ ```
98
+
99
+ ```jsonc
100
+ // workspace.json
101
+ {
102
+ "workspaceName": "my-product",
103
+ "shape": "one-product",
104
+ "repos": [
105
+ { "name": "api", "remote": "https://github.com/me/api.git", "stack": "rails", "role": "backend" },
106
+ { "name": "web", "remote": "https://github.com/me/web.git", "stack": "nextjs", "role": "frontend" }
107
+ ],
108
+ "dependencyOrder": ["api", "web"],
109
+ "pillars": ["manifest", "wiki", "enforcement", "contracts"],
110
+ "enforcement": { "mode": "auto", "warmPages": 5, "warmSessions": 10 }
111
+ }
112
+ ```
113
+
92
114
  ### `agentspace doctor`
93
115
 
94
116
  Mechanical health checks on a generated workspace — manifest validity,
package/dist/cli.js CHANGED
@@ -265,7 +265,7 @@ function generateManifest(ctx) {
265
265
  };
266
266
  return [
267
267
  { path: "manifest.yaml", contents: render(MANIFEST_YAML, view) },
268
- { path: "clone-repos.sh", contents: render(CLONE_REPOS_SH, view) },
268
+ { path: "clone-repos.sh", contents: render(CLONE_REPOS_SH, view), mode: 493 },
269
269
  { path: ".gitignore", contents: render(GITIGNORE, view) },
270
270
  { path: "CLAUDE.md", contents: render(ROOT_CLAUDE, view) },
271
271
  { path: "README.md", contents: render(ROOT_README, view) }
@@ -668,6 +668,7 @@ function claudeCodeAdapter(intents, ctx) {
668
668
 
669
669
  // src/fs/writeTree.ts
670
670
  import {
671
+ chmod,
671
672
  cp,
672
673
  mkdir,
673
674
  mkdtemp,
@@ -676,6 +677,7 @@ import {
676
677
  rm,
677
678
  writeFile
678
679
  } from "fs/promises";
680
+ import { existsSync as existsSync3 } from "fs";
679
681
  import { dirname as dirname2, join as join4 } from "path";
680
682
  async function isNonEmptyDir(dir) {
681
683
  try {
@@ -699,16 +701,12 @@ async function writeTree(files, targetDir, opts) {
699
701
  const dest = join4(temp, file.path);
700
702
  await mkdir(dirname2(dest), { recursive: true });
701
703
  await writeFile(dest, file.contents);
704
+ if (file.mode !== void 0) await chmod(dest, file.mode);
702
705
  }
703
- if (opts.force) {
704
- await cp(temp, targetDir, { recursive: true, force: true });
705
- await rm(temp, { recursive: true, force: true });
706
- } else if (await isNonEmptyDir(targetDir)) {
706
+ if (existsSync3(targetDir)) {
707
707
  await cp(temp, targetDir, { recursive: true, force: true });
708
708
  await rm(temp, { recursive: true, force: true });
709
709
  } else {
710
- await rm(targetDir, { recursive: true, force: true }).catch(() => {
711
- });
712
710
  await rename(temp, targetDir);
713
711
  }
714
712
  } catch (err) {
@@ -729,20 +727,24 @@ var DEFAULT_ENFORCEMENT = {
729
727
  };
730
728
 
731
729
  // src/wizard/assemble.ts
730
+ var str = (v) => (v ?? "").trim();
732
731
  function assembleConfig(answers) {
733
732
  const pillars = ["manifest"];
734
733
  if (answers.enableWiki) pillars.push("wiki");
735
734
  if (answers.enableEnforcement) pillars.push("enforcement");
736
735
  if (answers.enableContracts) pillars.push("contracts");
737
736
  return {
738
- workspaceName: answers.workspaceName.trim(),
737
+ workspaceName: str(answers.workspaceName),
739
738
  shape: answers.shape,
740
- repos: answers.repos.map((r) => ({
741
- name: r.name.trim(),
742
- remote: r.remote.trim() === "" ? null : r.remote.trim(),
743
- stack: r.stack,
744
- role: r.role.trim()
745
- })),
739
+ repos: answers.repos.map((r) => {
740
+ const remote = str(r.remote);
741
+ return {
742
+ name: str(r.name),
743
+ remote: remote === "" ? null : remote,
744
+ stack: str(r.stack) || "generic",
745
+ role: str(r.role)
746
+ };
747
+ }),
746
748
  dependencyOrder: shapeHasDependencyOrder(answers.shape) ? answers.dependencyOrder : null,
747
749
  pillars,
748
750
  enforcement: answers.enableEnforcement ? { ...DEFAULT_ENFORCEMENT } : null
@@ -873,6 +875,71 @@ async function runWizard() {
873
875
  });
874
876
  }
875
877
 
878
+ // src/config.ts
879
+ import { readFileSync as readFileSync3 } from "fs";
880
+ var SHAPES = [
881
+ "single-repo",
882
+ "one-product",
883
+ "peer-services",
884
+ "library-consumers",
885
+ "unrelated"
886
+ ];
887
+ var PILLARS = ["manifest", "wiki", "contracts", "enforcement"];
888
+ var MODES = ["auto", "warn", "block"];
889
+ function validateConfig(raw) {
890
+ if (!raw || typeof raw !== "object") {
891
+ throw new Error("config must be a JSON object");
892
+ }
893
+ const o = raw;
894
+ const workspaceName = typeof o.workspaceName === "string" ? o.workspaceName.trim() : "";
895
+ if (!workspaceName) throw new Error("config.workspaceName is required");
896
+ if (typeof o.shape !== "string" || !SHAPES.includes(o.shape)) {
897
+ throw new Error(`config.shape must be one of: ${SHAPES.join(", ")}`);
898
+ }
899
+ const shape = o.shape;
900
+ if (!Array.isArray(o.repos) || o.repos.length === 0) {
901
+ throw new Error("config.repos must be a non-empty array");
902
+ }
903
+ const repos = o.repos.map((r, i) => {
904
+ const rr = r ?? {};
905
+ const name = typeof rr.name === "string" ? rr.name.trim() : "";
906
+ if (!name) throw new Error(`config.repos[${i}].name is required`);
907
+ const remote = typeof rr.remote === "string" && rr.remote.trim() !== "" ? rr.remote.trim() : null;
908
+ const stack = typeof rr.stack === "string" && rr.stack.trim() ? rr.stack.trim() : "generic";
909
+ const role = typeof rr.role === "string" ? rr.role.trim() : "";
910
+ return { name, remote, stack, role };
911
+ });
912
+ let pillars = Array.isArray(o.pillars) ? o.pillars.filter((p2) => PILLARS.includes(p2)) : ["manifest", "wiki"];
913
+ if (pillars.length === 0) pillars = ["manifest", "wiki"];
914
+ if (!pillars.includes("manifest")) pillars = ["manifest", ...pillars];
915
+ const dependencyOrder = Array.isArray(o.dependencyOrder) ? o.dependencyOrder.filter((s) => typeof s === "string") : null;
916
+ let enforcement = null;
917
+ if (pillars.includes("enforcement")) {
918
+ const e = o.enforcement ?? {};
919
+ enforcement = {
920
+ mode: MODES.includes(e.mode) ? e.mode : DEFAULT_ENFORCEMENT.mode,
921
+ warmPages: typeof e.warmPages === "number" ? e.warmPages : DEFAULT_ENFORCEMENT.warmPages,
922
+ warmSessions: typeof e.warmSessions === "number" ? e.warmSessions : DEFAULT_ENFORCEMENT.warmSessions
923
+ };
924
+ }
925
+ return { workspaceName, shape, repos, dependencyOrder, pillars, enforcement };
926
+ }
927
+ function loadConfig(path) {
928
+ let raw;
929
+ try {
930
+ raw = readFileSync3(path, "utf8");
931
+ } catch {
932
+ throw new Error(`Cannot read config file: ${path}`);
933
+ }
934
+ let parsed;
935
+ try {
936
+ parsed = JSON.parse(raw);
937
+ } catch {
938
+ throw new Error(`config file is not valid JSON: ${path}`);
939
+ }
940
+ return validateConfig(parsed);
941
+ }
942
+
876
943
  // src/commands/init.ts
877
944
  function generateWorkspace(config, today) {
878
945
  const ctx = buildContext(config, today);
@@ -895,8 +962,8 @@ async function runInit(config, targetDir, opts) {
895
962
  await writeTree(files, targetDir, { force: opts.force });
896
963
  }
897
964
  async function initCommand(opts) {
898
- const config = await runWizard();
899
- await runInit(config, process.cwd(), opts);
965
+ const config = opts.configPath ? loadConfig(opts.configPath) : await runWizard();
966
+ await runInit(config, process.cwd(), { force: opts.force, today: opts.today });
900
967
  console.log(`
901
968
  \u2713 Created ${config.workspaceName} (${config.pillars.join(", ")}).`);
902
969
  console.log(" Next: ./clone-repos.sh");
@@ -909,7 +976,7 @@ async function initCommand(opts) {
909
976
 
910
977
  // src/commands/doctor.ts
911
978
  import { readFile, readdir as readdir2, stat } from "fs/promises";
912
- import { existsSync as existsSync3 } from "fs";
979
+ import { existsSync as existsSync4 } from "fs";
913
980
  import { join as join5, relative } from "path";
914
981
  import { parse as parse2 } from "yaml";
915
982
 
@@ -1004,7 +1071,7 @@ async function runChecks(workspaceDir, today, deps = {}) {
1004
1071
  }
1005
1072
  }
1006
1073
  }
1007
- if (existsSync3(join5(workspaceDir, "openspec")) && !isOpenspecAvailable()) {
1074
+ if (existsSync4(join5(workspaceDir, "openspec")) && !isOpenspecAvailable()) {
1008
1075
  findings.push({
1009
1076
  level: "warn",
1010
1077
  message: "openspec/ is present but the `openspec` CLI was not found on PATH. Install it (https://github.com/Fission-AI/OpenSpec) and run `openspec update`."
@@ -1031,25 +1098,28 @@ async function doctorCommand(workspaceDir, today, opts = {}) {
1031
1098
  }
1032
1099
 
1033
1100
  // src/version.ts
1034
- var VERSION = "0.3.2";
1101
+ var VERSION = "0.3.5";
1035
1102
 
1036
1103
  // src/cli.ts
1037
1104
  function parseArgs(argv) {
1038
1105
  const force = argv.includes("--force");
1039
1106
  const lint = argv.includes("--lint");
1107
+ const ci = argv.indexOf("--config");
1108
+ const configPath = ci >= 0 ? argv[ci + 1] : void 0;
1040
1109
  const first = argv[0];
1041
- if (first === "init") return { command: "init", force, lint };
1042
- if (first === "doctor") return { command: "doctor", force, lint };
1043
- if (first === "--version" || first === "-v") return { command: "version", force, lint };
1044
- return { command: "help", force, lint };
1110
+ if (first === "init") return { command: "init", force, lint, configPath };
1111
+ if (first === "doctor") return { command: "doctor", force, lint, configPath };
1112
+ if (first === "--version" || first === "-v") return { command: "version", force, lint, configPath };
1113
+ return { command: "help", force, lint, configPath };
1045
1114
  }
1046
1115
  var HELP = `agentspace \u2014 scaffold an agent-native multi-repo workspace
1047
1116
 
1048
1117
  Usage:
1049
- agentspace init [--force] Interactively scaffold a workspace in the current dir
1050
- agentspace doctor Run mechanical health checks on a workspace
1051
- agentspace --version Print version
1052
- agentspace --help Show this help
1118
+ agentspace init [--force] Interactively scaffold a workspace in the current dir
1119
+ agentspace init --config <file> Scaffold non-interactively from a JSON config
1120
+ agentspace doctor [--lint] Run mechanical health checks (--lint = JSON output)
1121
+ agentspace --version Print version
1122
+ agentspace --help Show this help
1053
1123
  `;
1054
1124
  function todayIso() {
1055
1125
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -1058,7 +1128,7 @@ async function main(argv) {
1058
1128
  const args = parseArgs(argv);
1059
1129
  switch (args.command) {
1060
1130
  case "init":
1061
- await initCommand({ force: args.force, today: todayIso() });
1131
+ await initCommand({ force: args.force, today: todayIso(), configPath: args.configPath });
1062
1132
  return 0;
1063
1133
  case "doctor":
1064
1134
  return doctorCommand(process.cwd(), todayIso(), { lint: args.lint });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shawaze/agentspace",
3
3
  "publishConfig": { "access": "public" },
4
- "version": "0.3.2",
4
+ "version": "0.3.5",
5
5
  "description": "Scaffold an agent-native multi-repo workspace — keep sibling repos coherent for AI coding agents",
6
6
  "type": "module",
7
7
  "bin": { "agentspace": "dist/cli.js" },
@@ -34,6 +34,7 @@
34
34
  "test": "vitest run",
35
35
  "test:watch": "vitest",
36
36
  "typecheck": "tsc --noEmit",
37
+ "smoke": "node scripts/smoke.mjs",
37
38
  "prepublishOnly": "npm run typecheck && npm run build && npm test"
38
39
  },
39
40
  "dependencies": {