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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/bin/create-dig-app.js +17 -0
  4. package/lib/cli.js +438 -0
  5. package/lib/index.js +235 -0
  6. package/lib/nft-cli.js +265 -0
  7. package/lib/nft-metadata.js +515 -0
  8. package/lib/substitute.js +47 -0
  9. package/lib/templates.js +185 -0
  10. package/package.json +51 -0
  11. package/templates/dapp-window-chia/README.md +63 -0
  12. package/templates/dapp-window-chia/_env.example +7 -0
  13. package/templates/dapp-window-chia/_gitignore +10 -0
  14. package/templates/dapp-window-chia/dig.toml +17 -0
  15. package/templates/dapp-window-chia/index.html +12 -0
  16. package/templates/dapp-window-chia/package.json +24 -0
  17. package/templates/dapp-window-chia/src/App.jsx +91 -0
  18. package/templates/dapp-window-chia/src/main.jsx +10 -0
  19. package/templates/dapp-window-chia/src/styles.css +71 -0
  20. package/templates/dapp-window-chia/src/wallet.js +47 -0
  21. package/templates/dapp-window-chia/vite.config.js +14 -0
  22. package/templates/next-static/README.md +41 -0
  23. package/templates/next-static/_gitignore +6 -0
  24. package/templates/next-static/app/globals.css +49 -0
  25. package/templates/next-static/app/layout.jsx +14 -0
  26. package/templates/next-static/app/page.jsx +18 -0
  27. package/templates/next-static/dig.toml +17 -0
  28. package/templates/next-static/next.config.mjs +11 -0
  29. package/templates/next-static/package.json +16 -0
  30. package/templates/nft-collection/README.md +119 -0
  31. package/templates/nft-collection/_env.example +5 -0
  32. package/templates/nft-collection/_gitignore +9 -0
  33. package/templates/nft-collection/assets/banner.svg +11 -0
  34. package/templates/nft-collection/assets/icon.svg +10 -0
  35. package/templates/nft-collection/collection.json +14 -0
  36. package/templates/nft-collection/dig.toml +23 -0
  37. package/templates/nft-collection/images/sample-1.svg +5 -0
  38. package/templates/nft-collection/images/sample-2.svg +5 -0
  39. package/templates/nft-collection/licenses/.gitkeep +1 -0
  40. package/templates/nft-collection/metadata/.gitkeep +2 -0
  41. package/templates/nft-collection/package.json +18 -0
  42. package/templates/nft-collection/scripts/dig-nft.mjs +479 -0
  43. package/templates/nft-collection/src/mint.js +72 -0
  44. package/templates/nft-collection/src/styles.css +64 -0
  45. package/templates/nft-collection/traits.csv +3 -0
  46. package/templates/nft-drop/README.md +71 -0
  47. package/templates/nft-drop/_env.example +7 -0
  48. package/templates/nft-drop/_gitignore +10 -0
  49. package/templates/nft-drop/dig.toml +18 -0
  50. package/templates/nft-drop/index.html +12 -0
  51. package/templates/nft-drop/package.json +24 -0
  52. package/templates/nft-drop/src/App.jsx +91 -0
  53. package/templates/nft-drop/src/main.jsx +10 -0
  54. package/templates/nft-drop/src/mint.js +72 -0
  55. package/templates/nft-drop/src/styles.css +70 -0
  56. package/templates/nft-drop/vite.config.js +13 -0
  57. package/templates/static-site/README.md +41 -0
  58. package/templates/static-site/_gitignore +5 -0
  59. package/templates/static-site/dig.toml +19 -0
  60. package/templates/static-site/package.json +10 -0
  61. package/templates/static-site/scripts/build.mjs +14 -0
  62. package/templates/static-site/src/index.html +20 -0
  63. package/templates/static-site/src/styles.css +49 -0
  64. package/templates/vite-react/README.md +42 -0
  65. package/templates/vite-react/_gitignore +5 -0
  66. package/templates/vite-react/dig.toml +17 -0
  67. package/templates/vite-react/index.html +12 -0
  68. package/templates/vite-react/package.json +20 -0
  69. package/templates/vite-react/src/App.jsx +18 -0
  70. package/templates/vite-react/src/main.jsx +10 -0
  71. package/templates/vite-react/src/styles.css +49 -0
  72. package/templates/vite-react/vite.config.js +9 -0
  73. package/templates-ts/dapp-window-chia/README.md +67 -0
  74. package/templates-ts/dapp-window-chia/_env.example +7 -0
  75. package/templates-ts/dapp-window-chia/_gitignore +10 -0
  76. package/templates-ts/dapp-window-chia/dig.toml +17 -0
  77. package/templates-ts/dapp-window-chia/index.html +12 -0
  78. package/templates-ts/dapp-window-chia/package.json +28 -0
  79. package/templates-ts/dapp-window-chia/src/App.tsx +93 -0
  80. package/templates-ts/dapp-window-chia/src/main.tsx +13 -0
  81. package/templates-ts/dapp-window-chia/src/styles.css +71 -0
  82. package/templates-ts/dapp-window-chia/src/vite-env.d.ts +11 -0
  83. package/templates-ts/dapp-window-chia/src/wallet.ts +62 -0
  84. package/templates-ts/dapp-window-chia/tsconfig.json +20 -0
  85. package/templates-ts/dapp-window-chia/vite.config.ts +14 -0
  86. package/templates-ts/next-static/README.md +43 -0
  87. package/templates-ts/next-static/_gitignore +7 -0
  88. package/templates-ts/next-static/app/globals.css +49 -0
  89. package/templates-ts/next-static/app/layout.tsx +16 -0
  90. package/templates-ts/next-static/app/page.tsx +18 -0
  91. package/templates-ts/next-static/dig.toml +17 -0
  92. package/templates-ts/next-static/next-env.d.ts +5 -0
  93. package/templates-ts/next-static/next.config.mjs +11 -0
  94. package/templates-ts/next-static/package.json +23 -0
  95. package/templates-ts/next-static/tsconfig.json +21 -0
  96. package/templates-ts/nft-drop/README.md +74 -0
  97. package/templates-ts/nft-drop/_env.example +7 -0
  98. package/templates-ts/nft-drop/_gitignore +10 -0
  99. package/templates-ts/nft-drop/dig.toml +18 -0
  100. package/templates-ts/nft-drop/index.html +12 -0
  101. package/templates-ts/nft-drop/package.json +28 -0
  102. package/templates-ts/nft-drop/src/App.tsx +93 -0
  103. package/templates-ts/nft-drop/src/main.tsx +13 -0
  104. package/templates-ts/nft-drop/src/mint.ts +86 -0
  105. package/templates-ts/nft-drop/src/styles.css +70 -0
  106. package/templates-ts/nft-drop/src/vite-env.d.ts +11 -0
  107. package/templates-ts/nft-drop/tsconfig.json +20 -0
  108. package/templates-ts/nft-drop/vite.config.ts +13 -0
  109. package/templates-ts/vite-react/README.md +44 -0
  110. package/templates-ts/vite-react/_gitignore +5 -0
  111. package/templates-ts/vite-react/dig.toml +17 -0
  112. package/templates-ts/vite-react/index.html +12 -0
  113. package/templates-ts/vite-react/package.json +24 -0
  114. package/templates-ts/vite-react/src/App.tsx +18 -0
  115. package/templates-ts/vite-react/src/main.tsx +13 -0
  116. package/templates-ts/vite-react/src/styles.css +49 -0
  117. package/templates-ts/vite-react/src/vite-env.d.ts +1 -0
  118. package/templates-ts/vite-react/tsconfig.json +20 -0
  119. package/templates-ts/vite-react/vite.config.ts +9 -0
