create-dig-app 0.1.1
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/LICENSE +21 -0
- package/README.md +217 -0
- package/bin/create-dig-app.js +17 -0
- package/lib/cli.js +438 -0
- package/lib/index.js +235 -0
- package/lib/nft-cli.js +265 -0
- package/lib/nft-metadata.js +515 -0
- package/lib/substitute.js +47 -0
- package/lib/templates.js +185 -0
- package/package.json +51 -0
- package/templates/dapp-window-chia/README.md +63 -0
- package/templates/dapp-window-chia/_env.example +7 -0
- package/templates/dapp-window-chia/_gitignore +10 -0
- package/templates/dapp-window-chia/dig.toml +17 -0
- package/templates/dapp-window-chia/index.html +12 -0
- package/templates/dapp-window-chia/package.json +24 -0
- package/templates/dapp-window-chia/src/App.jsx +91 -0
- package/templates/dapp-window-chia/src/main.jsx +10 -0
- package/templates/dapp-window-chia/src/styles.css +71 -0
- package/templates/dapp-window-chia/src/wallet.js +47 -0
- package/templates/dapp-window-chia/vite.config.js +14 -0
- package/templates/next-static/README.md +41 -0
- package/templates/next-static/_gitignore +6 -0
- package/templates/next-static/app/globals.css +49 -0
- package/templates/next-static/app/layout.jsx +14 -0
- package/templates/next-static/app/page.jsx +18 -0
- package/templates/next-static/dig.toml +17 -0
- package/templates/next-static/next.config.mjs +11 -0
- package/templates/next-static/package.json +16 -0
- package/templates/nft-collection/README.md +119 -0
- package/templates/nft-collection/_env.example +5 -0
- package/templates/nft-collection/_gitignore +9 -0
- package/templates/nft-collection/assets/banner.svg +11 -0
- package/templates/nft-collection/assets/icon.svg +10 -0
- package/templates/nft-collection/collection.json +14 -0
- package/templates/nft-collection/dig.toml +23 -0
- package/templates/nft-collection/images/sample-1.svg +5 -0
- package/templates/nft-collection/images/sample-2.svg +5 -0
- package/templates/nft-collection/licenses/.gitkeep +1 -0
- package/templates/nft-collection/metadata/.gitkeep +2 -0
- package/templates/nft-collection/package.json +18 -0
- package/templates/nft-collection/scripts/dig-nft.mjs +479 -0
- package/templates/nft-collection/src/mint.js +72 -0
- package/templates/nft-collection/src/styles.css +64 -0
- package/templates/nft-collection/traits.csv +3 -0
- package/templates/nft-drop/README.md +71 -0
- package/templates/nft-drop/_env.example +7 -0
- package/templates/nft-drop/_gitignore +10 -0
- package/templates/nft-drop/dig.toml +18 -0
- package/templates/nft-drop/index.html +12 -0
- package/templates/nft-drop/package.json +24 -0
- package/templates/nft-drop/src/App.jsx +91 -0
- package/templates/nft-drop/src/main.jsx +10 -0
- package/templates/nft-drop/src/mint.js +72 -0
- package/templates/nft-drop/src/styles.css +70 -0
- package/templates/nft-drop/vite.config.js +13 -0
- package/templates/static-site/README.md +41 -0
- package/templates/static-site/_gitignore +5 -0
- package/templates/static-site/dig.toml +19 -0
- package/templates/static-site/package.json +10 -0
- package/templates/static-site/scripts/build.mjs +14 -0
- package/templates/static-site/src/index.html +20 -0
- package/templates/static-site/src/styles.css +49 -0
- package/templates/vite-react/README.md +42 -0
- package/templates/vite-react/_gitignore +5 -0
- package/templates/vite-react/dig.toml +17 -0
- package/templates/vite-react/index.html +12 -0
- package/templates/vite-react/package.json +20 -0
- package/templates/vite-react/src/App.jsx +18 -0
- package/templates/vite-react/src/main.jsx +10 -0
- package/templates/vite-react/src/styles.css +49 -0
- package/templates/vite-react/vite.config.js +9 -0
- package/templates-ts/dapp-window-chia/README.md +67 -0
- package/templates-ts/dapp-window-chia/_env.example +7 -0
- package/templates-ts/dapp-window-chia/_gitignore +10 -0
- package/templates-ts/dapp-window-chia/dig.toml +17 -0
- package/templates-ts/dapp-window-chia/index.html +12 -0
- package/templates-ts/dapp-window-chia/package.json +28 -0
- package/templates-ts/dapp-window-chia/src/App.tsx +93 -0
- package/templates-ts/dapp-window-chia/src/main.tsx +13 -0
- package/templates-ts/dapp-window-chia/src/styles.css +71 -0
- package/templates-ts/dapp-window-chia/src/vite-env.d.ts +11 -0
- package/templates-ts/dapp-window-chia/src/wallet.ts +62 -0
- package/templates-ts/dapp-window-chia/tsconfig.json +20 -0
- package/templates-ts/dapp-window-chia/vite.config.ts +14 -0
- package/templates-ts/next-static/README.md +43 -0
- package/templates-ts/next-static/_gitignore +7 -0
- package/templates-ts/next-static/app/globals.css +49 -0
- package/templates-ts/next-static/app/layout.tsx +16 -0
- package/templates-ts/next-static/app/page.tsx +18 -0
- package/templates-ts/next-static/dig.toml +17 -0
- package/templates-ts/next-static/next-env.d.ts +5 -0
- package/templates-ts/next-static/next.config.mjs +11 -0
- package/templates-ts/next-static/package.json +23 -0
- package/templates-ts/next-static/tsconfig.json +21 -0
- package/templates-ts/nft-drop/README.md +74 -0
- package/templates-ts/nft-drop/_env.example +7 -0
- package/templates-ts/nft-drop/_gitignore +10 -0
- package/templates-ts/nft-drop/dig.toml +18 -0
- package/templates-ts/nft-drop/index.html +12 -0
- package/templates-ts/nft-drop/package.json +28 -0
- package/templates-ts/nft-drop/src/App.tsx +93 -0
- package/templates-ts/nft-drop/src/main.tsx +13 -0
- package/templates-ts/nft-drop/src/mint.ts +86 -0
- package/templates-ts/nft-drop/src/styles.css +70 -0
- package/templates-ts/nft-drop/src/vite-env.d.ts +11 -0
- package/templates-ts/nft-drop/tsconfig.json +20 -0
- package/templates-ts/nft-drop/vite.config.ts +13 -0
- package/templates-ts/vite-react/README.md +44 -0
- package/templates-ts/vite-react/_gitignore +5 -0
- package/templates-ts/vite-react/dig.toml +17 -0
- package/templates-ts/vite-react/index.html +12 -0
- package/templates-ts/vite-react/package.json +24 -0
- package/templates-ts/vite-react/src/App.tsx +18 -0
- package/templates-ts/vite-react/src/main.tsx +13 -0
- package/templates-ts/vite-react/src/styles.css +49 -0
- package/templates-ts/vite-react/src/vite-env.d.ts +1 -0
- package/templates-ts/vite-react/tsconfig.json +20 -0
- package/templates-ts/vite-react/vite.config.ts +9 -0
package/lib/cli.js
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// CLI argument parsing + interactive flow for `create-dig-app`.
|
|
2
|
+
//
|
|
3
|
+
// Kept separate from index.js (the pure scaffolding lib) so the arg/prompt logic is independently
|
|
4
|
+
// testable and the scaffold core stays free of process/TTY concerns. `run()` is what bin/ calls.
|
|
5
|
+
//
|
|
6
|
+
// Agent-friendly surface (see AGENT_FRIENDLY.md → create-dig-app):
|
|
7
|
+
// - `--json` one structured object on stdout, all human prose on stderr, no prompts.
|
|
8
|
+
// - `--help-json` the full flag/template tree + the exit-code table as data.
|
|
9
|
+
// - `--list-templates` the TEMPLATES registry as data (pair with `--json`).
|
|
10
|
+
// - a differentiated, documented EXIT table (no single generic exit 1).
|
|
11
|
+
// - on failure, `{ok:false,error:{code,exit_code,message,hint,…}}` under `--json`.
|
|
12
|
+
|
|
13
|
+
import { createInterface } from "node:readline/promises";
|
|
14
|
+
import { stdin, stdout } from "node:process";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
import { TEMPLATES, templateNames, UnknownTemplateError } from "./templates.js";
|
|
18
|
+
import { scaffold, normalizeAppName, normalizeLang, resolveTemplate } from "./index.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The schema version of the machine-readable output envelopes (`--json`, `--help-json`,
|
|
22
|
+
* `--list-templates --json`). Bump on any breaking change to those shapes.
|
|
23
|
+
*/
|
|
24
|
+
export const SCHEMA_VERSION = 1;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The stable, documented exit-code table. Each failure class gets a distinct non-zero code so a
|
|
28
|
+
* script/agent can branch on the *kind* of failure, not just success/failure. Documented in README
|
|
29
|
+
* and emitted from `--help-json`.
|
|
30
|
+
* @type {{ SUCCESS:0, USAGE:2, UNKNOWN_TEMPLATE:3, TARGET_NOT_EMPTY:4, MISSING_ARGS:5, TEMPLATE_FILES_MISSING:6, INVALID_APP_NAME:7, INTERNAL:1 }}
|
|
31
|
+
*/
|
|
32
|
+
export const EXIT = Object.freeze({
|
|
33
|
+
SUCCESS: 0,
|
|
34
|
+
INTERNAL: 1, // an unexpected/uncategorized error (kept as the generic fallback only)
|
|
35
|
+
USAGE: 2, // bad/unknown option or malformed arguments
|
|
36
|
+
UNKNOWN_TEMPLATE: 3, // --template names a template that does not exist
|
|
37
|
+
TARGET_NOT_EMPTY: 4, // the target directory exists and is not empty (refusing to overwrite)
|
|
38
|
+
MISSING_ARGS: 5, // required args missing and not running interactively (no TTY)
|
|
39
|
+
TEMPLATE_FILES_MISSING: 6, // the bundled template files are missing (a packaging bug)
|
|
40
|
+
INVALID_APP_NAME: 7, // the app name normalizes to nothing usable
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Human-readable meaning for each exit code (emitted in `--help-json`). */
|
|
44
|
+
const EXIT_MEANINGS = Object.freeze({
|
|
45
|
+
[EXIT.SUCCESS]: "success",
|
|
46
|
+
[EXIT.INTERNAL]: "unexpected internal error",
|
|
47
|
+
[EXIT.USAGE]: "usage error (bad or unknown option / malformed arguments)",
|
|
48
|
+
[EXIT.UNKNOWN_TEMPLATE]: "unknown template id",
|
|
49
|
+
[EXIT.TARGET_NOT_EMPTY]: "target directory exists and is not empty",
|
|
50
|
+
[EXIT.MISSING_ARGS]: "required arguments missing in non-interactive mode",
|
|
51
|
+
[EXIT.TEMPLATE_FILES_MISSING]: "bundled template files are missing",
|
|
52
|
+
[EXIT.INVALID_APP_NAME]: "app name is not usable",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse argv (after `node script`) into the scaffold inputs.
|
|
57
|
+
* Supports: `<name>` positional, `--template/-t <t>`, the language flags
|
|
58
|
+
* (`--typescript`/`--ts`, `--javascript`/`--js`, `--lang <js|ts>`/`--lang=<v>`), `--help/-h`,
|
|
59
|
+
* `--version/-v`, the machine flags `--json` / `--help-json` / `--list-templates`, and the
|
|
60
|
+
* npm-init passthrough form (`npm create dig-app name -- --template t`).
|
|
61
|
+
*
|
|
62
|
+
* @param {string[]} argv
|
|
63
|
+
* @returns {{ appName?: string, template?: string, lang?: ("js"|"ts"), help?: boolean,
|
|
64
|
+
* helpJson?: boolean, version?: boolean, json?: boolean, listTemplates?: boolean }}
|
|
65
|
+
*/
|
|
66
|
+
export function parseArgs(argv) {
|
|
67
|
+
const out = {};
|
|
68
|
+
for (let i = 0; i < argv.length; i++) {
|
|
69
|
+
const a = argv[i];
|
|
70
|
+
if (a === "--help" || a === "-h") out.help = true;
|
|
71
|
+
else if (a === "--help-json") out.helpJson = true;
|
|
72
|
+
else if (a === "--version" || a === "-v") out.version = true;
|
|
73
|
+
else if (a === "--json") out.json = true;
|
|
74
|
+
else if (a === "--list-templates") out.listTemplates = true;
|
|
75
|
+
else if (a === "--template" || a === "-t") out.template = argv[++i];
|
|
76
|
+
else if (a.startsWith("--template=")) out.template = a.slice("--template=".length);
|
|
77
|
+
// Language: long/short shorthands and the explicit `--lang <value>` (value normalized below).
|
|
78
|
+
else if (a === "--typescript" || a === "--ts") out.lang = "ts";
|
|
79
|
+
else if (a === "--javascript" || a === "--js") out.lang = "js";
|
|
80
|
+
else if (a === "--lang") out.lang = normalizeLang(argv[++i]);
|
|
81
|
+
else if (a.startsWith("--lang=")) out.lang = normalizeLang(a.slice("--lang=".length));
|
|
82
|
+
else if (a === "--") continue; // npm-init separator
|
|
83
|
+
else if (a.startsWith("-")) throw new Error(`Unknown option: ${a}`);
|
|
84
|
+
else if (out.appName === undefined) out.appName = a;
|
|
85
|
+
// ignore extra positionals
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The `--help` text. */
|
|
91
|
+
export function helpText() {
|
|
92
|
+
const list = templateNames()
|
|
93
|
+
.map((n) => ` ${n.padEnd(18)} ${TEMPLATES[n].description}`)
|
|
94
|
+
.join("\n");
|
|
95
|
+
return [
|
|
96
|
+
"create-dig-app — scaffold a wallet-wired, deployable DIG Network app (free, no mint).",
|
|
97
|
+
"Prefer Rust? `digstore new` scaffolds the same starters from the CLI.",
|
|
98
|
+
"",
|
|
99
|
+
"Usage:",
|
|
100
|
+
" npm create dig-app@latest <name> -- --template <template>",
|
|
101
|
+
" npm create dig-app@latest # interactive",
|
|
102
|
+
"",
|
|
103
|
+
"Options:",
|
|
104
|
+
" -t, --template <template> One of the templates below",
|
|
105
|
+
" --typescript, --ts Scaffold the TypeScript variant (where available)",
|
|
106
|
+
" --javascript, --js Scaffold the JavaScript variant (default)",
|
|
107
|
+
" --lang <js|ts> Same as the language flags above",
|
|
108
|
+
" --json Emit one structured result on stdout (prose → stderr)",
|
|
109
|
+
" --list-templates List the templates (pair with --json for machine output)",
|
|
110
|
+
" --help-json Print the full flag/template tree + exit codes as JSON",
|
|
111
|
+
" -h, --help Show this help",
|
|
112
|
+
" -v, --version Print the version",
|
|
113
|
+
"",
|
|
114
|
+
"Templates:",
|
|
115
|
+
list,
|
|
116
|
+
"",
|
|
117
|
+
"Scaffolding is FREE: no mint, no chain, no spend. You spend $DIG only when you publish a",
|
|
118
|
+
"capsule with `digstore deploy`. Preview free first with `digstore dev`.",
|
|
119
|
+
"Published apps live on DIGHUb (hub.dig.net/stores/<id>) and, optionally, at <name>.on.dig.net.",
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The template registry as plain data (for `--list-templates --json`): id, title, description,
|
|
125
|
+
* output dir, build command, wallet flag, and supported languages — straight from {@link TEMPLATES}
|
|
126
|
+
* (no second source of truth).
|
|
127
|
+
* @returns {Array<{id:string,title:string,description:string,outputDir:string,buildCommand:string,wallet:boolean,langs:string[]}>}
|
|
128
|
+
*/
|
|
129
|
+
export function listTemplatesJson() {
|
|
130
|
+
return templateNames().map((id) => {
|
|
131
|
+
const t = TEMPLATES[id];
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
title: t.title,
|
|
135
|
+
description: t.description,
|
|
136
|
+
outputDir: t.outputDir,
|
|
137
|
+
buildCommand: t.buildCommand,
|
|
138
|
+
wallet: t.wallet,
|
|
139
|
+
langs: [...t.langs],
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The complete machine self-description (`--help-json`): the global flag list, the template tree,
|
|
146
|
+
* and the exit-code table — so one introspection call yields the whole invocation contract.
|
|
147
|
+
* @returns {object}
|
|
148
|
+
*/
|
|
149
|
+
export function helpJson() {
|
|
150
|
+
return {
|
|
151
|
+
schemaVersion: SCHEMA_VERSION,
|
|
152
|
+
name: "create-dig-app",
|
|
153
|
+
summary: "Scaffold a wallet-wired, deployable DIG Network app (free, no mint).",
|
|
154
|
+
flags: [
|
|
155
|
+
{ name: "--template", alias: "-t", value: "<template>", description: "The template to scaffold." },
|
|
156
|
+
{ name: "--typescript", aliases: ["--ts"], description: "Scaffold the TypeScript variant (where available)." },
|
|
157
|
+
{ name: "--javascript", aliases: ["--js"], description: "Scaffold the JavaScript variant (default)." },
|
|
158
|
+
{ name: "--lang", value: "<js|ts>", description: "Same as the language flags." },
|
|
159
|
+
{ name: "--json", description: "Emit one structured result object on stdout; route prose to stderr." },
|
|
160
|
+
{ name: "--list-templates", description: "List the available templates (use with --json for data)." },
|
|
161
|
+
{ name: "--help-json", description: "Print this machine self-description as JSON." },
|
|
162
|
+
{ name: "--help", alias: "-h", description: "Show human help." },
|
|
163
|
+
{ name: "--version", alias: "-v", description: "Print the version." },
|
|
164
|
+
],
|
|
165
|
+
templates: listTemplatesJson(),
|
|
166
|
+
exitCodes: Object.fromEntries(
|
|
167
|
+
Object.entries(EXIT_MEANINGS).map(([code, meaning]) => [code, meaning]),
|
|
168
|
+
),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Classify an error thrown by the scaffolder/CLI into a stable machine code, exit code, and hint.
|
|
174
|
+
* The codes are UPPER_SNAKE symbolic strings (never derived from the human message) and pair 1:1
|
|
175
|
+
* with the {@link EXIT} table. Structured fields already on the error (e.g.
|
|
176
|
+
* {@link UnknownTemplateError}.requested) are carried through rather than flattened to prose.
|
|
177
|
+
*
|
|
178
|
+
* @param {unknown} e
|
|
179
|
+
* @returns {{ code: string, exit_code: number, message: string, hint: string, extra?: object }}
|
|
180
|
+
*/
|
|
181
|
+
export function classifyError(e) {
|
|
182
|
+
const message = (e && e.message) || String(e);
|
|
183
|
+
if (e instanceof UnknownTemplateError) {
|
|
184
|
+
return {
|
|
185
|
+
code: "UNKNOWN_TEMPLATE",
|
|
186
|
+
exit_code: EXIT.UNKNOWN_TEMPLATE,
|
|
187
|
+
message,
|
|
188
|
+
hint: `Run with --list-templates (or --help) to see the available templates.`,
|
|
189
|
+
extra: { template: e.requested },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// Plain-Error cases from the scaffolder, distinguished by their stable wording.
|
|
193
|
+
if (/exists and is not empty/i.test(message)) {
|
|
194
|
+
return {
|
|
195
|
+
code: "TARGET_NOT_EMPTY",
|
|
196
|
+
exit_code: EXIT.TARGET_NOT_EMPTY,
|
|
197
|
+
message,
|
|
198
|
+
hint: "Choose a new app name or empty the target directory, then re-run.",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (/template files for .* are missing/i.test(message)) {
|
|
202
|
+
return {
|
|
203
|
+
code: "TEMPLATE_FILES_MISSING",
|
|
204
|
+
exit_code: EXIT.TEMPLATE_FILES_MISSING,
|
|
205
|
+
message,
|
|
206
|
+
hint: "This is a packaging bug — reinstall create-dig-app or report it.",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (/is not a usable app name|app name must be a string/i.test(message)) {
|
|
210
|
+
return {
|
|
211
|
+
code: "INVALID_APP_NAME",
|
|
212
|
+
exit_code: EXIT.INVALID_APP_NAME,
|
|
213
|
+
message,
|
|
214
|
+
hint: "Use a name containing letters or digits (it is slugified to an npm-safe package name).",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
code: "INTERNAL",
|
|
219
|
+
exit_code: EXIT.INTERNAL,
|
|
220
|
+
message,
|
|
221
|
+
hint: "An unexpected error occurred. Re-run with --help for usage.",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Ask a free-text question with an optional default.
|
|
227
|
+
* Exported for unit testing; `rl` is any object with an async `question(prompt) => string` (the
|
|
228
|
+
* production caller passes a `node:readline/promises` interface).
|
|
229
|
+
* @param {{ question: (q: string) => Promise<string> }} rl
|
|
230
|
+
* @param {string} question
|
|
231
|
+
* @param {string} [fallback]
|
|
232
|
+
* @returns {Promise<string>}
|
|
233
|
+
*/
|
|
234
|
+
export async function ask(rl, question, fallback) {
|
|
235
|
+
const suffix = fallback ? ` (${fallback})` : "";
|
|
236
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
237
|
+
return answer || fallback || "";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Prompt for a template by number from the registry (or by typing its name). Re-prompts on an
|
|
242
|
+
* out-of-range / unrecognized answer. Exported for unit testing (pass a fake `rl`).
|
|
243
|
+
* @param {{ question: (q: string) => Promise<string> }} rl
|
|
244
|
+
* @returns {Promise<string>} a canonical template id.
|
|
245
|
+
*/
|
|
246
|
+
export async function askTemplate(rl) {
|
|
247
|
+
const names = templateNames();
|
|
248
|
+
stdout.write("\nPick a template:\n");
|
|
249
|
+
names.forEach((n, i) => {
|
|
250
|
+
stdout.write(` ${i + 1}) ${n} — ${TEMPLATES[n].description}\n`);
|
|
251
|
+
});
|
|
252
|
+
while (true) {
|
|
253
|
+
const raw = (await rl.question(`Template [1-${names.length}] (1): `)).trim() || "1";
|
|
254
|
+
const idx = Number.parseInt(raw, 10);
|
|
255
|
+
if (Number.isInteger(idx) && idx >= 1 && idx <= names.length) return names[idx - 1];
|
|
256
|
+
if (names.includes(raw)) return raw; // allow typing the name
|
|
257
|
+
stdout.write(`Please enter a number 1-${names.length} (or a template name).\n`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Prompt for the scaffold language. Only asked for templates that actually offer a TypeScript
|
|
263
|
+
* variant; JS-only templates (e.g. `static-site`) skip the prompt entirely. Returns "js" or "ts".
|
|
264
|
+
* Exported for unit testing (pass a fake `rl`).
|
|
265
|
+
* @param {{ question: (q: string) => Promise<string> }} rl
|
|
266
|
+
* @param {string} template A validated template id.
|
|
267
|
+
* @returns {Promise<("js"|"ts")>}
|
|
268
|
+
*/
|
|
269
|
+
export async function askLang(rl, template) {
|
|
270
|
+
// Only the JS variant exists for some templates — don't ask a question with one answer.
|
|
271
|
+
if (!resolveTemplate(template).langs.includes("ts")) return "js";
|
|
272
|
+
stdout.write("\nLanguage:\n");
|
|
273
|
+
stdout.write(" 1) JavaScript\n");
|
|
274
|
+
stdout.write(" 2) TypeScript\n");
|
|
275
|
+
while (true) {
|
|
276
|
+
const raw = (await rl.question("Language [1-2] (1): ")).trim() || "1";
|
|
277
|
+
if (raw === "1") return "js";
|
|
278
|
+
if (raw === "2") return "ts";
|
|
279
|
+
try {
|
|
280
|
+
return normalizeLang(raw); // also accept "js"/"javascript"/"ts"/"typescript" typed directly
|
|
281
|
+
} catch {
|
|
282
|
+
stdout.write("Please enter 1 (JavaScript) or 2 (TypeScript).\n");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Run the CLI end-to-end: parse args, fill gaps interactively, scaffold, print next steps.
|
|
289
|
+
*
|
|
290
|
+
* @param {string[]} argv argv after `node <script>`.
|
|
291
|
+
* @param {{ cwd?: string, log?: (s: string) => void, out?: (s: string) => void,
|
|
292
|
+
* err?: (s: string) => void, version?: string }} [io]
|
|
293
|
+
* `log` is the default human sink (stdout). Under `--json`, structured output goes to `out`
|
|
294
|
+
* (stdout) and all human prose goes to `err` (stderr). When `out`/`err` are omitted they default
|
|
295
|
+
* to process stdout/stderr.
|
|
296
|
+
* @returns {Promise<number>} a process exit code (see {@link EXIT}).
|
|
297
|
+
*/
|
|
298
|
+
export async function run(argv, io = {}) {
|
|
299
|
+
const log = io.log ?? ((s) => stdout.write(s + "\n"));
|
|
300
|
+
const out = io.out ?? ((s) => stdout.write(s + "\n"));
|
|
301
|
+
const err = io.err ?? ((s) => process.stderr.write(s + "\n"));
|
|
302
|
+
const cwd = io.cwd ?? process.cwd();
|
|
303
|
+
|
|
304
|
+
let args;
|
|
305
|
+
try {
|
|
306
|
+
args = parseArgs(argv);
|
|
307
|
+
} catch (e) {
|
|
308
|
+
// A parse failure (unknown option / bad --lang) is a USAGE error.
|
|
309
|
+
if (argvHasJson(argv)) {
|
|
310
|
+
emitJsonError(out, { code: "BAD_USAGE", exit_code: EXIT.USAGE, message: e.message, hint: "Run with --help for usage." });
|
|
311
|
+
} else {
|
|
312
|
+
log(e.message);
|
|
313
|
+
log("Run with --help for usage.");
|
|
314
|
+
}
|
|
315
|
+
return EXIT.USAGE;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const json = args.json === true;
|
|
319
|
+
// Under --json, human prose must go to stderr; otherwise it goes to the normal log sink.
|
|
320
|
+
const human = json ? err : log;
|
|
321
|
+
|
|
322
|
+
// Machine self-description — always to stdout, never gated by --json.
|
|
323
|
+
if (args.helpJson) {
|
|
324
|
+
out(JSON.stringify(helpJson(), null, 2));
|
|
325
|
+
return EXIT.SUCCESS;
|
|
326
|
+
}
|
|
327
|
+
if (args.listTemplates) {
|
|
328
|
+
if (json) out(JSON.stringify({ schemaVersion: SCHEMA_VERSION, templates: listTemplatesJson() }, null, 2));
|
|
329
|
+
else log(helpText());
|
|
330
|
+
return EXIT.SUCCESS;
|
|
331
|
+
}
|
|
332
|
+
if (args.help) {
|
|
333
|
+
human(helpText());
|
|
334
|
+
return EXIT.SUCCESS;
|
|
335
|
+
}
|
|
336
|
+
if (args.version) {
|
|
337
|
+
if (json) out(JSON.stringify({ schemaVersion: SCHEMA_VERSION, version: io.version ?? "unknown" }, null, 2));
|
|
338
|
+
else log(io.version ?? "unknown");
|
|
339
|
+
return EXIT.SUCCESS;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Resolve app name + template (+ language), prompting interactively for anything missing.
|
|
343
|
+
// Under --json we never prompt (agents run unattended) — missing args fail closed.
|
|
344
|
+
let { appName, template, lang } = args;
|
|
345
|
+
const interactive = !json && stdin.isTTY && stdout.isTTY;
|
|
346
|
+
let rl;
|
|
347
|
+
try {
|
|
348
|
+
if (!appName || !template) {
|
|
349
|
+
if (!interactive) {
|
|
350
|
+
if (json) {
|
|
351
|
+
emitJsonError(out, {
|
|
352
|
+
code: "MISSING_ARGS",
|
|
353
|
+
exit_code: EXIT.MISSING_ARGS,
|
|
354
|
+
message: "Missing required arguments: <name> and --template are required in non-interactive mode.",
|
|
355
|
+
hint: "Pass a name and --template, e.g. `create-dig-app my-app --template vite-react --json`.",
|
|
356
|
+
});
|
|
357
|
+
} else {
|
|
358
|
+
log("Missing required arguments. Usage:");
|
|
359
|
+
log(" npm create dig-app@latest <name> -- --template <template>");
|
|
360
|
+
log("Run with --help to see templates.");
|
|
361
|
+
}
|
|
362
|
+
return EXIT.MISSING_ARGS;
|
|
363
|
+
}
|
|
364
|
+
rl = createInterface({ input: stdin, output: stdout });
|
|
365
|
+
if (!appName) appName = await ask(rl, "Project name", "my-dig-app");
|
|
366
|
+
if (!template) template = await askTemplate(rl);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Validate the template early (accepts the hidden `static` alias via resolveTemplate).
|
|
370
|
+
try {
|
|
371
|
+
resolveTemplate(template);
|
|
372
|
+
} catch (e) {
|
|
373
|
+
const info = classifyError(e);
|
|
374
|
+
if (json) emitJsonError(out, info);
|
|
375
|
+
else log(info.message);
|
|
376
|
+
return info.exit_code;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Ask for the language only when interactive and not already specified on the command line.
|
|
380
|
+
if (lang === undefined && rl) lang = await askLang(rl, template);
|
|
381
|
+
|
|
382
|
+
const targetDir = resolve(cwd, normalizeAppName(appName));
|
|
383
|
+
const result = scaffold({ appName, template, lang, targetDir });
|
|
384
|
+
|
|
385
|
+
if (json) {
|
|
386
|
+
out(JSON.stringify({ schemaVersion: SCHEMA_VERSION, ok: true, result }, null, 2));
|
|
387
|
+
return EXIT.SUCCESS;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Tell the user the language that was actually scaffolded. If they asked for TS on a JS-only
|
|
391
|
+
// template (e.g. `static-site`), say so plainly rather than silently producing a JS project.
|
|
392
|
+
const langLabel = result.lang === "ts" ? "TypeScript" : "JavaScript";
|
|
393
|
+
human("");
|
|
394
|
+
human(
|
|
395
|
+
`Scaffolded "${result.appName}" (${result.template}, ${langLabel}) — free, no mint, no chain, no spend.`,
|
|
396
|
+
);
|
|
397
|
+
if (result.requestedLang === "ts" && result.lang === "js") {
|
|
398
|
+
human(
|
|
399
|
+
`Note: the "${result.template}" template has no TypeScript variant, so it was scaffolded in JavaScript.`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
human("");
|
|
403
|
+
human("Next steps:");
|
|
404
|
+
for (const step of result.nextSteps) human(` ${step}`);
|
|
405
|
+
human("");
|
|
406
|
+
human("Free to build and preview — you spend $DIG only when you publish a capsule.");
|
|
407
|
+
human("Your app deploys to DIGHUb (hub.dig.net/stores/<id>); register an optional name.on.dig.net too.");
|
|
408
|
+
human("Deploy from CI: https://docs.dig.net/docs/digstore/cli/deploy-from-github-actions");
|
|
409
|
+
return EXIT.SUCCESS;
|
|
410
|
+
} catch (e) {
|
|
411
|
+
const info = classifyError(e);
|
|
412
|
+
if (json) emitJsonError(out, info);
|
|
413
|
+
else log(`Error: ${info.message}`);
|
|
414
|
+
return info.exit_code;
|
|
415
|
+
} finally {
|
|
416
|
+
if (rl) rl.close();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** True if `--json` appears in argv (needed before parseArgs succeeds, e.g. a parse error). */
|
|
421
|
+
function argvHasJson(argv) {
|
|
422
|
+
return Array.isArray(argv) && argv.includes("--json");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Emit a structured error envelope to the machine stdout sink. */
|
|
426
|
+
function emitJsonError(out, { code, exit_code, message, hint, extra }) {
|
|
427
|
+
out(
|
|
428
|
+
JSON.stringify(
|
|
429
|
+
{
|
|
430
|
+
schemaVersion: SCHEMA_VERSION,
|
|
431
|
+
ok: false,
|
|
432
|
+
error: { code, exit_code, message, hint, ...(extra || {}) },
|
|
433
|
+
},
|
|
434
|
+
null,
|
|
435
|
+
2,
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// create-dig-app — the scaffolding library.
|
|
2
|
+
//
|
|
3
|
+
// `scaffold()` copies a template's file tree into a target directory, substituting `__TOKEN__`
|
|
4
|
+
// placeholders (app name, pinned SDK version, output dir, build command), and returns the resolved
|
|
5
|
+
// app name + the printable "next steps". The bin (bin/create-dig-app.js) is a thin CLI wrapper
|
|
6
|
+
// around this; tests drive this library directly.
|
|
7
|
+
//
|
|
8
|
+
// Design principle (ROADMAP §2 "free until publish"): scaffolding writes a LOCAL, runnable project
|
|
9
|
+
// and never mints, spends, or touches the chain. The next-steps + scaffolded README make that
|
|
10
|
+
// explicit and point at `digstore dev` (free preview) → `digstore deploy` (publish, $DIG).
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
cpSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
renameSync,
|
|
19
|
+
rmSync,
|
|
20
|
+
statSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_LANG,
|
|
28
|
+
TEMPLATES,
|
|
29
|
+
TEMPLATE_ALIASES,
|
|
30
|
+
UnknownTemplateError,
|
|
31
|
+
canonicalTemplateName,
|
|
32
|
+
normalizeLang,
|
|
33
|
+
resolveLang,
|
|
34
|
+
resolveTemplate,
|
|
35
|
+
templateNames,
|
|
36
|
+
} from "./templates.js";
|
|
37
|
+
import { applySubstitutions, buildSubstitutions } from "./substitute.js";
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
DEFAULT_LANG,
|
|
41
|
+
TEMPLATES,
|
|
42
|
+
TEMPLATE_ALIASES,
|
|
43
|
+
UnknownTemplateError,
|
|
44
|
+
canonicalTemplateName,
|
|
45
|
+
normalizeLang,
|
|
46
|
+
resolveLang,
|
|
47
|
+
resolveTemplate,
|
|
48
|
+
templateNames,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
52
|
+
|
|
53
|
+
// The @dignetwork/dig-sdk version specifier scaffolded wallet templates pin.
|
|
54
|
+
//
|
|
55
|
+
// The SDK is pre-1.0 and still stabilizing its first published release, so scaffolded projects track
|
|
56
|
+
// the `latest` dist-tag rather than a frozen caret range: a freshly scaffolded app installs the
|
|
57
|
+
// newest stable SDK at scaffold time, and we never ship a pin to a version that isn't published yet.
|
|
58
|
+
// (npm accepts a dist-tag wherever a version range is allowed.) Bump this to a caret range — e.g.
|
|
59
|
+
// `^1.0.0` — once the SDK cuts a stable semver release worth freezing scaffolds to.
|
|
60
|
+
export const SDK_VERSION = "latest";
|
|
61
|
+
|
|
62
|
+
/** Absolute path to the bundled JavaScript templates directory. */
|
|
63
|
+
const TEMPLATES_ROOT = join(__dirname, "..", "templates");
|
|
64
|
+
/** Absolute path to the bundled TypeScript template variants (one dir per TS-capable template). */
|
|
65
|
+
const TEMPLATES_TS_ROOT = join(__dirname, "..", "templates-ts");
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The on-disk source directory for a (template, lang) pair. JS templates live under templates/<name>;
|
|
69
|
+
* the TypeScript variant of a template lives under templates-ts/<name>. Kept here (not in the
|
|
70
|
+
* registry) so the registry stays declarative and the path convention is in one place.
|
|
71
|
+
* @param {import("./templates.js").TemplateMeta} meta
|
|
72
|
+
* @param {import("./templates.js").Lang} lang
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
function templateSrcDir(meta, lang) {
|
|
76
|
+
return lang === "ts" ? join(TEMPLATES_TS_ROOT, meta.name) : join(TEMPLATES_ROOT, meta.name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Files we ship in a template dir under a renamed form because npm/npx would otherwise mangle them
|
|
80
|
+
// when this package is published (npm renames a packaged `.gitignore` to `.npmignore`-safe `gitignore`,
|
|
81
|
+
// and refuses to publish a nested `package.json`-shaped file untouched). Templates therefore store
|
|
82
|
+
// these under a `_`-prefixed name and we rename them back on scaffold.
|
|
83
|
+
const RENAME_ON_COPY = {
|
|
84
|
+
_gitignore: ".gitignore",
|
|
85
|
+
"_npmrc": ".npmrc",
|
|
86
|
+
// Wallet templates ship a sample env (the WalletConnect projectId). npm's packing rules around
|
|
87
|
+
// dotfiles are finicky, so we ship it `_`-prefixed and restore the real `.env.example` on scaffold.
|
|
88
|
+
"_env.example": ".env.example",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Normalize a user-supplied app name into an npm-safe package name.
|
|
93
|
+
* Lowercase, trim, collapse runs of non-alphanumerics to a single dash, strip leading/trailing
|
|
94
|
+
* dashes. Throws if the result is empty.
|
|
95
|
+
* @param {string} raw
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function normalizeAppName(raw) {
|
|
99
|
+
if (typeof raw !== "string") throw new Error("app name must be a string");
|
|
100
|
+
const slug = raw
|
|
101
|
+
.trim()
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
104
|
+
.replace(/^-+|-+$/g, "");
|
|
105
|
+
if (!slug) throw new Error(`"${raw}" is not a usable app name (it must contain letters or digits)`);
|
|
106
|
+
return slug;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** True if a directory does not exist or exists but is empty. */
|
|
110
|
+
function isEmptyDir(dir) {
|
|
111
|
+
if (!existsSync(dir)) return true;
|
|
112
|
+
if (!statSync(dir).isDirectory()) return false;
|
|
113
|
+
return readdirSync(dir).length === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Recursively list every file path under dir. */
|
|
117
|
+
function walkFiles(dir) {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const entry of readdirSync(dir)) {
|
|
120
|
+
const p = join(dir, entry);
|
|
121
|
+
if (statSync(p).isDirectory()) out.push(...walkFiles(p));
|
|
122
|
+
else out.push(p);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Heuristic: treat a file as text (substitutable) unless its extension marks it binary. */
|
|
128
|
+
const BINARY_EXT = new Set([".png", ".jpg", ".jpeg", ".gif", ".ico", ".webp", ".woff", ".woff2", ".wasm"]);
|
|
129
|
+
function isBinary(path) {
|
|
130
|
+
const dot = path.lastIndexOf(".");
|
|
131
|
+
return dot >= 0 && BINARY_EXT.has(path.slice(dot).toLowerCase());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Scaffold a new project from a template.
|
|
136
|
+
*
|
|
137
|
+
* @param {Object} args
|
|
138
|
+
* @param {string} args.appName The user-supplied app name (normalized to an npm-safe slug).
|
|
139
|
+
* @param {string} args.template A template id (must be in {@link TEMPLATES}).
|
|
140
|
+
* @param {import("./templates.js").Lang} [args.lang] Requested language ("js" default, or "ts").
|
|
141
|
+
* If the template has no variant for the requested language (e.g. "ts" for `static-site`), it
|
|
142
|
+
* falls back to the default and the returned `lang` reflects what was actually scaffolded.
|
|
143
|
+
* @param {string} args.targetDir Absolute/relative path to create the project in (must be empty/new).
|
|
144
|
+
* @returns {{ appName: string, template: string, lang: import("./templates.js").Lang, requestedLang: import("./templates.js").Lang, targetDir: string, nextSteps: string[] }}
|
|
145
|
+
*/
|
|
146
|
+
export function scaffold({ appName, template, lang, targetDir }) {
|
|
147
|
+
// Validate template + name BEFORE writing anything, so a bad invocation leaves the FS untouched.
|
|
148
|
+
const meta = resolveTemplate(template);
|
|
149
|
+
const slug = normalizeAppName(appName);
|
|
150
|
+
// Normalize the requested language, then resolve it against what the template actually offers
|
|
151
|
+
// (a TS request for a JS-only template like `static-site` resolves back to "js").
|
|
152
|
+
const requestedLang = lang === undefined ? DEFAULT_LANG : normalizeLang(lang);
|
|
153
|
+
const resolvedLang = resolveLang(meta, requestedLang);
|
|
154
|
+
|
|
155
|
+
if (!targetDir) throw new Error("targetDir is required");
|
|
156
|
+
if (!isEmptyDir(targetDir)) {
|
|
157
|
+
throw new Error(`target directory "${targetDir}" exists and is not empty — choose a new name or empty it.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const src = templateSrcDir(meta, resolvedLang);
|
|
161
|
+
if (!existsSync(src)) {
|
|
162
|
+
throw new Error(`template files for "${meta.name}" (${resolvedLang}) are missing at ${src}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const subs = buildSubstitutions({
|
|
166
|
+
appName: slug,
|
|
167
|
+
displayName: appName.trim(),
|
|
168
|
+
sdkVersion: SDK_VERSION,
|
|
169
|
+
outputDir: meta.outputDir,
|
|
170
|
+
buildCommand: meta.buildCommand,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Copy the whole tree, then substitute placeholders in text files, then apply the renames.
|
|
174
|
+
// Copy first (cpSync handles nested dirs) so binary assets come across untouched.
|
|
175
|
+
mkdirSync(targetDir, { recursive: true });
|
|
176
|
+
try {
|
|
177
|
+
cpSync(src, targetDir, { recursive: true });
|
|
178
|
+
|
|
179
|
+
for (const file of walkFiles(targetDir)) {
|
|
180
|
+
if (isBinary(file)) continue;
|
|
181
|
+
const text = readFileSync(file, "utf8");
|
|
182
|
+
const next = applySubstitutions(text, subs);
|
|
183
|
+
if (next !== text) writeFileSync(file, next);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Restore `_`-prefixed shipped files to their dotfile names.
|
|
187
|
+
for (const [shipped, real] of Object.entries(RENAME_ON_COPY)) {
|
|
188
|
+
const from = join(targetDir, shipped);
|
|
189
|
+
if (existsSync(from)) renameSync(from, join(targetDir, real));
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
// Leave no half-written tree behind on failure.
|
|
193
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
appName: slug,
|
|
199
|
+
template: meta.name,
|
|
200
|
+
lang: resolvedLang,
|
|
201
|
+
requestedLang,
|
|
202
|
+
targetDir,
|
|
203
|
+
nextSteps: nextSteps({ slug, meta }),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The printable next-steps for a freshly scaffolded project. Names the free local loop and makes the
|
|
209
|
+
* "no mint / no spend at scaffold time" guarantee explicit (ROADMAP §2).
|
|
210
|
+
* @param {{ slug: string, meta: import("./templates.js").TemplateMeta }} args
|
|
211
|
+
* @returns {string[]}
|
|
212
|
+
*/
|
|
213
|
+
export function nextSteps({ slug, meta }) {
|
|
214
|
+
const steps = [
|
|
215
|
+
`cd ${slug}`,
|
|
216
|
+
`npm install`,
|
|
217
|
+
];
|
|
218
|
+
// `nft-collection` is an asset workspace, not a dev-server app: instead of `npm run dev` it has a
|
|
219
|
+
// generate → validate loop that produces the canonical CHIP-0007 metadata + the mint manifest.
|
|
220
|
+
if (meta.name === "nft-collection") {
|
|
221
|
+
steps.push(
|
|
222
|
+
`npm run generate # write licenses/ + metadata/ + items.json from images/ + collection.json`,
|
|
223
|
+
`npm run validate # check the CHIP-0007 schema + every data/metadata/license hash`,
|
|
224
|
+
);
|
|
225
|
+
} else if (meta.buildCommand && meta.name !== "static-site") {
|
|
226
|
+
// The static-site template has no dev server (its "build" just copies src/ → public/), so skip
|
|
227
|
+
// the `npm run dev` step for it; every other app template runs a real dev server.
|
|
228
|
+
steps.push(`npm run dev # work on your app locally`);
|
|
229
|
+
}
|
|
230
|
+
steps.push(
|
|
231
|
+
`digstore dev # preview on the real chia:// read path — FREE, no mint, no chain, no spend`,
|
|
232
|
+
`digstore deploy # publish a capsule when you're ready (this is the only step that spends $DIG)`,
|
|
233
|
+
);
|
|
234
|
+
return steps;
|
|
235
|
+
}
|