sdtk-design-kit 0.3.0 → 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.
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ const { executeUpdate } = require("../lib/update");
4
+
5
+ async function cmdUpdate(args) {
6
+ return executeUpdate(args);
7
+ }
8
+
9
+ module.exports = {
10
+ cmdUpdate,
11
+ };
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ const { cmdScreens } = require("./commands/screens");
10
10
  const { cmdStart } = require("./commands/start");
11
11
  const { cmdStatus } = require("./commands/status");
12
12
  const { cmdSystem } = require("./commands/system");
13
+ const { cmdUpdate } = require("./commands/update");
13
14
  const { cmdWireframe } = require("./commands/wireframe");
14
15
  const { ValidationError } = require("./lib/errors");
15
16
 
@@ -76,6 +77,9 @@ async function run(argv) {
76
77
  if (command === "status") {
77
78
  return cmdStatus(args);
78
79
  }
80
+ if (command === "update") {
81
+ return cmdUpdate(args);
82
+ }
79
83
  if (DEFERRED_COMMANDS.has(command)) {
80
84
  throw new ValidationError(
81
85
  `Command "${command}" is planned for ${DEFERRED_COMMANDS.get(command)} and is not implemented yet. Run "sdtk-design --help" for the current Foundation Beta surface.`
@@ -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
+ };
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+
3
+ const { spawn } = require("child_process");
4
+ const path = require("path");
5
+ const { parseFlags } = require("./args");
6
+ const { CliError, ValidationError } = require("./errors");
7
+
8
+ const PACKAGE_NAME = "sdtk-design-kit";
9
+ const PRODUCT_NAME = "SDTK-DESIGN";
10
+ const NPM_BIN = process.platform === "win32" ? "npm.cmd" : "npm";
11
+ const NPM_DISPLAY = "npm";
12
+ const NPM_VIEW_ARGS = ["view", PACKAGE_NAME, "version"];
13
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
14
+ const FLAG_DEFS = {
15
+ version: { type: "string" },
16
+ "project-path": { type: "string" },
17
+ "check-only": { type: "boolean" },
18
+ "skip-project-files": { type: "boolean" },
19
+ verbose: { type: "boolean" },
20
+ };
21
+ const pkg = require("../../package.json");
22
+
23
+ let commandExecutor = defaultCommandExecutor;
24
+
25
+ function defaultCommandExecutor(command, args, options = {}) {
26
+ return new Promise((resolve, reject) => {
27
+ const child = spawn(command, args, {
28
+ cwd: options.cwd || process.cwd(),
29
+ env: options.env || process.env,
30
+ shell: options.shell || false,
31
+ windowsHide: true,
32
+ });
33
+ let stdout = "";
34
+ let stderr = "";
35
+
36
+ child.stdout.on("data", (chunk) => {
37
+ const text = chunk.toString();
38
+ stdout += text;
39
+ if (options.verbose) process.stdout.write(text);
40
+ });
41
+ child.stderr.on("data", (chunk) => {
42
+ const text = chunk.toString();
43
+ stderr += text;
44
+ if (options.verbose) process.stderr.write(text);
45
+ });
46
+ child.on("error", (error) => {
47
+ if (error && error.code === "ENOENT") {
48
+ reject(new CliError(`Required command not found in PATH: ${command}`));
49
+ return;
50
+ }
51
+ reject(error);
52
+ });
53
+ child.on("close", (exitCode) => {
54
+ resolve({ exitCode: typeof exitCode === "number" ? exitCode : 1, stdout, stderr });
55
+ });
56
+ });
57
+ }
58
+
59
+ function setCommandExecutorForTests(executor) {
60
+ commandExecutor = executor || defaultCommandExecutor;
61
+ }
62
+
63
+ function resetCommandExecutorForTests() {
64
+ commandExecutor = defaultCommandExecutor;
65
+ }
66
+
67
+ function quote(value) {
68
+ const text = String(value);
69
+ return /[\s"]/u.test(text) ? JSON.stringify(text) : text;
70
+ }
71
+
72
+ function formatCommand(command, args) {
73
+ return [command, ...args].map((value) => quote(value)).join(" ");
74
+ }
75
+
76
+ function validateVersion(targetVersion) {
77
+ if (targetVersion !== "latest" && !VERSION_PATTERN.test(targetVersion)) {
78
+ throw new ValidationError(`Invalid value for --version: "${targetVersion}". Must be "latest" or x.y.z.`);
79
+ }
80
+ }
81
+
82
+ function extractResolvedVersion(stdout) {
83
+ const lines = String(stdout || "")
84
+ .split(/\r?\n/u)
85
+ .map((line) => line.trim())
86
+ .filter(Boolean);
87
+ const candidate = (lines[lines.length - 1] || "").replace(/^['"]|['"]$/gu, "");
88
+ if (!VERSION_PATTERN.test(candidate)) {
89
+ throw new CliError(`npm registry lookup returned an invalid version for ${PACKAGE_NAME}: "${candidate || "<empty>"}"`);
90
+ }
91
+ return candidate;
92
+ }
93
+
94
+ async function resolveTargetVersion(options) {
95
+ if (options.requestedVersion !== "latest") {
96
+ return options.requestedVersion;
97
+ }
98
+
99
+ let result;
100
+ try {
101
+ result = await commandExecutor(NPM_BIN, NPM_VIEW_ARGS, {
102
+ verbose: options.verbose,
103
+ shell: process.platform === "win32",
104
+ });
105
+ } catch (error) {
106
+ throw new CliError(`npm registry lookup failed for ${PACKAGE_NAME} while resolving --version latest.\n${error.message}`, error.exitCode || 4);
107
+ }
108
+
109
+ if (result.exitCode !== 0) {
110
+ const detail = (result.stderr || result.stdout || "").trim();
111
+ throw new CliError(`npm registry lookup failed for ${PACKAGE_NAME} while resolving --version latest (exit code ${result.exitCode}).${detail ? `\n${detail}` : ""}`);
112
+ }
113
+
114
+ try {
115
+ return extractResolvedVersion(result.stdout);
116
+ } catch (error) {
117
+ throw new CliError(`npm registry lookup failed for ${PACKAGE_NAME} while resolving --version latest.\n${error.message}`, error.exitCode || 4);
118
+ }
119
+ }
120
+
121
+ function buildPlan(options) {
122
+ const npmArgs = ["install", "-g", `${PACKAGE_NAME}@${options.targetVersion}`];
123
+ return {
124
+ installedVersion: pkg.version,
125
+ requestedVersion: options.requestedVersion,
126
+ targetVersion: options.targetVersion,
127
+ updateNeeded: pkg.version !== options.targetVersion,
128
+ npmArgs,
129
+ npmCommand: formatCommand(NPM_DISPLAY, npmArgs),
130
+ projectPath: options.projectPath,
131
+ checkOnly: options.checkOnly,
132
+ skipProjectFiles: options.skipProjectFiles,
133
+ projectRefreshCommand: options.skipProjectFiles
134
+ ? "skipped (--skip-project-files)"
135
+ : "skipped (R1 package-only update; project files are never mutated)",
136
+ runtimeRefreshCommand: "skipped (no runtime asset update in R1)",
137
+ };
138
+ }
139
+
140
+ function parseUpdateOptions(args) {
141
+ const parsed = parseFlags(args || [], FLAG_DEFS);
142
+ const flags = parsed.flags || {};
143
+ const positional = parsed.positional || parsed.positionals || [];
144
+
145
+ if (positional.length > 0) {
146
+ throw new ValidationError(`Unexpected arguments: ${positional.join(" ")}`);
147
+ }
148
+
149
+ const requestedVersion = flags.version || "latest";
150
+ validateVersion(requestedVersion);
151
+
152
+ return {
153
+ requestedVersion,
154
+ projectPath: path.resolve(flags["project-path"] || process.cwd()),
155
+ checkOnly: Boolean(flags["check-only"]),
156
+ skipProjectFiles: Boolean(flags["skip-project-files"]),
157
+ verbose: Boolean(flags.verbose),
158
+ };
159
+ }
160
+
161
+ function printPlan(plan) {
162
+ console.log(`${PRODUCT_NAME} update plan`);
163
+ console.log(` Installed package version: ${plan.installedVersion}`);
164
+ console.log(` Requested package version: ${plan.requestedVersion}`);
165
+ console.log(` Target package version: ${plan.targetVersion}`);
166
+ console.log(` Package update needed: ${plan.updateNeeded ? "yes" : `no (already installed: ${plan.installedVersion})`}`);
167
+ console.log(` Package refresh command: ${plan.npmCommand}`);
168
+ console.log(` Project path: ${plan.projectPath}`);
169
+ console.log(` Project file refresh: ${plan.projectRefreshCommand}`);
170
+ console.log(` Runtime asset refresh: ${plan.runtimeRefreshCommand}`);
171
+ console.log(` Mode: ${plan.checkOnly ? "check-only (no changes applied)" : "apply"}`);
172
+ }
173
+
174
+ async function runCommand(label, command, args, options) {
175
+ const result = await commandExecutor(command, args, options);
176
+ if (result.exitCode !== 0) {
177
+ const detail = (result.stderr || result.stdout || "").trim();
178
+ throw new CliError(`${label} failed (exit code ${result.exitCode}).${detail ? `\n${detail}` : ""}`);
179
+ }
180
+ return result;
181
+ }
182
+
183
+ async function applyPlan(plan, options) {
184
+ console.log("");
185
+ console.log(`Applying ${PRODUCT_NAME} update...`);
186
+ console.log(` npm refresh: ${plan.npmCommand}`);
187
+ await runCommand("npm package refresh", NPM_BIN, plan.npmArgs, {
188
+ verbose: options.verbose,
189
+ shell: process.platform === "win32",
190
+ });
191
+ console.log(` project refresh: ${plan.projectRefreshCommand}`);
192
+ console.log(` runtime refresh: ${plan.runtimeRefreshCommand}`);
193
+ console.log("");
194
+ console.log(`${PRODUCT_NAME} update completed successfully.`);
195
+ }
196
+
197
+ async function executeUpdate(args) {
198
+ const options = parseUpdateOptions(args);
199
+ const targetVersion = await resolveTargetVersion(options);
200
+ const plan = buildPlan({ ...options, targetVersion });
201
+ printPlan(plan);
202
+
203
+ if (options.checkOnly) {
204
+ return 0;
205
+ }
206
+
207
+ await applyPlan(plan, options);
208
+ return 0;
209
+ }
210
+
211
+ module.exports = {
212
+ buildPlan,
213
+ executeUpdate,
214
+ formatCommand,
215
+ parseUpdateOptions,
216
+ resetCommandExecutorForTests,
217
+ resolveTargetVersion,
218
+ setCommandExecutorForTests,
219
+ };