package/lib/nft-cli.js ADDED
@@ -0,0 +1,265 @@
1
+ // NFT collection tooling — the project-directory orchestrator.
2
+ //
3
+ // Walks a real `nft-collection` project tree and runs the three generators over it:
4
+ // generateMetadata images/ [+ traits.csv/json] + collection.json → metadata/*.json + items.json
5
+ // generateLicense collection.json.license → licenses/LICENSE-<id>.txt (+ its hash/URI)
6
+ // validateProject metadata/*.json + items.json → re-verify schema + URI/hash agreement
7
+ //
8
+ // The pure CHIP-0007 logic lives in nft-metadata.js (this module is the file-system glue). Both are
9
+ // VENDORED into the template's scripts/dig-nft.mjs so a scaffolded project runs the same code with
10
+ // ZERO dependencies. The emitted items.json is the exact manifest `digstore collection mint
11
+ // --collection collection.json --manifest items.json` consumes (digstore-chain ManifestItem/Media),
12
+ // and `metadata/*.json` are the off-chain CHIP-0007 docs `metadata_uris` points at. (SYSTEM.md →
13
+ // "CHIP-0007 NFT metadata"; the asset CLI `collection mint`.)
14
+
15
+ import {
16
+ existsSync,
17
+ mkdirSync,
18
+ readFileSync,
19
+ readdirSync,
20
+ statSync,
21
+ writeFileSync,
22
+ } from "node:fs";
23
+ import { basename, extname, join } from "node:path";
24
+
25
+ import {
26
+ buildChip0007Metadata,
27
+ canonicalJson,
28
+ capsuleResourceUris,
29
+ collectionId,
30
+ generateItemMetadata,
31
+ itemsFromImages,
32
+ licenseFileName,
33
+ licenseText,
34
+ metadataHashHex,
35
+ parseTraitsCsv,
36
+ parseTraitsJson,
37
+ sha256Hex,
38
+ validateMetadata,
39
+ LICENSES,
40
+ } from "./nft-metadata.js";
41
+
42
+ /** Image extensions treated as collection art (case-insensitive). */
43
+ const IMAGE_EXT = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".avif"]);
44
+
45
+ /** Placeholder store id used in generated URIs until the capsule is published (then `digstore` fills the real one). */
46
+ export const PLACEHOLDER_STORE_ID = "STORE_ID_AFTER_PUBLISH";
47
+
48
+ /** Read + parse `collection.json`, deriving the canonical collection `id` from its name. */
49
+ export function loadCollection(root) {
50
+ const path = join(root, "collection.json");
51
+ if (!existsSync(path)) throw new Error(`collection.json not found at ${path}`);
52
+ const col = JSON.parse(readFileSync(path, "utf8"));
53
+ if (!col?.name) throw new Error("collection.json must have a `name`.");
54
+ return { ...col, id: col.id || collectionId(col.name) };
55
+ }
56
+
57
+ /** List the image files in images/ (sorted), throwing if there are none. */
58
+ function listImages(root) {
59
+ const dir = join(root, "images");
60
+ if (!existsSync(dir)) throw new Error(`images/ directory not found at ${dir}`);
61
+ const files = readdirSync(dir)
62
+ .filter((f) => statSync(join(dir, f)).isFile() && IMAGE_EXT.has(extname(f).toLowerCase()))
63
+ .sort();
64
+ if (files.length === 0) {
65
+ throw new Error("no images found in images/ — add your item art (png/jpg/gif/webp/svg) first.");
66
+ }
67
+ return files;
68
+ }
69
+
70
+ /** Build the normalized item list: from traits.csv / traits.json if present, else one per image. */
71
+ function resolveItems(root, imageFiles) {
72
+ const csv = join(root, "traits.csv");
73
+ const jsonTraits = join(root, "traits.json");
74
+ let items;
75
+ if (existsSync(csv)) {
76
+ items = parseTraitsCsv(readFileSync(csv, "utf8"));
77
+ } else if (existsSync(jsonTraits)) {
78
+ items = parseTraitsJson(readFileSync(jsonTraits, "utf8"));
79
+ } else {
80
+ return itemsFromImages(imageFiles);
81
+ }
82
+ // A manifest may name files explicitly; otherwise pair items with images positionally.
83
+ return items.map((item, i) => ({ ...item, file: item.file || imageFiles[i] }));
84
+ }
85
+
86
+ /**
87
+ * Generate per-item CHIP-0007 metadata + the items.json manifest from the project's images +
88
+ * collection.json (+ optional traits.csv/json). Computes data_hash from the REAL image bytes and
89
+ * metadata_hash from the canonical metadata JSON; if a license has been generated, wires its URI +
90
+ * hash in too.
91
+ *
92
+ * @param {string} root Project directory.
93
+ * @returns {{count:number, manifestPath:string, metadataDir:string}}
94
+ */
95
+ export function generateMetadata(root) {
96
+ const collection = loadCollection(root);
97
+ const imageFiles = listImages(root);
98
+ const items = resolveItems(root, imageFiles);
99
+
100
+ const metadataDir = join(root, "metadata");
101
+ mkdirSync(metadataDir, { recursive: true });
102
+
103
+ // If a license file already exists in licenses/, fold its URI + hash into each item's media.
104
+ const license = readGeneratedLicense(root, collection);
105
+
106
+ const mds = generateItemMetadata(collection, items);
107
+ const manifest = [];
108
+
109
+ for (let i = 0; i < items.length; i++) {
110
+ const item = items[i];
111
+ const md = mds[i];
112
+ const file = item.file || imageFiles[i];
113
+ const imgPath = join(root, "images", file);
114
+ if (!existsSync(imgPath)) throw new Error(`item "${item.name}" references missing image: images/${file}`);
115
+
116
+ const imgBytes = readFileSync(imgPath);
117
+ const dataHash = sha256Hex(imgBytes);
118
+
119
+ const stem = basename(file, extname(file));
120
+ const metaName = `${stem}.json`;
121
+ const metaJson = canonicalJson(md);
122
+ writeFileSync(join(metadataDir, metaName), metaJson + "\n");
123
+ const metadataHash = metadataHashHex(md);
124
+
125
+ const data = capsuleResourceUris({ storeId: PLACEHOLDER_STORE_ID, resource: `images/${file}` });
126
+ const meta = capsuleResourceUris({ storeId: PLACEHOLDER_STORE_ID, resource: `metadata/${metaName}` });
127
+
128
+ const media = {
129
+ data_uris: data.uris,
130
+ data_hash: dataHash,
131
+ metadata_uris: meta.uris,
132
+ metadata_hash: metadataHash,
133
+ license_uris: license ? license.uris : [],
134
+ license_hash: license ? license.hash : null,
135
+ };
136
+
137
+ manifest.push({
138
+ name: md.name,
139
+ description: item.description || undefined,
140
+ attributes: md.attributes || [],
141
+ media,
142
+ });
143
+ }
144
+
145
+ const manifestPath = join(root, "items.json");
146
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
147
+ return { count: items.length, manifestPath, metadataDir };
148
+ }
149
+
150
+ /** Read an already-generated license file (if any) and return its uris + hash, else null. */
151
+ function readGeneratedLicense(root, collection) {
152
+ const id = collection?.license;
153
+ if (!id || !LICENSES[id]) return null;
154
+ const file = licenseFileName(id);
155
+ const path = join(root, "licenses", file);
156
+ if (!existsSync(path)) return null;
157
+ const bytes = readFileSync(path);
158
+ const { uris } = capsuleResourceUris({ storeId: PLACEHOLDER_STORE_ID, resource: `licenses/${file}` });
159
+ return { id, file, hash: sha256Hex(bytes), uris };
160
+ }
161
+
162
+ /**
163
+ * Write the license chosen in collection.json (`license` field) into licenses/, returning its id,
164
+ * file name, and computed hash. Re-run `generateMetadata` afterwards to wire the license URI + hash
165
+ * into the manifest.
166
+ *
167
+ * @param {string} root Project directory.
168
+ * @returns {{id:string, file:string, hash:string, path:string}}
169
+ */
170
+ export function generateLicense(root) {
171
+ const collection = loadCollection(root);
172
+ const id = collection?.license;
173
+ if (!id) throw new Error('collection.json has no `license` field — set one (e.g. "cc0").');
174
+ if (!LICENSES[id]) {
175
+ throw new Error(`Unknown license "${id}". Available: ${Object.keys(LICENSES).join(", ")}.`);
176
+ }
177
+ const holder = collection.creator || collection.name || "the creator";
178
+ const year = new Date().getFullYear();
179
+ const text = licenseText(id, { holder, year });
180
+
181
+ const licensesDir = join(root, "licenses");
182
+ mkdirSync(licensesDir, { recursive: true });
183
+ const file = licenseFileName(id);
184
+ const path = join(licensesDir, file);
185
+ writeFileSync(path, text);
186
+ return { id, file, hash: sha256Hex(Buffer.from(text, "utf8")), path };
187
+ }
188
+
189
+ /**
190
+ * Validate every generated metadata file + the manifest: each doc must be schema-valid CHIP-0007,
191
+ * its on-disk JSON must be canonical, and the manifest's data_hash / metadata_hash / license_hash
192
+ * must agree with the REAL bytes (image, metadata JSON, license file). Mirrors digstore's
193
+ * `validate_uri_hash` pre-mint check.
194
+ *
195
+ * @param {string} root Project directory.
196
+ * @returns {{ok:true, checked:number}}
197
+ * @throws {Error} on the first failure (schema, non-canonical JSON, or hash disagreement).
198
+ */
199
+ export function validateProject(root) {
200
+ const manifestPath = join(root, "items.json");
201
+ if (!existsSync(manifestPath)) {
202
+ throw new Error("items.json not found — run the metadata generator first.");
203
+ }
204
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
205
+ if (!Array.isArray(manifest) || manifest.length === 0) {
206
+ throw new Error("items.json is empty — nothing to validate.");
207
+ }
208
+
209
+ const metadataDir = join(root, "metadata");
210
+ const metaFiles = existsSync(metadataDir)
211
+ ? readdirSync(metadataDir).filter((f) => f.endsWith(".json")).sort()
212
+ : [];
213
+
214
+ // Validate each on-disk metadata doc: schema + canonical-JSON stability.
215
+ for (const f of metaFiles) {
216
+ const raw = readFileSync(join(metadataDir, f), "utf8");
217
+ const md = JSON.parse(raw);
218
+ validateMetadata(md);
219
+ if (raw.trim() !== canonicalJson(md)) {
220
+ throw new Error(`metadata/${f} is not in canonical form (re-run the metadata generator).`);
221
+ }
222
+ }
223
+
224
+ // Validate each manifest item's URI/hash agreement against the real bytes.
225
+ let checked = 0;
226
+ for (const item of manifest) {
227
+ const media = item?.media ?? {};
228
+ // metadata_hash agrees with the matching metadata doc (by name match on data_uris resource).
229
+ const md = buildChip0007Metadata({ name: item.name, attributes: item.attributes });
230
+ // We re-derive only when no explicit doc; the on-disk docs were already validated above.
231
+ if (media.metadata_hash != null) {
232
+ // Find the metadata file whose canonical hash matches; otherwise verify against the rebuilt doc.
233
+ const matches = metaFiles.some(
234
+ (f) => metadataHashHex(JSON.parse(readFileSync(join(metadataDir, f), "utf8"))) === media.metadata_hash,
235
+ );
236
+ if (!matches && metadataHashHex(md) !== media.metadata_hash) {
237
+ throw new Error(`item "${item.name}": metadata_hash does not match any metadata/*.json`);
238
+ }
239
+ }
240
+ // data_hash agrees with the real image bytes (resolve the image path from the dig:// URN tail).
241
+ const imgFile = resourceTail(media.data_uris?.[0]);
242
+ if (imgFile && media.data_hash != null) {
243
+ const imgPath = join(root, imgFile);
244
+ if (!existsSync(imgPath)) throw new Error(`item "${item.name}": image not found at ${imgFile}`);
245
+ validateMetadata(md, { media: { data_hash: media.data_hash, data_bytes: readFileSync(imgPath) } });
246
+ }
247
+ // license_hash agrees with the real license bytes.
248
+ const licFile = resourceTail(media.license_uris?.[0]);
249
+ if (licFile && media.license_hash != null) {
250
+ const licPath = join(root, licFile);
251
+ if (!existsSync(licPath)) throw new Error(`item "${item.name}": license not found at ${licFile}`);
252
+ validateMetadata(md, { license: { license_hash: media.license_hash, license_bytes: readFileSync(licPath) } });
253
+ }
254
+ checked++;
255
+ }
256
+
257
+ return { ok: true, checked };
258
+ }
259
+
260
+ /** Extract the resource path (everything after the first `/`) from a `dig://urn:…/<resource>` URI. */
261
+ function resourceTail(uri) {
262
+ if (!uri || typeof uri !== "string") return null;
263
+ const i = uri.indexOf("/", uri.indexOf("://") + 3);
264
+ return i >= 0 ? uri.slice(i + 1) : null;
265
+ }