@shawaze/agentspace 0.3.3 → 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 +87 -21
  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) {
@@ -877,6 +875,71 @@ async function runWizard() {
877
875
  });
878
876
  }
879
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
+
880
943
  // src/commands/init.ts
881
944
  function generateWorkspace(config, today) {
882
945
  const ctx = buildContext(config, today);
@@ -899,8 +962,8 @@ async function runInit(config, targetDir, opts) {
899
962
  await writeTree(files, targetDir, { force: opts.force });
900
963
  }
901
964
  async function initCommand(opts) {
902
- const config = await runWizard();
903
- 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 });
904
967
  console.log(`
905
968
  \u2713 Created ${config.workspaceName} (${config.pillars.join(", ")}).`);
906
969
  console.log(" Next: ./clone-repos.sh");
@@ -913,7 +976,7 @@ async function initCommand(opts) {
913
976
 
914
977
  // src/commands/doctor.ts
915
978
  import { readFile, readdir as readdir2, stat } from "fs/promises";
916
- import { existsSync as existsSync3 } from "fs";
979
+ import { existsSync as existsSync4 } from "fs";
917
980
  import { join as join5, relative } from "path";
918
981
  import { parse as parse2 } from "yaml";
919
982
 
@@ -1008,7 +1071,7 @@ async function runChecks(workspaceDir, today, deps = {}) {
1008
1071
  }
1009
1072
  }
1010
1073
  }
1011
- if (existsSync3(join5(workspaceDir, "openspec")) && !isOpenspecAvailable()) {
1074
+ if (existsSync4(join5(workspaceDir, "openspec")) && !isOpenspecAvailable()) {
1012
1075
  findings.push({
1013
1076
  level: "warn",
1014
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`."
@@ -1035,25 +1098,28 @@ async function doctorCommand(workspaceDir, today, opts = {}) {
1035
1098
  }
1036
1099
 
1037
1100
  // src/version.ts
1038
- var VERSION = "0.3.3";
1101
+ var VERSION = "0.3.5";
1039
1102
 
1040
1103
  // src/cli.ts
1041
1104
  function parseArgs(argv) {
1042
1105
  const force = argv.includes("--force");
1043
1106
  const lint = argv.includes("--lint");
1107
+ const ci = argv.indexOf("--config");
1108
+ const configPath = ci >= 0 ? argv[ci + 1] : void 0;
1044
1109
  const first = argv[0];
1045
- if (first === "init") return { command: "init", force, lint };
1046
- if (first === "doctor") return { command: "doctor", force, lint };
1047
- if (first === "--version" || first === "-v") return { command: "version", force, lint };
1048
- 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 };
1049
1114
  }
1050
1115
  var HELP = `agentspace \u2014 scaffold an agent-native multi-repo workspace
1051
1116
 
1052
1117
  Usage:
1053
- agentspace init [--force] Interactively scaffold a workspace in the current dir
1054
- agentspace doctor Run mechanical health checks on a workspace
1055
- agentspace --version Print version
1056
- 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
1057
1123
  `;
1058
1124
  function todayIso() {
1059
1125
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -1062,7 +1128,7 @@ async function main(argv) {
1062
1128
  const args = parseArgs(argv);
1063
1129
  switch (args.command) {
1064
1130
  case "init":
1065
- await initCommand({ force: args.force, today: todayIso() });
1131
+ await initCommand({ force: args.force, today: todayIso(), configPath: args.configPath });
1066
1132
  return 0;
1067
1133
  case "doctor":
1068
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.3",
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": {