designsource 0.2.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/dist/index.js +524 -0
  4. package/package.json +47 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Karo Muradyan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ <p align="center">
2
+ <img src="https://designsource.app/designsource/cover.png" alt="designsource — your design system, in your code" width="100%">
3
+ </p>
4
+
5
+ # designsource
6
+
7
+ The command line for a [DS source](https://designsource.app) design system. Two things it does:
8
+
9
+ - **`integrate`** — add the design system to the app you already have: a `design-system/` folder with your tokens as CSS variables, a prompt that tells your coding agent how to wire them in, and the component kit as reference. Vue, Svelte, Angular or plain CSS.
10
+ - **`create`** — start a new Next.js project from it: tokens, all 28 components and your mockup page, running after `pnpm install && pnpm dev`.
11
+
12
+ ```sh
13
+ npx designsource integrate
14
+ ```
15
+
16
+ ```sh
17
+ npx designsource create my-app
18
+ ```
19
+
20
+ Works the same with `pnpm dlx designsource …`, `yarn dlx designsource …` and `bunx designsource …`. Installed globally (`npm i -g designsource`) it is also available as **`dss`**.
21
+
22
+ <p align="center">
23
+ <img src="https://designsource.app/designsource/demo.gif" alt="Terminal demo: pick what to do with the arrow keys, paste the project token, files are written" width="100%">
24
+ </p>
25
+
26
+ ## How it works
27
+
28
+ 1. Design your system at [designsource.app](https://designsource.app) — colours, radii, shadows, typography, three tiers of tokens, a component kit and an AI-generated mockup page.
29
+ 2. Run the command. It asks with the arrow keys where there is a list to pick from, asks for a folder, and asks for your **project token** (workbench → Project settings). The token is typed hidden, sent once as a bearer header, and never written to disk or printed.
30
+ 3. Your app has the system in it: every colour, radius and shadow on screen is a token from your design system.
31
+
32
+ ## `integrate` — add to an existing app
33
+
34
+ ```sh
35
+ npx designsource integrate [folder] [--framework vue|svelte|angular|css] [--token <project token>] [--api <origin>] [--force]
36
+ ```
37
+
38
+ Writes `design-system/` into the folder (default: the current one). Refuses when that folder already has one, unless `--force`.
39
+
40
+ | File | What it is |
41
+ | --- | --- |
42
+ | `tokens.css` | Every token as plain CSS custom properties, light and dark (`[data-theme="dark"]` plus a guarded `prefers-color-scheme` block). |
43
+ | `tokens.json` | The canonical export, with references. |
44
+ | `page.json` | Your mockup page as a layout spec, when the project has one. |
45
+ | `PROMPT.md` | Paste into your coding agent: where to import `tokens.css` for your framework, how to wire an existing theme toggle, how to port the components idiomatically, and how to migrate the styles you already have. |
46
+ | `reference/` | The component kit, renderer and page schema as annotated React source ("port, do not run") plus `components.md`, an index of every variable each component reads. |
47
+
48
+ ## `create` — a new Next.js project
49
+
50
+ ```sh
51
+ npx designsource create [folder] [--token <project token>] [--api <origin>] [--force]
52
+ ```
53
+
54
+ Writes a runnable Next.js app into a new or empty folder (default `my-design-system`): Tailwind v4 with the shadcn theme CSS built from your tokens, 28 UI components under `components/ui`, your mockup page on `/`, a component gallery on `/components`, light and dark via `next-themes`. Next steps print in the dialect of the runner you used (`pnpm`, `npm`, `yarn` or `bun`); a pnpm run pins `packageManager`.
55
+
56
+ <p align="center">
57
+ <img src="https://designsource.app/designsource/starter-dark-home.png" alt="Generated Next.js starter, home page, dark" width="49%">
58
+ <img src="https://designsource.app/designsource/starter-light-home.png" alt="Generated Next.js starter, home page, light" width="49%">
59
+ </p>
60
+ <p align="center">
61
+ <img src="https://designsource.app/designsource/starter-dark-components.png" alt="Component gallery, dark" width="49%">
62
+ <img src="https://designsource.app/designsource/starter-light-components.png" alt="Component gallery, light" width="49%">
63
+ </p>
64
+
65
+ ## Options
66
+
67
+ | Flag | What it does |
68
+ | --- | --- |
69
+ | `folder` | Target folder. `integrate` defaults to `.`, `create` to `my-design-system`. |
70
+ | `--framework` | `integrate` only. Skip the picker: `vue`, `svelte`, `angular` or `css`. |
71
+ | `--token` | The project token, or set `DS_SYNC_TOKEN` for scripts. Asked hidden when neither is given. |
72
+ | `--api` | API origin. Default `https://designsource.app`; point it at a self-hosted instance. |
73
+ | `--force` | Overwrite files that already exist. Without it the command refuses rather than touching them. |
74
+
75
+ Outside a terminal (CI, pipes) the pickers become numbered prompts, and `designsource` with no command prints usage instead of the menu. Ctrl+C or Esc in a picker exits 130 with nothing written.
76
+
77
+ ## Safety
78
+
79
+ - Every write is contained to the target folder. On any failure the command removes what it created, restores what it overwrote, and says so in one line.
80
+ - The project token authorises read-only access to one project. Regenerate it in Project settings to revoke it; the command never stores it.
81
+ - Zero runtime dependencies. Node 20 or newer.
82
+
83
+ ## Local development
84
+
85
+ From the [design-system](https://github.com/KaroMourad/design-system) repo, against a dev server:
86
+
87
+ ```sh
88
+ pnpm --filter designsource build
89
+ pnpm --filter designsource start -- integrate --framework vue --api http://localhost:3001
90
+ pnpm --filter designsource start -- create my-app --api http://localhost:3001
91
+ ```
92
+
93
+ `pnpm --filter designsource test` builds and runs the unit tests plus an end-to-end run of the binary against a throwaway local HTTP server, asserting the token never reaches stdout or stderr.
94
+
95
+ ## Publishing
96
+
97
+ Manual, by the maintainer, from this folder. Nothing in the repo publishes.
98
+
99
+ 1. `npm login` (npm requires 2FA to publish).
100
+ 2. `pnpm version patch|minor` — every publish needs a bump.
101
+ 3. `pnpm publish --dry-run --access public` and check the tarball is `dist/index.js`, `LICENSE`, `README.md`, `package.json`. `prepublishOnly` rebuilds `dist/` first, so a stale or missing build cannot ship.
102
+ 4. `pnpm publish --access public`.
103
+ 5. Verify from an empty folder: `npx designsource@latest --help`.
104
+
105
+ The README's images live in the web app at `apps/web/public/designsource/` and are served from `designsource.app`; ship image changes there before the npm publish that references them. Their sources are in `assets/` here (`cover.html`, `make-gif.mjs`).
106
+
107
+ `create-ds-app` was this package's first name (0.1.x). It is deprecated on npm, pointing here: `npm deprecate create-ds-app@"*" "Moved to designsource: npx designsource create my-app"`.
108
+
109
+ ## License
110
+
111
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,524 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/args.ts
4
+ var FRAMEWORKS = ["next", "vue", "svelte", "angular", "css"];
5
+ var INTEGRATE_FRAMEWORKS = ["vue", "svelte", "angular", "css"];
6
+ var DEFAULT_API_ORIGIN = "https://designsource.app";
7
+ var CREATE_DEFAULT_FOLDER = "my-design-system";
8
+ var INTEGRATE_DEFAULT_FOLDER = ".";
9
+ var USAGE = `designsource <command> [options] (also installed as: dss)
10
+
11
+ Commands
12
+ integrate [folder] add the design system to an existing app \u2014 writes design-system/ (default folder: .)
13
+ create [folder] new Next.js project with the tokens, every component and your mockup page (default folder: ${CREATE_DEFAULT_FOLDER})
14
+
15
+ Run a command with --help for its options. In a terminal, no command shows a menu.
16
+ `;
17
+ var TOKEN_LINE = " --token the project token (workbench \u2192 Project settings); or DS_SYNC_TOKEN";
18
+ var API_LINE = ` --api API origin (default ${DEFAULT_API_ORIGIN})`;
19
+ var CREATE_USAGE = `designsource create [folder] [--token <project token>] [--api <origin>] [--force]
20
+
21
+ Writes a runnable Next.js project from your design system. Asks for anything not given.
22
+ folder target folder (default ${CREATE_DEFAULT_FOLDER}); must be new or empty
23
+ ${TOKEN_LINE}
24
+ ${API_LINE}
25
+ --force overwrite files the starter writes in a non-empty folder
26
+ `;
27
+ var INTEGRATE_USAGE = `designsource integrate [folder] [--framework vue|svelte|angular|css] [--token <project token>] [--api <origin>] [--force]
28
+
29
+ Adds a design-system/ folder to an existing app: tokens, a prompt for your coding agent, the component kit as reference. Asks for anything not given.
30
+ folder the app's folder (default ${INTEGRATE_DEFAULT_FOLDER}); must not already contain design-system/
31
+ --framework picked with the arrow keys when omitted
32
+ ${TOKEN_LINE}
33
+ ${API_LINE}
34
+ --force overwrite files the bundle writes when design-system/ already exists
35
+ `;
36
+ function usageFor(verb) {
37
+ return verb === "create" ? CREATE_USAGE : verb === "integrate" ? INTEGRATE_USAGE : USAGE;
38
+ }
39
+ var ArgsError = class extends Error {
40
+ constructor(message, usage) {
41
+ super(message);
42
+ this.usage = usage;
43
+ }
44
+ };
45
+ function baseArgs(env) {
46
+ const args = { api: DEFAULT_API_ORIGIN, force: false };
47
+ if (env.DS_SYNC_TOKEN) args.token = env.DS_SYNC_TOKEN;
48
+ return args;
49
+ }
50
+ var HELP = /* @__PURE__ */ new Set(["--help", "-h", "help"]);
51
+ function parseArgs(argv, env) {
52
+ const [first, ...rest] = argv;
53
+ if (first === void 0) return { kind: "menu" };
54
+ if (HELP.has(first)) return { kind: "help", verb: void 0 };
55
+ if (first === "create" || first === "integrate") return parseVerb(first, rest, env);
56
+ if (first.startsWith("-")) throw new ArgsError("Missing command. Use integrate or create.", USAGE);
57
+ throw new ArgsError(`Unknown command "${first}". Use integrate or create.`, USAGE);
58
+ }
59
+ function parseVerb(verb, argv, env) {
60
+ const usage = usageFor(verb);
61
+ const args = baseArgs(env);
62
+ for (let i = 0; i < argv.length; i++) {
63
+ const arg = argv[i];
64
+ if (arg === "--help" || arg === "-h") return { kind: "help", verb };
65
+ if (arg === "--force") {
66
+ args.force = true;
67
+ continue;
68
+ }
69
+ if (arg.startsWith("--")) {
70
+ const eq = arg.indexOf("=");
71
+ const key = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
72
+ const value = eq === -1 ? argv[++i] : arg.slice(eq + 1);
73
+ const needs = () => {
74
+ if (value === void 0) throw new ArgsError(`Option --${key} needs a value`, usage);
75
+ };
76
+ if (key === "framework" && verb === "integrate") {
77
+ needs();
78
+ if (!INTEGRATE_FRAMEWORKS.includes(value)) {
79
+ throw new ArgsError(`Unknown framework "${value}" \u2014 one of ${INTEGRATE_FRAMEWORKS.join(", ")}`, usage);
80
+ }
81
+ args.framework = value;
82
+ } else if (key === "token") {
83
+ needs();
84
+ args.token = value;
85
+ } else if (key === "api") {
86
+ needs();
87
+ const origin = value.replace(/\/+$/, "");
88
+ let parsed;
89
+ try {
90
+ parsed = new URL(origin);
91
+ } catch {
92
+ }
93
+ if (!parsed || !parsed.host) {
94
+ throw new ArgsError(`Option --api needs a full origin such as http://localhost:3001 (got "${value}")`, usage);
95
+ }
96
+ args.api = origin;
97
+ } else throw new ArgsError(`Unknown option --${key}`, usage);
98
+ continue;
99
+ }
100
+ if (arg.startsWith("-")) throw new ArgsError(`Unknown option ${arg}`, usage);
101
+ if (args.folder !== void 0) throw new ArgsError("Expected one folder argument", usage);
102
+ args.folder = arg;
103
+ }
104
+ if (verb === "create") {
105
+ const { framework: _drop, ...common } = args;
106
+ return { kind: "create", args: common };
107
+ }
108
+ return { kind: "integrate", args };
109
+ }
110
+
111
+ // src/errors.ts
112
+ var CliError = class extends Error {
113
+ constructor(message, exitCode = 1) {
114
+ super(message);
115
+ this.exitCode = exitCode;
116
+ }
117
+ };
118
+ var CANCELLED = "Cancelled.";
119
+ var TOKEN_MESSAGE = "Project token not recognised. In the workbench open Project settings and copy the project token.";
120
+ var UNREACHABLE = (api) => `Could not reach ${api}.`;
121
+ var UPDATE_MESSAGE = "This design system needs a newer designsource CLI. Update it (for example `npx designsource@latest`) and run again.";
122
+ function messageForResponse(status, body, api) {
123
+ if (status === 401) return TOKEN_MESSAGE;
124
+ const serverMessage = typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
125
+ if (status === 400) return serverMessage ?? `${api} rejected the request (400).`;
126
+ return `${api} answered ${status}.`;
127
+ }
128
+
129
+ // src/manifest.ts
130
+ var malformed = () => new CliError("Unexpected response from the API \u2014 not a starter manifest.");
131
+ function validateManifest(json) {
132
+ if (typeof json !== "object" || json === null) throw malformed();
133
+ const m = json;
134
+ if (typeof m.version !== "number") throw malformed();
135
+ if (m.version !== 1) throw new CliError(UPDATE_MESSAGE);
136
+ if (typeof m.framework !== "string" || !FRAMEWORKS.includes(m.framework)) throw malformed();
137
+ if (typeof m.name !== "string" || !Array.isArray(m.files) || !Array.isArray(m.nextSteps)) throw malformed();
138
+ for (const f of m.files) {
139
+ if (typeof f !== "object" || f === null) throw malformed();
140
+ const { path, contents } = f;
141
+ if (typeof path !== "string" || typeof contents !== "string") throw malformed();
142
+ }
143
+ if (!m.nextSteps.every((s) => typeof s === "string")) throw malformed();
144
+ return { version: 1, framework: m.framework, name: m.name, files: m.files, nextSteps: m.nextSteps };
145
+ }
146
+
147
+ // src/fetch-manifest.ts
148
+ async function fetchManifest(opts, fetchImpl = fetch) {
149
+ const url = new URL("/api/starter", `${opts.api}/`);
150
+ url.searchParams.set("framework", opts.framework);
151
+ if (opts.name) url.searchParams.set("name", opts.name);
152
+ let res;
153
+ try {
154
+ res = await fetchImpl(url.toString(), { headers: { Authorization: `Bearer ${opts.token}`, Accept: "application/json" } });
155
+ } catch {
156
+ throw new CliError(UNREACHABLE(opts.api));
157
+ }
158
+ const body = await res.json().catch(() => null);
159
+ if (!res.ok) throw new CliError(messageForResponse(res.status, body, opts.api));
160
+ return validateManifest(body);
161
+ }
162
+
163
+ // src/package-name.ts
164
+ function packageNameFor(folder) {
165
+ const trimmed = folder.replace(/[\\/]+$/, "");
166
+ const segments = trimmed.split(/[\\/]/).filter((p) => p !== "" && p !== ".");
167
+ const last = segments[segments.length - 1];
168
+ if (!last) return void 0;
169
+ const slug = last.normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-._]+/, "").replace(/[-._]+$/, "").slice(0, 214).replace(/[-._]+$/, "");
170
+ return slug || void 0;
171
+ }
172
+
173
+ // src/prompts.ts
174
+ import { createInterface } from "node:readline";
175
+ var shared;
176
+ var closed = false;
177
+ var queued = [];
178
+ var waiting = [];
179
+ var EOF_MESSAGE = "No more input. Pass the answer as a flag (for example --token or DS_SYNC_TOKEN) when running without a terminal.";
180
+ function iface() {
181
+ if (shared) return shared;
182
+ const r = createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY === true });
183
+ r.on("line", (line) => {
184
+ const next = waiting.shift();
185
+ if (next) next.resolve(line);
186
+ else queued.push(line);
187
+ });
188
+ r.on("close", () => {
189
+ closed = true;
190
+ while (waiting.length) waiting.shift().reject(new CliError(EOF_MESSAGE));
191
+ });
192
+ if (process.stdin.isTTY === true) {
193
+ r.on("SIGINT", () => {
194
+ process.stdout.write("\n");
195
+ closed = true;
196
+ while (waiting.length) waiting.shift().reject(new CliError(CANCELLED, 130));
197
+ r.close();
198
+ });
199
+ }
200
+ shared = r;
201
+ return r;
202
+ }
203
+ function hasInterface() {
204
+ return shared !== void 0;
205
+ }
206
+ function nextLine() {
207
+ iface();
208
+ const line = queued.shift();
209
+ if (line !== void 0) return Promise.resolve(line);
210
+ if (closed) return Promise.reject(new CliError(EOF_MESSAGE));
211
+ process.stdin.ref?.();
212
+ return new Promise((resolve3, reject) => waiting.push({
213
+ resolve: (line2) => {
214
+ process.stdin.unref?.();
215
+ resolve3(line2);
216
+ },
217
+ reject: (err) => {
218
+ process.stdin.unref?.();
219
+ reject(err);
220
+ }
221
+ }));
222
+ }
223
+ function promptLine(promptText, fallback) {
224
+ const r = iface();
225
+ r.setPrompt(promptText);
226
+ r.prompt();
227
+ return nextLine().then((a) => a.trim() || fallback);
228
+ }
229
+ function ask(question, fallback) {
230
+ return promptLine(`${question} (${fallback}) `, fallback);
231
+ }
232
+ async function askChoice(question, options, defaultIndex) {
233
+ const list = options.map((o, i) => ` ${i + 1}) ${o}${i === defaultIndex ? " (default)" : ""}`).join("\n");
234
+ process.stdout.write(`${question}
235
+ ${list}
236
+ `);
237
+ for (; ; ) {
238
+ const answer = await ask("Choice", String(defaultIndex + 1));
239
+ const idx = Number(answer) - 1;
240
+ const byName = options.indexOf(answer);
241
+ if (byName !== -1) return options[byName];
242
+ if (Number.isInteger(idx) && options[idx] !== void 0) return options[idx];
243
+ process.stdout.write(`Pick 1-${options.length}.
244
+ `);
245
+ }
246
+ }
247
+ async function askHidden(question) {
248
+ const r = iface();
249
+ const original = typeof r._writeToOutput === "function" ? r._writeToOutput.bind(r) : void 0;
250
+ if (!original) {
251
+ throw new CliError("Cannot hide input on this terminal. Pass --token or set DS_SYNC_TOKEN instead.");
252
+ }
253
+ let muted = false;
254
+ r._writeToOutput = (s) => {
255
+ if (!muted) original(s);
256
+ };
257
+ r.setPrompt(`${question} `);
258
+ r.prompt();
259
+ muted = true;
260
+ try {
261
+ const a = await nextLine();
262
+ return a.trim();
263
+ } finally {
264
+ muted = false;
265
+ original("\n");
266
+ r._writeToOutput = original;
267
+ }
268
+ }
269
+
270
+ // src/runner.ts
271
+ function detectRunner(userAgent) {
272
+ const match = userAgent?.match(/^(pnpm|npm|yarn|bun)\/(\S+)/);
273
+ if (!match) return { runner: "pnpm" };
274
+ return { runner: match[1], version: match[2] };
275
+ }
276
+ var INSTALL = { pnpm: "pnpm install", npm: "npm install", yarn: "yarn", bun: "bun install" };
277
+ var DEV = { pnpm: "pnpm dev", npm: "npm run dev", yarn: "yarn dev", bun: "bun dev" };
278
+ function nextStepsText({ runner, framework, folder, created }) {
279
+ const lines = ["Next steps:"];
280
+ if (framework === "next") {
281
+ if (created) lines.push(` cd ${folder}`);
282
+ lines.push(` ${INSTALL[runner]}`, ` ${DEV[runner]}`);
283
+ } else if (folder === ".") {
284
+ lines.push(" Open design-system/PROMPT.md and paste it into your coding agent.");
285
+ } else {
286
+ lines.push(` Open ${folder}/design-system/PROMPT.md and paste it into your coding agent, run from ${folder}.`);
287
+ }
288
+ return `${lines.join("\n")}
289
+ `;
290
+ }
291
+
292
+ // src/write.ts
293
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
294
+ import { dirname, resolve as resolve2 } from "node:path";
295
+
296
+ // src/paths.ts
297
+ import { isAbsolute, resolve, sep } from "node:path";
298
+ function resolveInside(root, relPath) {
299
+ const escapes = (why) => new CliError(`Manifest path "${relPath}" is outside the target folder (${why}).`);
300
+ if (!relPath) throw escapes("empty");
301
+ if (isAbsolute(relPath) || /^[a-zA-Z]:[\\/]/.test(relPath) || relPath.startsWith("\\")) throw escapes("absolute");
302
+ if (relPath.split(/[\\/]/).includes("..")) throw escapes("..");
303
+ const abs = resolve(root, relPath);
304
+ if (abs === root || !abs.startsWith(root.endsWith(sep) ? root : root + sep)) throw escapes("resolves above it");
305
+ return abs;
306
+ }
307
+
308
+ // src/write.ts
309
+ function planTarget(cwd, folder, framework, force) {
310
+ const root = resolve2(cwd, folder);
311
+ if (!existsSync(root)) return { root, created: true };
312
+ if (!statSync(root).isDirectory()) throw new CliError(`${folder} exists and is not a folder.`);
313
+ if (framework === "next") {
314
+ if (readdirSync(root).length > 0 && !force) throw new CliError(`Folder ${folder} is not empty. Pick another name, or pass --force to overwrite the files the starter writes.`);
315
+ } else if (existsSync(resolve2(root, "design-system")) && !force) {
316
+ throw new CliError(`${folder}/design-system/ already exists. Remove it, or pass --force to overwrite the files the bundle writes.`);
317
+ }
318
+ return { root, created: false };
319
+ }
320
+ function writeFiles(root, files, opts) {
321
+ const targets = files.map((f) => ({ file: f, abs: resolveInside(root, f.path) }));
322
+ if (!opts.force) {
323
+ const clash = targets.find((t) => existsSync(t.abs));
324
+ if (clash) throw new CliError(`${clash.file.path} already exists. Pass --force to overwrite the files the starter writes.`);
325
+ }
326
+ const written = [];
327
+ const previous = /* @__PURE__ */ new Map();
328
+ let failingPath;
329
+ try {
330
+ for (const { file, abs } of targets) {
331
+ failingPath = file.path;
332
+ if (!previous.has(file.path)) previous.set(file.path, existsSync(abs) ? readFileSync(abs) : void 0);
333
+ mkdirSync(dirname(abs), { recursive: true });
334
+ writeFileSync(abs, file.path === "package.json" && opts.packageManager ? withPackageManager(file.contents, opts.packageManager) : file.contents);
335
+ written.push(file.path);
336
+ }
337
+ } catch (e) {
338
+ const code2 = e?.code ?? e.message;
339
+ if (opts.created) {
340
+ rmSync(root, { recursive: true, force: true });
341
+ throw new CliError(`Could not write ${failingPath} (${code2}). Removed ${root} again; nothing was left behind.`);
342
+ }
343
+ let restored = 0;
344
+ let removed = 0;
345
+ for (const p of written) {
346
+ const abs = resolveInside(root, p);
347
+ const before = previous.get(p);
348
+ if (before === void 0) {
349
+ rmSync(abs, { force: true });
350
+ removed++;
351
+ } else {
352
+ writeFileSync(abs, before);
353
+ restored++;
354
+ }
355
+ }
356
+ throw new CliError(`Could not write ${failingPath} (${code2}). Restored ${restored} file(s) and removed ${removed} new file(s); the folder is as it was.`);
357
+ }
358
+ return { written };
359
+ }
360
+ function withPackageManager(contents, packageManager) {
361
+ const pkg = JSON.parse(contents);
362
+ return `${JSON.stringify({ ...pkg, packageManager }, null, 2)}
363
+ `;
364
+ }
365
+
366
+ // src/commands/scaffold.ts
367
+ function askFolder(fallback) {
368
+ return process.stdin.isTTY === true ? ask("Folder", fallback) : Promise.resolve(fallback);
369
+ }
370
+ async function requireToken(given) {
371
+ const token = given ?? await askHidden("Project token (workbench \u2192 Project settings):");
372
+ if (!token) throw new CliError("A project token is required. In the workbench open Project settings and copy the project token.");
373
+ return token;
374
+ }
375
+ async function scaffold(input, env, io) {
376
+ const { folder, framework, token, api, force } = input;
377
+ const { root, created } = planTarget(io.cwd, folder, framework, force);
378
+ const manifest = await fetchManifest({ api, token, framework, name: packageNameFor(folder) }, io.fetchImpl);
379
+ const { runner, version } = detectRunner(env.npm_config_user_agent);
380
+ const packageManager = runner === "pnpm" && version ? `pnpm@${version}` : void 0;
381
+ const { written } = writeFiles(root, manifest.files, { force, created, packageManager });
382
+ io.stdout(`${created ? "Created" : "Wrote into"} ${folder} (${written.length} files):
383
+ ${written.map((p) => ` ${p}`).join("\n")}
384
+
385
+ `);
386
+ io.stdout(nextStepsText({ runner, framework, folder, created }));
387
+ }
388
+
389
+ // src/commands/create.ts
390
+ async function runCreate(args, env, io) {
391
+ const folder = args.folder ?? await askFolder(CREATE_DEFAULT_FOLDER);
392
+ const token = await requireToken(args.token);
393
+ await scaffold({ folder, framework: "next", token, api: args.api, force: args.force }, env, io);
394
+ }
395
+
396
+ // src/picker.ts
397
+ import { emitKeypressEvents } from "node:readline";
398
+ function pick(question, options, defaultIndex = 0) {
399
+ const stdin = process.stdin;
400
+ const stdout = process.stdout;
401
+ const tty = stdin.isTTY === true && stdout.isTTY === true && typeof stdin.setRawMode === "function" && !hasInterface();
402
+ if (!tty) return askChoice(question, options.map((o) => o.value), defaultIndex);
403
+ return pickTty(question, options, defaultIndex, stdin, stdout);
404
+ }
405
+ function pickTty(question, options, start, stdin, stdout) {
406
+ const color = !process.env.NO_COLOR;
407
+ const bold = (s) => color ? `\x1B[1m${s}\x1B[22m` : s;
408
+ const dim = (s) => color ? `\x1B[2m${s}\x1B[22m` : s;
409
+ const n = options.length;
410
+ let index = start;
411
+ const rows = () => options.map((o, i) => {
412
+ const label = i === index ? bold(o.label) : o.label;
413
+ return `${i === index ? "\u276F" : " "} ${label}${o.hint ? ` ${dim(o.hint)}` : ""}`;
414
+ });
415
+ const draw = (lines) => stdout.write(lines.map((l) => `\r\x1B[2K${l}`).join("\n") + "\n");
416
+ const up = () => stdout.write(`\x1B[${n + 1}A`);
417
+ emitKeypressEvents(stdin);
418
+ stdin.setRawMode(true);
419
+ stdin.resume();
420
+ stdout.write("\x1B[?25l");
421
+ draw([question, ...rows()]);
422
+ return new Promise((resolve3, reject) => {
423
+ const finish = (after) => {
424
+ stdin.off("keypress", onKey);
425
+ stdin.setRawMode(false);
426
+ stdin.pause();
427
+ after();
428
+ stdout.write("\x1B[?25h");
429
+ };
430
+ const onKey = (str, key = {}) => {
431
+ if (key.ctrl && key.name === "c" || key.name === "escape") {
432
+ finish(() => stdout.write("\n"));
433
+ reject(new CliError(CANCELLED, 130));
434
+ return;
435
+ }
436
+ if (key.name === "return" || key.name === "enter") {
437
+ const chosen = options[index];
438
+ finish(() => {
439
+ up();
440
+ stdout.write(`\r\x1B[J${question} ${chosen.label}
441
+ `);
442
+ });
443
+ resolve3(chosen.value);
444
+ return;
445
+ }
446
+ if (key.name === "up" || str === "k") index = (index - 1 + n) % n;
447
+ else if (key.name === "down" || str === "j") index = (index + 1) % n;
448
+ else if (str && /^[1-9]$/.test(str) && Number(str) <= n) index = Number(str) - 1;
449
+ else return;
450
+ up();
451
+ draw([question, ...rows()]);
452
+ };
453
+ stdin.on("keypress", onKey);
454
+ });
455
+ }
456
+
457
+ // src/commands/integrate.ts
458
+ var INTEGRATE_OPTIONS = [
459
+ { value: "vue", label: "Vue", hint: "Vite, Nuxt" },
460
+ { value: "svelte", label: "Svelte", hint: "SvelteKit" },
461
+ { value: "angular", label: "Angular" },
462
+ { value: "css", label: "Plain CSS", hint: "any other stack" }
463
+ ];
464
+ async function runIntegrate(args, env, io) {
465
+ const framework = args.framework ?? await pick("Framework", INTEGRATE_OPTIONS, 0);
466
+ const folder = args.folder ?? await askFolder(INTEGRATE_DEFAULT_FOLDER);
467
+ const token = await requireToken(args.token);
468
+ await scaffold({ folder, framework, token, api: args.api, force: args.force }, env, io);
469
+ }
470
+
471
+ // src/index.ts
472
+ var MENU = [
473
+ { value: "integrate", label: "Add to an existing project", hint: "writes design-system/ \u2014 tokens, prompt, reference kit" },
474
+ { value: "create", label: "Create a new project", hint: "a Next.js app with every component and your mockup page" }
475
+ ];
476
+ async function main(argv, env, io) {
477
+ let command;
478
+ try {
479
+ command = parseArgs(argv, env);
480
+ } catch (e) {
481
+ if (e instanceof ArgsError) {
482
+ io.stderr(`${e.message}
483
+
484
+ ${e.usage}`);
485
+ return 1;
486
+ }
487
+ throw e;
488
+ }
489
+ if (command.kind === "help") {
490
+ io.stdout(usageFor(command.verb));
491
+ return 0;
492
+ }
493
+ try {
494
+ if (command.kind === "menu") {
495
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
496
+ io.stderr(`Missing command. Use integrate or create.
497
+
498
+ ${USAGE}`);
499
+ return 1;
500
+ }
501
+ const verb = await pick("What do you want to do?", MENU, 0);
502
+ command = verb === "create" ? { kind: "create", args: baseArgs(env) } : { kind: "integrate", args: baseArgs(env) };
503
+ }
504
+ if (command.kind === "create") await runCreate(command.args, env, io);
505
+ else await runIntegrate(command.args, env, io);
506
+ return 0;
507
+ } catch (e) {
508
+ if (e instanceof CliError) {
509
+ io.stderr(`${e.message}
510
+ `);
511
+ return e.exitCode;
512
+ }
513
+ throw e;
514
+ }
515
+ }
516
+ var code = await main(process.argv.slice(2), process.env, {
517
+ stdout: (s) => process.stdout.write(s),
518
+ stderr: (s) => process.stderr.write(s),
519
+ cwd: process.cwd()
520
+ });
521
+ process.exitCode = code;
522
+ export {
523
+ main
524
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "designsource",
3
+ "version": "0.2.0",
4
+ "description": "The DS source CLI: add a design system to an existing app (integrate) or start a new Next.js project from it (create) — tokens, every component and your mockup page.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/KaroMourad/design-system.git",
9
+ "directory": "apps/designsource"
10
+ },
11
+ "homepage": "https://designsource.app",
12
+ "keywords": [
13
+ "design-system",
14
+ "design-tokens",
15
+ "cli",
16
+ "scaffold",
17
+ "nextjs",
18
+ "tailwind",
19
+ "shadcn",
20
+ "vue",
21
+ "svelte",
22
+ "angular"
23
+ ],
24
+ "type": "module",
25
+ "bin": {
26
+ "designsource": "dist/index.js",
27
+ "dss": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "scripts": {
36
+ "build": "node esbuild.mjs",
37
+ "prepublishOnly": "node esbuild.mjs",
38
+ "start": "node dist/index.js",
39
+ "test": "node esbuild.mjs && vitest run"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^26",
43
+ "esbuild": "^0.24.0",
44
+ "typescript": "~6.0.3",
45
+ "vitest": "^4.1.10"
46
+ }
47
+ }