sdtk-design-kit 0.3.1 → 0.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdtk-design-kit",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Local-first MVP design planner and reviewer for SDTK workspaces.",
5
5
  "bin": {
6
6
  "sdtk-design": "bin/sdtk-design.js"
@@ -7,41 +7,67 @@ const { ValidationError } = require("../lib/errors");
7
7
  const {
8
8
  assertProjectLocalPath,
9
9
  describeDesignPaths,
10
+ isPathInsideOrEqual,
10
11
  resolveProjectPath,
11
12
  } = require("../lib/design-paths");
13
+ const {
14
+ VALID_RUNTIMES,
15
+ VALID_SCOPES,
16
+ defaultScope,
17
+ resolveSkillsDir,
18
+ } = require("../lib/runtime-skills");
12
19
 
13
20
  const INIT_FLAG_DEFS = {
14
21
  help: { type: "boolean" },
15
22
  "project-path": { type: "string" },
16
23
  force: { type: "boolean" },
17
24
  "no-state": { type: "boolean" },
25
+ runtime: { type: "string" },
26
+ "runtime-scope": { type: "string" },
27
+ global: { type: "boolean" },
18
28
  };
19
29
 
20
30
  const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
21
31
  const DESIGN_PROTOTYPE_SKILL_SOURCE = path.join(PACKAGE_ROOT, "skills", "design-prototype");
22
- const DESIGN_PROTOTYPE_SKILL_TARGETS = [
23
- path.join(".claude", "skills", "design-prototype"),
24
- path.join(".agents", "skills", "design-prototype"),
25
- ];
26
32
 
27
33
  function toPosix(value) {
28
34
  return String(value || "").replace(/\\/g, "/");
29
35
  }
30
36
 
37
+ // Report a target either project-relative (when inside the project) or as an
38
+ // absolute posix path (e.g. for user/global skills under the home directory).
39
+ function reportPath(targetPath, projectPath) {
40
+ if (isPathInsideOrEqual(targetPath, projectPath)) {
41
+ return toPosix(path.relative(projectPath, targetPath));
42
+ }
43
+ return toPosix(targetPath);
44
+ }
45
+
31
46
  function cmdInitHelp() {
32
47
  console.log(`SDTK-DESIGN Init
33
48
 
34
49
  Usage:
35
- sdtk-design init [--project-path <path>] [--force] [--no-state]
50
+ sdtk-design init [--runtime <claude|codex>] [--runtime-scope <project|user>]
51
+ [--global] [--project-path <path>] [--force] [--no-state]
36
52
 
37
53
  Example:
38
54
  sdtk-design init
55
+ sdtk-design init --runtime codex
56
+ sdtk-design init --runtime claude --global
57
+
58
+ Runtime:
59
+ --runtime claude Install the design-prototype skill under .claude/skills (default).
60
+ --runtime codex Install it under .codex/skills (launch Codex with CODEX_HOME).
61
+ --runtime-scope project Project-local skills dir (default for claude).
62
+ --runtime-scope user User/global skills dir (default for codex).
63
+ --global Shorthand for --runtime-scope user (~/.claude or ~/.codex).
39
64
 
40
65
  Creates:
41
66
  docs/design/
42
67
  docs/design/wireframes/
43
68
  docs/design/reviews/
44
69
  docs/design/README.md
70
+ <runtime skills dir>/design-prototype/ (e.g. .claude/skills or .codex/skills)
45
71
  .sdtk/design/manifest.json unless --no-state is used
46
72
 
47
73
  Safety:
@@ -139,41 +165,70 @@ function listSkillFiles(sourceDir) {
139
165
  return results.sort();
140
166
  }
141
167
 
142
- function installDesignPrototypeSkill(projectPath, force, result) {
168
+ // Install the bundled design-prototype skill into a single runtime skills dir
169
+ // (.claude/skills or .codex/skills, per the resolved runtime/scope). For a
170
+ // project-local target the path is guarded inside the project root; for a
171
+ // user/global target (home directory) traversal is guarded against the skills
172
+ // target dir instead, since the destination is intentionally outside the project.
173
+ function installDesignPrototypeSkill(skillsDir, projectPath, force, result) {
143
174
  if (!fs.existsSync(DESIGN_PROTOTYPE_SKILL_SOURCE) || !fs.statSync(DESIGN_PROTOTYPE_SKILL_SOURCE).isDirectory()) {
144
175
  throw new ValidationError(`Bundled design-prototype skill is missing: ${DESIGN_PROTOTYPE_SKILL_SOURCE}. No project files were changed.`);
145
176
  }
146
177
 
147
- for (const relativeTargetDir of DESIGN_PROTOTYPE_SKILL_TARGETS) {
148
- const targetDir = path.join(projectPath, relativeTargetDir);
178
+ const targetDir = path.join(skillsDir, "design-prototype");
179
+ if (isPathInsideOrEqual(targetDir, projectPath)) {
149
180
  assertProjectLocalPath(targetDir, projectPath, "skill directory");
150
- fs.mkdirSync(targetDir, { recursive: true });
151
- result.created.push(toPosix(relativeTargetDir) + "/");
152
-
153
- for (const sourceFile of listSkillFiles(DESIGN_PROTOTYPE_SKILL_SOURCE)) {
154
- const relativeFile = path.relative(DESIGN_PROTOTYPE_SKILL_SOURCE, sourceFile);
155
- const targetFile = path.join(targetDir, relativeFile);
156
- assertProjectLocalPath(targetFile, projectPath, "skill file");
157
- if (fs.existsSync(targetFile) && !force) {
158
- result.skipped.push(toPosix(path.relative(projectPath, targetFile)));
159
- continue;
160
- }
161
- fs.mkdirSync(path.dirname(targetFile), { recursive: true });
162
- fs.copyFileSync(sourceFile, targetFile);
163
- result.created.push(toPosix(path.relative(projectPath, targetFile)));
181
+ }
182
+ fs.mkdirSync(targetDir, { recursive: true });
183
+ result.created.push(reportPath(targetDir, projectPath) + "/");
184
+
185
+ for (const sourceFile of listSkillFiles(DESIGN_PROTOTYPE_SKILL_SOURCE)) {
186
+ const relativeFile = path.relative(DESIGN_PROTOTYPE_SKILL_SOURCE, sourceFile);
187
+ const targetFile = path.join(targetDir, relativeFile);
188
+ if (!isPathInsideOrEqual(targetFile, targetDir)) {
189
+ throw new ValidationError(`Refusing to write skill file outside its target directory: ${targetFile}`);
190
+ }
191
+ if (fs.existsSync(targetFile) && !force) {
192
+ result.skipped.push(reportPath(targetFile, projectPath));
193
+ continue;
164
194
  }
195
+ fs.mkdirSync(path.dirname(targetFile), { recursive: true });
196
+ fs.copyFileSync(sourceFile, targetFile);
197
+ result.created.push(reportPath(targetFile, projectPath));
198
+ }
199
+ }
200
+
201
+ function resolveRuntimeAndScope({ runtime, runtimeScope, global: globalFlag }) {
202
+ const resolvedRuntime = runtime || "claude";
203
+ if (!VALID_RUNTIMES.includes(resolvedRuntime)) {
204
+ throw new ValidationError(`--runtime must be one of: ${VALID_RUNTIMES.join(", ")}. Got: ${resolvedRuntime}.`);
205
+ }
206
+ const resolvedScope = globalFlag ? "user" : (runtimeScope || defaultScope(resolvedRuntime));
207
+ if (!VALID_SCOPES.includes(resolvedScope)) {
208
+ throw new ValidationError(`--runtime-scope must be one of: ${VALID_SCOPES.join(", ")}. Got: ${resolvedScope}.`);
165
209
  }
210
+ return { runtime: resolvedRuntime, scope: resolvedScope };
166
211
  }
167
212
 
168
- function runDesignInit({ projectPath, force = false, state = true }) {
213
+ function runDesignInit({ projectPath, force = false, state = true, runtime, runtimeScope, global: globalFlag }) {
169
214
  const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
170
215
  if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
171
216
  throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}. No project files were changed.`);
172
217
  }
173
218
 
219
+ const { runtime: resolvedRuntime, scope: resolvedScope } = resolveRuntimeAndScope({
220
+ runtime,
221
+ runtimeScope,
222
+ global: globalFlag,
223
+ });
224
+ const skillsDir = resolveSkillsDir(resolvedRuntime, resolvedScope, resolvedProjectPath);
225
+
174
226
  const paths = describeDesignPaths(resolvedProjectPath);
175
227
  const result = {
176
228
  projectPath: resolvedProjectPath,
229
+ runtime: resolvedRuntime,
230
+ scope: resolvedScope,
231
+ skillsDir,
177
232
  created: [],
178
233
  skipped: [],
179
234
  stateEnabled: Boolean(state),
@@ -183,7 +238,7 @@ function runDesignInit({ projectPath, force = false, state = true }) {
183
238
  ensureDirectory(paths.wireframesPath, resolvedProjectPath, result);
184
239
  ensureDirectory(paths.reviewsPath, resolvedProjectPath, result);
185
240
  writeManagedFile(paths.designReadmePath, designReadmeContent(), resolvedProjectPath, force, result);
186
- installDesignPrototypeSkill(resolvedProjectPath, force, result);
241
+ installDesignPrototypeSkill(skillsDir, resolvedProjectPath, force, result);
187
242
 
188
243
  if (state) {
189
244
  ensureDirectory(paths.designStatePath, resolvedProjectPath, result);
@@ -201,9 +256,14 @@ function cmdInit(args) {
201
256
  projectPath: flags["project-path"],
202
257
  force: Boolean(flags.force),
203
258
  state: !flags["no-state"],
259
+ runtime: flags.runtime,
260
+ runtimeScope: flags["runtime-scope"],
261
+ global: Boolean(flags.global),
204
262
  });
205
263
 
206
264
  console.log(`[design] Initialized SDTK-DESIGN workspace: ${result.projectPath}`);
265
+ console.log(`[design] Runtime: ${result.runtime} (scope: ${result.scope})`);
266
+ console.log(`[design] Skill: ${path.join(result.skillsDir, "design-prototype")}`);
207
267
  console.log(`[design] Created: ${result.created.length}`);
208
268
  console.log(`[design] Skipped: ${result.skipped.length}`);
209
269
  console.log(`[design] State: ${result.stateEnabled ? ".sdtk/design enabled" : "disabled by --no-state"}`);
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ // Runtime/scope → skills directory resolver for SDTK-DESIGN.
4
+ //
5
+ // Mirrors the convention used by the runtime-aware kits (sdtk-spec/ops/code,
6
+ // see their src/lib/scope.js): claude installs skills under `.claude/skills`,
7
+ // codex under `.codex/skills` (honoring CODEX_HOME for the user/global scope).
8
+ // Each kit is an independent npm package, so this small resolver is duplicated
9
+ // rather than imported across package boundaries.
10
+
11
+ const path = require("path");
12
+ const os = require("os");
13
+
14
+ const VALID_RUNTIMES = ["claude", "codex"];
15
+ const VALID_SCOPES = ["project", "user"];
16
+
17
+ // Claude defaults to project-local; Codex defaults to user/global.
18
+ function defaultScope(runtime) {
19
+ return runtime === "claude" ? "project" : "user";
20
+ }
21
+
22
+ function resolveCodexHome(scope, projectPath) {
23
+ if (scope === "project") {
24
+ return path.join(projectPath || process.cwd(), ".codex");
25
+ }
26
+ return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
27
+ }
28
+
29
+ // Absolute skills directory for the given runtime/scope/project root.
30
+ function resolveSkillsDir(runtime, scope, projectPath) {
31
+ if (runtime === "claude") {
32
+ if (scope === "user") {
33
+ return path.join(os.homedir(), ".claude", "skills");
34
+ }
35
+ return path.join(projectPath || process.cwd(), ".claude", "skills");
36
+ }
37
+ return path.join(resolveCodexHome(scope, projectPath), "skills");
38
+ }
39
+
40
+ module.exports = {
41
+ VALID_RUNTIMES,
42
+ VALID_SCOPES,
43
+ defaultScope,
44
+ resolveCodexHome,
45
+ resolveSkillsDir,
46
+ };