create-addfox-app 0.1.1-beta.7 → 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 (160) hide show
  1. package/README.md +18 -18
  2. package/dist/cli.js +259 -102
  3. package/package.json +9 -8
  4. package/templates/README.md +3 -0
  5. package/templates/template-preact-js/AGENTS.md +54 -0
  6. package/templates/template-preact-js/addfox.config.js +27 -0
  7. package/templates/template-preact-js/app/background/index.js +8 -0
  8. package/templates/template-preact-js/app/content/index.js +1 -0
  9. package/templates/template-preact-js/app/options/App.jsx +5 -0
  10. package/templates/template-preact-js/app/options/index.jsx +5 -0
  11. package/templates/template-preact-js/app/popup/App.jsx +31 -0
  12. package/templates/template-preact-js/app/popup/index.jsx +5 -0
  13. package/templates/template-preact-js/app/sidepanel/App.jsx +5 -0
  14. package/templates/template-preact-js/app/sidepanel/index.jsx +5 -0
  15. package/templates/template-preact-js/package.json +18 -0
  16. package/templates/template-preact-js/public/icons/icon_128.png +0 -0
  17. package/templates/template-preact-js/public/welcome.html +20 -0
  18. package/templates/template-preact-ts/AGENTS.md +61 -0
  19. package/templates/template-preact-ts/addfox.config.ts +27 -0
  20. package/templates/template-preact-ts/app/background/index.ts +8 -0
  21. package/templates/template-preact-ts/app/content/index.ts +1 -0
  22. package/templates/template-preact-ts/app/options/App.tsx +5 -0
  23. package/templates/template-preact-ts/app/options/index.tsx +5 -0
  24. package/templates/template-preact-ts/app/popup/App.tsx +31 -0
  25. package/templates/template-preact-ts/app/popup/index.tsx +5 -0
  26. package/templates/template-preact-ts/app/sidepanel/App.tsx +5 -0
  27. package/templates/template-preact-ts/app/sidepanel/index.tsx +5 -0
  28. package/templates/template-preact-ts/package.json +20 -0
  29. package/templates/template-preact-ts/public/icons/icon_128.png +0 -0
  30. package/templates/template-preact-ts/public/welcome.html +20 -0
  31. package/templates/template-preact-ts/tsconfig.json +19 -0
  32. package/templates/template-react-js/AGENTS.md +66 -0
  33. package/templates/template-react-js/addfox.config.js +27 -0
  34. package/templates/template-react-js/app/background/index.js +8 -0
  35. package/templates/template-react-js/app/content/index.js +1 -0
  36. package/templates/template-react-js/app/options/App.jsx +3 -0
  37. package/templates/template-react-js/app/options/index.jsx +5 -0
  38. package/templates/template-react-js/app/popup/App.jsx +29 -0
  39. package/templates/template-react-js/app/popup/index.jsx +5 -0
  40. package/templates/template-react-js/app/sidepanel/App.jsx +3 -0
  41. package/templates/template-react-js/app/sidepanel/index.jsx +5 -0
  42. package/templates/template-react-js/package.json +19 -0
  43. package/templates/template-react-js/public/icons/icon_128.png +0 -0
  44. package/templates/template-react-js/public/welcome.html +20 -0
  45. package/templates/template-react-ts/AGENTS.md +93 -0
  46. package/templates/template-react-ts/addfox.config.ts +27 -0
  47. package/templates/template-react-ts/app/background/index.ts +8 -0
  48. package/templates/template-react-ts/app/content/index.ts +1 -0
  49. package/templates/template-react-ts/app/options/App.tsx +3 -0
  50. package/templates/template-react-ts/app/options/index.tsx +5 -0
  51. package/templates/template-react-ts/app/popup/App.tsx +29 -0
  52. package/templates/template-react-ts/app/popup/index.tsx +5 -0
  53. package/templates/template-react-ts/app/sidepanel/App.tsx +3 -0
  54. package/templates/template-react-ts/app/sidepanel/index.tsx +5 -0
  55. package/templates/template-react-ts/package.json +23 -0
  56. package/templates/template-react-ts/public/icons/icon_128.png +0 -0
  57. package/templates/template-react-ts/public/welcome.html +20 -0
  58. package/templates/template-react-ts/tsconfig.json +19 -0
  59. package/templates/template-solid-js/AGENTS.md +54 -0
  60. package/templates/template-solid-js/addfox.config.js +28 -0
  61. package/templates/template-solid-js/app/background/index.js +8 -0
  62. package/templates/template-solid-js/app/content/index.js +1 -0
  63. package/templates/template-solid-js/app/options/App.jsx +3 -0
  64. package/templates/template-solid-js/app/options/index.jsx +5 -0
  65. package/templates/template-solid-js/app/popup/App.jsx +34 -0
  66. package/templates/template-solid-js/app/popup/index.jsx +5 -0
  67. package/templates/template-solid-js/app/sidepanel/App.jsx +3 -0
  68. package/templates/template-solid-js/app/sidepanel/index.jsx +5 -0
  69. package/templates/template-solid-js/package.json +19 -0
  70. package/templates/template-solid-js/public/icons/icon_128.png +0 -0
  71. package/templates/template-solid-js/public/welcome.html +20 -0
  72. package/templates/template-solid-ts/AGENTS.md +61 -0
  73. package/templates/template-solid-ts/addfox.config.ts +28 -0
  74. package/templates/template-solid-ts/app/background/index.ts +8 -0
  75. package/templates/template-solid-ts/app/content/index.ts +1 -0
  76. package/templates/template-solid-ts/app/options/App.tsx +3 -0
  77. package/templates/template-solid-ts/app/options/index.tsx +5 -0
  78. package/templates/template-solid-ts/app/popup/App.tsx +34 -0
  79. package/templates/template-solid-ts/app/popup/index.tsx +5 -0
  80. package/templates/template-solid-ts/app/sidepanel/App.tsx +3 -0
  81. package/templates/template-solid-ts/app/sidepanel/index.tsx +5 -0
  82. package/templates/template-solid-ts/package.json +21 -0
  83. package/templates/template-solid-ts/public/icons/icon_128.png +0 -0
  84. package/templates/template-solid-ts/public/welcome.html +20 -0
  85. package/templates/template-solid-ts/tsconfig.json +19 -0
  86. package/templates/template-svelte-js/AGENTS.md +54 -0
  87. package/templates/template-svelte-js/addfox.config.js +27 -0
  88. package/templates/template-svelte-js/app/background/index.js +8 -0
  89. package/templates/template-svelte-js/app/content/index.js +1 -0
  90. package/templates/template-svelte-js/app/options/App.svelte +4 -0
  91. package/templates/template-svelte-js/app/options/index.js +4 -0
  92. package/templates/template-svelte-js/app/popup/App.svelte +30 -0
  93. package/templates/template-svelte-js/app/popup/index.js +4 -0
  94. package/templates/template-svelte-js/app/sidepanel/App.svelte +4 -0
  95. package/templates/template-svelte-js/app/sidepanel/index.js +4 -0
  96. package/templates/template-svelte-js/package.json +18 -0
  97. package/templates/template-svelte-js/public/icons/icon_128.png +0 -0
  98. package/templates/template-svelte-js/public/welcome.html +20 -0
  99. package/templates/template-svelte-ts/AGENTS.md +61 -0
  100. package/templates/template-svelte-ts/addfox.config.ts +27 -0
  101. package/templates/template-svelte-ts/app/background/index.ts +8 -0
  102. package/templates/template-svelte-ts/app/content/index.ts +1 -0
  103. package/templates/template-svelte-ts/app/options/App.svelte +4 -0
  104. package/templates/template-svelte-ts/app/options/index.ts +4 -0
  105. package/templates/template-svelte-ts/app/popup/App.svelte +30 -0
  106. package/templates/template-svelte-ts/app/popup/index.ts +4 -0
  107. package/templates/template-svelte-ts/app/sidepanel/App.svelte +4 -0
  108. package/templates/template-svelte-ts/app/sidepanel/index.ts +4 -0
  109. package/templates/template-svelte-ts/package.json +20 -0
  110. package/templates/template-svelte-ts/public/icons/icon_128.png +0 -0
  111. package/templates/template-svelte-ts/public/welcome.html +20 -0
  112. package/templates/template-svelte-ts/tsconfig.json +19 -0
  113. package/templates/template-vanilla-js/AGENTS.md +52 -0
  114. package/templates/template-vanilla-js/addfox.config.js +25 -0
  115. package/templates/template-vanilla-js/app/background/index.js +8 -0
  116. package/templates/template-vanilla-js/app/content/index.js +1 -0
  117. package/templates/template-vanilla-js/app/options/index.js +2 -0
  118. package/templates/template-vanilla-js/app/popup/index.js +48 -0
  119. package/templates/template-vanilla-js/app/sidepanel/index.js +2 -0
  120. package/templates/template-vanilla-js/package.json +13 -0
  121. package/templates/template-vanilla-js/public/icons/icon_128.png +0 -0
  122. package/templates/template-vanilla-js/public/welcome.html +20 -0
  123. package/templates/template-vanilla-ts/AGENTS.md +59 -0
  124. package/templates/template-vanilla-ts/addfox.config.ts +25 -0
  125. package/templates/template-vanilla-ts/app/background/index.ts +8 -0
  126. package/templates/template-vanilla-ts/app/content/index.ts +1 -0
  127. package/templates/template-vanilla-ts/app/options/index.ts +2 -0
  128. package/templates/template-vanilla-ts/app/popup/index.ts +48 -0
  129. package/templates/template-vanilla-ts/app/sidepanel/index.ts +2 -0
  130. package/templates/template-vanilla-ts/package.json +14 -0
  131. package/templates/template-vanilla-ts/public/icons/icon_128.png +0 -0
  132. package/templates/template-vanilla-ts/public/welcome.html +20 -0
  133. package/templates/template-vanilla-ts/tsconfig.json +17 -0
  134. package/templates/template-vue-js/AGENTS.md +60 -0
  135. package/templates/template-vue-js/addfox.config.js +27 -0
  136. package/templates/template-vue-js/app/background/index.js +8 -0
  137. package/templates/template-vue-js/app/content/index.js +1 -0
  138. package/templates/template-vue-js/app/options/App.vue +6 -0
  139. package/templates/template-vue-js/app/options/index.js +5 -0
  140. package/templates/template-vue-js/app/popup/App.vue +34 -0
  141. package/templates/template-vue-js/app/popup/index.js +5 -0
  142. package/templates/template-vue-js/app/sidepanel/App.vue +6 -0
  143. package/templates/template-vue-js/app/sidepanel/index.js +5 -0
  144. package/templates/template-vue-js/package.json +18 -0
  145. package/templates/template-vue-js/public/icons/icon_128.png +0 -0
  146. package/templates/template-vue-js/public/welcome.html +20 -0
  147. package/templates/template-vue-ts/AGENTS.md +65 -0
  148. package/templates/template-vue-ts/addfox.config.ts +27 -0
  149. package/templates/template-vue-ts/app/background/index.ts +8 -0
  150. package/templates/template-vue-ts/app/content/index.ts +1 -0
  151. package/templates/template-vue-ts/app/options/App.vue +6 -0
  152. package/templates/template-vue-ts/app/options/index.ts +5 -0
  153. package/templates/template-vue-ts/app/popup/App.vue +32 -0
  154. package/templates/template-vue-ts/app/popup/index.ts +5 -0
  155. package/templates/template-vue-ts/app/sidepanel/App.vue +6 -0
  156. package/templates/template-vue-ts/app/sidepanel/index.ts +5 -0
  157. package/templates/template-vue-ts/package.json +20 -0
  158. package/templates/template-vue-ts/public/icons/icon_128.png +0 -0
  159. package/templates/template-vue-ts/public/welcome.html +20 -0
  160. package/templates/template-vue-ts/tsconfig.json +19 -0
package/README.md CHANGED
@@ -1,18 +1,18 @@
1
- <p align="center">
2
- <img width="200" src="https://raw.githubusercontent.com/addfox/addfox/main/addfox.png" alt="Addfox">
3
- </p>
4
-
5
- # create-addfox-app
6
-
7
- [中文](README-zh_CN.md) | English
8
-
9
- ---
10
-
11
- Interactive scaffolder: generates a addfox-based extension project from options (template, package manager, entries, skills).
12
-
13
- - Commands: `create-addfox-app` or `pnpm create addfox-app` (and npm/yarn/bun equivalents)
14
- - Flow: (1) select framework (vanilla / vue / react / preact / svelte / solid), (2) language (TypeScript / JavaScript), (3) package manager (pnpm / npm / yarn / bun), (4) entries to include (multi-select), (5) whether to install addfox skills (yes/no). Output to cwd or a given directory. Generated project uses **addfox.config.ts** or **addfox.config.js** with minimal manifest (entry discovery; no built-in entry paths in manifest).
15
-
16
- ## Templates
17
-
18
- Scaffold templates live at **repo root** in **`templates/`** (e.g. `template-vanilla-ts`, `template-react-ts`). The CLI downloads the chosen template from the GitHub repo tarball, so the repo must have `templates/` at root and committed. The npm package does not bundle templates; it fetches from GitHub at scaffold time.
1
+ <p align="center">
2
+ <img width="200" src="https://raw.githubusercontent.com/addfox/addfox/main/addfox.png" alt="Addfox">
3
+ </p>
4
+
5
+ # create-addfox-app
6
+
7
+ [中文](README-zh_CN.md) | English
8
+
9
+ ---
10
+
11
+ Interactive scaffolder: generates a addfox-based extension project from options (template, package manager, entries, skills).
12
+
13
+ - Commands: `create-addfox-app` or `pnpm create addfox-app` (and npm/yarn/bun equivalents)
14
+ - Flow: (1) select framework (vanilla / vue / react / preact / svelte / solid), (2) language (TypeScript / JavaScript), (3) package manager (pnpm / npm / yarn / bun), (4) entries to include (multi-select), (5) whether to install addfox skills (yes/no). Output to cwd or a given directory. Generated project uses **addfox.config.ts** or **addfox.config.js** with minimal manifest (entry discovery; no built-in entry paths in manifest).
15
+
16
+ ## Templates
17
+
18
+ Scaffold templates ship inside this package under **`templates/`** (e.g. `template-vanilla-ts`, `template-react-ts`). They are included in the published npm tarball; the CLI copies the chosen folder into your new project (no remote download). The template’s **`addfox.config`** (including `manifest`) is **preserved**; the CLI only **merges** Rsbuild Less/Sass plugins when those style engines are selected. If no config file exists, a full fallback config is generated.
package/dist/cli.js CHANGED
@@ -1,82 +1,165 @@
1
1
  #!/usr/bin/env node
2
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
- import { dirname, join, relative, resolve } from "node:path";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, relative, resolve as external_node_path_resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { createRequire } from "node:module";
6
6
  import { execSync } from "node:child_process";
7
7
  import prompts from "prompts";
8
- import { blue, cyan, dim, gray, green, lightBlue, magenta, red, trueColor, yellow } from "kolorist";
9
- import minimist from "minimist";
10
- import { gunzipSync } from "node:zlib";
8
+ import { cp, mkdir } from "node:fs/promises";
11
9
  import { detectPackageManager, getExecCommand, getInstallCommand, getRunCommand } from "@addfox/pkg-manager";
12
- const GITHUB_REPO = "addfox/addfox";
13
- const TEMPLATE_BASE = "templates";
14
- function getRepoRoot() {
15
- const fromDist = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
16
- return fromDist;
17
- }
18
- function tryLocalTemplates(templateName, destDir) {
19
- const repoRoot = getRepoRoot();
20
- const templatePath = join(repoRoot, TEMPLATE_BASE, templateName);
21
- if (!existsSync(templatePath)) return false;
22
- mkdirSync(destDir, {
23
- recursive: true
24
- });
25
- cpSync(templatePath, destDir, {
26
- recursive: true
27
- });
28
- return true;
10
+ const RESET = "\x1b[0m";
11
+ function color(code) {
12
+ return (s)=>`\x1b[${code}m${s}${RESET}`;
29
13
  }
30
- function parseTarHeader(buf) {
31
- if (buf.every((b)=>0 === b)) return null;
32
- const rawName = buf.subarray(0, 100).toString("utf8").replace(/\0+$/, "");
33
- const sizeStr = buf.subarray(124, 136).toString("utf8").replace(/\0+$/, "").trim();
34
- const type = String.fromCharCode(buf[156]);
35
- const prefix = buf.subarray(345, 500).toString("utf8").replace(/\0+$/, "");
36
- return {
37
- name: prefix ? `${prefix}/${rawName}` : rawName,
38
- size: parseInt(sizeStr, 8) || 0,
39
- type
40
- };
14
+ color(30);
15
+ const red = color(31);
16
+ const green = color(32);
17
+ const yellow = color(33);
18
+ const blue = color(34);
19
+ const magenta = color(35);
20
+ const cyan = color(36);
21
+ color(37);
22
+ const gray = color(90);
23
+ color(91);
24
+ color(92);
25
+ color(93);
26
+ const lightBlue = color(94);
27
+ color(95);
28
+ color(96);
29
+ color(97);
30
+ const dim = color(2);
31
+ function trueColor(r, g, b) {
32
+ return (s)=>`\x1b[38;2;${r};${g};${b}m${s}${RESET}`;
41
33
  }
42
- function extractMatchingFiles(tarBuffer, templatePrefix, destDir) {
43
- let offset = 0;
44
- while(offset + 512 <= tarBuffer.length){
45
- const header = parseTarHeader(tarBuffer.subarray(offset, offset + 512));
46
- offset += 512;
47
- if (!header) break;
48
- const dataBlocks = 512 * Math.ceil(header.size / 512);
49
- const idx = header.name.indexOf(templatePrefix);
50
- if (-1 !== idx) {
51
- const relativePath = header.name.substring(idx + templatePrefix.length).replace(/^\//, "");
52
- if (relativePath) {
53
- const destPath = join(destDir, relativePath);
54
- const isDir = "5" === header.type || relativePath.endsWith("/");
55
- if (isDir) mkdirSync(destPath, {
56
- recursive: true
57
- });
58
- else if ("0" === header.type || "\0" === header.type) {
59
- mkdirSync(dirname(destPath), {
60
- recursive: true
61
- });
62
- writeFileSync(destPath, tarBuffer.subarray(offset, offset + header.size));
34
+ function parseArgv(argv) {
35
+ const result = {
36
+ _: []
37
+ };
38
+ for(let i = 0; i < argv.length; i++){
39
+ const arg = argv[i];
40
+ if ("--" === arg) {
41
+ result._.push(...argv.slice(i + 1));
42
+ break;
43
+ }
44
+ if (arg.startsWith("--")) {
45
+ const eqIdx = arg.indexOf("=");
46
+ let key;
47
+ let value;
48
+ if (eqIdx >= 0) {
49
+ key = arg.slice(2, eqIdx);
50
+ value = arg.slice(eqIdx + 1);
51
+ } else {
52
+ key = arg.slice(2);
53
+ const next = argv[i + 1];
54
+ if (null == next || next.startsWith("-")) value = true;
55
+ else {
56
+ value = next;
57
+ i++;
63
58
  }
64
59
  }
60
+ const num = Number(value);
61
+ if (!isNaN(num) && String(value) === String(num)) value = num;
62
+ result[key] = value;
63
+ continue;
65
64
  }
66
- offset += dataBlocks;
65
+ if (arg.startsWith("-") && arg.length > 1) {
66
+ const chars = arg.slice(1).split("");
67
+ for(let j = 0; j < chars.length; j++){
68
+ const key = chars[j];
69
+ const next = argv[i + 1];
70
+ if (j !== chars.length - 1 || null == next || next.startsWith("-")) result[key] = true;
71
+ else {
72
+ const num = Number(next);
73
+ result[key] = isNaN(num) || String(next) !== String(num) ? next : num;
74
+ i++;
75
+ }
76
+ }
77
+ continue;
78
+ }
79
+ const num = Number(arg);
80
+ result._.push(isNaN(num) || String(arg) !== String(num) ? arg : num);
67
81
  }
82
+ return result;
83
+ }
84
+ const TEMPLATE_BASE = "templates";
85
+ function getPackageRoot() {
86
+ return external_node_path_resolve(dirname(fileURLToPath(import.meta.url)), "..");
68
87
  }
69
- async function downloadTemplate(templateName, destDir, branch = "main") {
70
- const url = `https://codeload.github.com/${GITHUB_REPO}/tar.gz/${branch}`;
71
- const templatePrefix = `${TEMPLATE_BASE}/${templateName}/`;
72
- const response = await fetch(url);
73
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
74
- const compressed = Buffer.from(await response.arrayBuffer());
75
- const tarBuffer = gunzipSync(compressed);
76
- mkdirSync(destDir, {
88
+ function getBundledTemplatesDir() {
89
+ return join(getPackageRoot(), TEMPLATE_BASE);
90
+ }
91
+ function hasLocalTemplate(templateName) {
92
+ return existsSync(join(getBundledTemplatesDir(), templateName));
93
+ }
94
+ const SKIP_RELATIVE_SEGMENTS = new Set([
95
+ "node_modules",
96
+ ".git",
97
+ ".pnpm"
98
+ ]);
99
+ function shouldCopyLocalTemplatePath(src, templateRoot) {
100
+ const root = external_node_path_resolve(templateRoot);
101
+ const rel = relative(root, external_node_path_resolve(src));
102
+ if (rel.startsWith("..")) return true;
103
+ for (const part of rel.split(/[/\\]/))if (SKIP_RELATIVE_SEGMENTS.has(part)) return false;
104
+ return true;
105
+ }
106
+ async function copyBundledTemplate(templateName, destDir) {
107
+ const templatePath = join(getBundledTemplatesDir(), templateName);
108
+ if (!existsSync(templatePath)) throw new Error(`Bundled template "${templateName}" not found under create-addfox-app. Reinstall the package.`);
109
+ await mkdir(destDir, {
77
110
  recursive: true
78
111
  });
79
- extractMatchingFiles(tarBuffer, templatePrefix, destDir);
112
+ const resolvedTemplate = external_node_path_resolve(templatePath);
113
+ await cp(resolvedTemplate, destDir, {
114
+ recursive: true,
115
+ filter: (src)=>shouldCopyLocalTemplatePath(src, resolvedTemplate)
116
+ });
117
+ }
118
+ const FRAMES = [
119
+ "⠋",
120
+ "⠙",
121
+ "⠹",
122
+ "⠸",
123
+ "⠼",
124
+ "⠴",
125
+ "⠦",
126
+ "⠧",
127
+ "⠇",
128
+ "⠏"
129
+ ];
130
+ const INTERVAL_MS = 80;
131
+ function clearSpinnerLine() {
132
+ process.stdout.write("\r\x1b[K");
133
+ }
134
+ function sleep(ms) {
135
+ return new Promise((resolve)=>{
136
+ setTimeout(resolve, ms);
137
+ });
138
+ }
139
+ async function runWithTemplateSpinner(label, fn, options) {
140
+ const minVisibleMs = options?.minVisibleMs ?? 0;
141
+ if (!process.stdout.isTTY) {
142
+ console.log(yellow(`\n ${label}\n`));
143
+ return fn();
144
+ }
145
+ let frameIndex = 0;
146
+ process.stdout.write("\n");
147
+ const timer = setInterval(()=>{
148
+ const frame = FRAMES[frameIndex % FRAMES.length];
149
+ frameIndex += 1;
150
+ process.stdout.write(`\r ${yellow(frame)} ${label}`);
151
+ }, INTERVAL_MS);
152
+ const startedAt = Date.now();
153
+ try {
154
+ const result = await fn();
155
+ const elapsed = Date.now() - startedAt;
156
+ if (elapsed < minVisibleMs) await sleep(minVisibleMs - elapsed);
157
+ return result;
158
+ } finally{
159
+ clearInterval(timer);
160
+ clearSpinnerLine();
161
+ process.stdout.write("\n");
162
+ }
80
163
  }
81
164
  const STYLE_ENGINES = [
82
165
  {
@@ -233,7 +316,7 @@ function getStylePlugin(engine) {
233
316
  };
234
317
  return null;
235
318
  }
236
- const MINIMAL_MANIFEST = ' name: "My Extension",\n version: "1.0.0",\n manifest_version: 3,\n description: "Browser extension built with addfox",\n permissions: ["storage", "activeTab"],';
319
+ const MINIMAL_MANIFEST = ' name: "My Extension",\n version: "1.0.0",\n manifest_version: 3,\n description: "Browser extension built with addfox",\n permissions: ["activeTab"],\n icons: {\n "16": "icons/icon_128.png",\n "48": "icons/icon_128.png",\n "128": "icons/icon_128.png",\n },\n action: {\n default_icon: {\n "16": "icons/icon_128.png",\n "48": "icons/icon_128.png",\n "128": "icons/icon_128.png",\n },\n },';
237
320
  function generateAddfoxConfig(framework, _language, styleEngine) {
238
321
  const importLines = [];
239
322
  const fwImport = getFrameworkPluginImport(framework);
@@ -261,6 +344,67 @@ function generateAddfoxConfig(framework, _language, styleEngine) {
261
344
  ];
262
345
  return parts.filter(Boolean).join("\n");
263
346
  }
347
+ const MANIFEST_LINE = " manifest: { chromium: manifest, firefox: { ...manifest } },";
348
+ function findMatchingArrayClose(source, openBracketIdx) {
349
+ if ("[" !== source[openBracketIdx]) return -1;
350
+ let depth = 0;
351
+ for(let i = openBracketIdx; i < source.length; i++){
352
+ const c = source[i];
353
+ if ("[" === c) depth++;
354
+ else if ("]" === c) {
355
+ depth--;
356
+ if (0 === depth) return i;
357
+ }
358
+ }
359
+ return -1;
360
+ }
361
+ function hasImportFromModule(source, modulePath) {
362
+ return source.includes(`from "${modulePath}"`) || source.includes(`from '${modulePath}'`) || source.includes(`from \`${modulePath}\``);
363
+ }
364
+ function insertImportAfterLastImport(source, importLine) {
365
+ const trimmed = importLine.trim();
366
+ const moduleMatch = /from\s+["']([^"']+)["']/.exec(trimmed);
367
+ if (moduleMatch && hasImportFromModule(source, moduleMatch[1])) return source;
368
+ const lines = source.split("\n");
369
+ let lastImportIdx = -1;
370
+ for(let i = 0; i < lines.length; i++)if (lines[i].trim().startsWith("import ")) lastImportIdx = i;
371
+ if (-1 === lastImportIdx) return `${importLine}\n${source}`;
372
+ const next = [
373
+ ...lines
374
+ ];
375
+ next.splice(lastImportIdx + 1, 0, importLine);
376
+ return next.join("\n");
377
+ }
378
+ function pluginCallPresent(inner, call) {
379
+ const fn = call.replace(/\(\)$/, "");
380
+ return inner.includes(`${fn}(`);
381
+ }
382
+ function mergeCallsIntoPluginsArray(source, calls) {
383
+ const pluginsIdx = source.indexOf("plugins:");
384
+ if (-1 === pluginsIdx) return addPluginsBlockAfterManifest(source, calls);
385
+ const bracketStart = source.indexOf("[", pluginsIdx);
386
+ if (-1 === bracketStart) return source;
387
+ const bracketEnd = findMatchingArrayClose(source, bracketStart);
388
+ if (-1 === bracketEnd) return source;
389
+ let inner = source.slice(bracketStart + 1, bracketEnd).trim();
390
+ for (const c of calls)if (!pluginCallPresent(inner, c)) inner = inner ? `${inner}, ${c}` : c;
391
+ return source.slice(0, bracketStart + 1) + inner + source.slice(bracketEnd);
392
+ }
393
+ function addPluginsBlockAfterManifest(source, calls) {
394
+ const idx = source.indexOf(MANIFEST_LINE);
395
+ if (-1 === idx) return source;
396
+ const insertPos = idx + MANIFEST_LINE.length;
397
+ return `${source.slice(0, insertPos)}\n plugins: [${calls.join(", ")}],${source.slice(insertPos)}`;
398
+ }
399
+ function mergeScaffoldIntoAddfoxConfig(source, styleEngine) {
400
+ const st = getStylePlugin(styleEngine);
401
+ if (!st) return source;
402
+ let out = insertImportAfterLastImport(source, st.importLine);
403
+ out = mergeCallsIntoPluginsArray(out, [
404
+ st.call
405
+ ]);
406
+ return out;
407
+ }
264
408
  const UTF8_BOM = "\uFEFF";
265
409
  function stripUtf8Bom(text) {
266
410
  return text.startsWith(UTF8_BOM) ? text.slice(UTF8_BOM.length) : text;
@@ -346,10 +490,10 @@ function generateRstestConfig(language, testKinds) {
346
490
  }
347
491
  function writeUnitSample(root, language) {
348
492
  const ext = getTestExt(language);
349
- mkdirSync(resolve(root, "__tests__"), {
493
+ mkdirSync(external_node_path_resolve(root, "__tests__"), {
350
494
  recursive: true
351
495
  });
352
- const path = resolve(root, "__tests__", `example.test.${ext}`);
496
+ const path = external_node_path_resolve(root, "__tests__", `example.test.${ext}`);
353
497
  const body = "ts" === language ? `import { describe, expect, it } from "@rstest/core";
354
498
 
355
499
  describe("example unit tests", () => {
@@ -369,11 +513,11 @@ describe("example unit tests", () => {
369
513
  }
370
514
  function writeE2eSample(root, language) {
371
515
  const ext = getTestExt(language);
372
- const dir = resolve(root, "__tests__", "e2e");
516
+ const dir = external_node_path_resolve(root, "__tests__", "e2e");
373
517
  mkdirSync(dir, {
374
518
  recursive: true
375
519
  });
376
- const path = resolve(dir, `example.browser.test.${ext}`);
520
+ const path = external_node_path_resolve(dir, `example.browser.test.${ext}`);
377
521
  const body = `import { describe, expect, it } from "@rstest/core";
378
522
  import { page } from "@rstest/browser";
379
523
 
@@ -410,7 +554,7 @@ function mergeScripts(pkg, testKinds) {
410
554
  function applyTestAndReportSetup(root, language, selection) {
411
555
  const { testKinds, installRsdoctor } = selection;
412
556
  if (0 === testKinds.length && !installRsdoctor) return;
413
- const pkgPath = resolve(root, "package.json");
557
+ const pkgPath = external_node_path_resolve(root, "package.json");
414
558
  const pkg = readJsonFile(pkgPath);
415
559
  mergeDevDeps(pkg, testKinds, installRsdoctor);
416
560
  mergeScripts(pkg, testKinds);
@@ -418,22 +562,12 @@ function applyTestAndReportSetup(root, language, selection) {
418
562
  if (testKinds.length > 0) {
419
563
  const configName = "ts" === language ? "rstest.config.ts" : "rstest.config.js";
420
564
  const content = generateRstestConfig(language, testKinds);
421
- writeFileSync(resolve(root, configName), content, "utf-8");
565
+ writeFileSync(external_node_path_resolve(root, configName), content, "utf-8");
422
566
  if (testKinds.includes("unit")) writeUnitSample(root, language);
423
567
  if (testKinds.includes("e2e")) writeE2eSample(root, language);
424
568
  }
425
569
  }
426
- const RESET = "\x1b[0m";
427
- const ORANGE = [
428
- 255,
429
- 118,
430
- 38
431
- ];
432
- const PINK = [
433
- 255,
434
- 92,
435
- 186
436
- ];
570
+ const logo_RESET = "\x1b[0m";
437
571
  const LOGO_LINES = [
438
572
  " █████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗",
439
573
  "██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔═══██╗╚██╗██╔╝",
@@ -442,13 +576,15 @@ const LOGO_LINES = [
442
576
  "██║ ██║██████╔╝██████╔╝██║ ╚██████╔╝██╔╝ ██╗",
443
577
  "╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝"
444
578
  ];
445
- function lerpChannel(a, b, t) {
446
- return Math.round(a + (b - a) * t);
579
+ const GRADIENT_START_256 = 208;
580
+ const GRADIENT_END_256 = 213;
581
+ function shouldEmitAnsiColor() {
582
+ return !process.env.NO_COLOR;
447
583
  }
448
- function rgbFg(r, g, b) {
449
- return `\x1b[38;2;${r};${g};${b}m`;
584
+ function ansi256Fg(code) {
585
+ return `\x1b[38;5;${code}m`;
450
586
  }
451
- function colorizeLine(line) {
587
+ function colorizeLineAnsi256(line) {
452
588
  const len = line.length;
453
589
  if (0 === len) return "";
454
590
  let out = "";
@@ -459,16 +595,20 @@ function colorizeLine(line) {
459
595
  continue;
460
596
  }
461
597
  const t = len <= 1 ? 0 : i / (len - 1);
462
- const r = lerpChannel(ORANGE[0], PINK[0], t);
463
- const g = lerpChannel(ORANGE[1], PINK[1], t);
464
- const b = lerpChannel(ORANGE[2], PINK[2], t);
465
- out += rgbFg(r, g, b) + ch;
598
+ const code = Math.round(GRADIENT_START_256 + t * (GRADIENT_END_256 - GRADIENT_START_256));
599
+ out += ansi256Fg(code) + ch;
466
600
  }
467
- return out + RESET;
601
+ return out + logo_RESET;
602
+ }
603
+ function printPlainLogo() {
604
+ console.log("");
605
+ for (const line of LOGO_LINES)console.log(line);
606
+ console.log("");
468
607
  }
469
608
  function printAddfoxLogo() {
609
+ if (!shouldEmitAnsiColor()) return void printPlainLogo();
470
610
  console.log("");
471
- for (const line of LOGO_LINES)console.log(colorizeLine(line));
611
+ for (const line of LOGO_LINES)console.log(colorizeLineAnsi256(line));
472
612
  console.log("");
473
613
  }
474
614
  const SKIP_STYLE_ENTRIES = new Set([
@@ -671,7 +811,7 @@ function getStyleEngineChoicesColored() {
671
811
  }
672
812
  function getVersion() {
673
813
  try {
674
- const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
814
+ const pkgPath = external_node_path_resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
675
815
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
676
816
  return pkg.version ?? "0.0.0";
677
817
  } catch {
@@ -841,14 +981,14 @@ function resolveTestSelectionFromArgv(argv) {
841
981
  };
842
982
  }
843
983
  function updatePackageName(destDir, projectName) {
844
- const pkgPath = resolve(destDir, "package.json");
984
+ const pkgPath = external_node_path_resolve(destDir, "package.json");
845
985
  if (!existsSync(pkgPath)) return;
846
986
  const pkg = readJsonFile(pkgPath);
847
987
  pkg.name = projectName.replace(/\s+/g, "-").toLowerCase();
848
988
  writeJsonFile(pkgPath, pkg);
849
989
  }
850
990
  async function main() {
851
- const argv = minimist(process.argv.slice(2));
991
+ const argv = parseArgv(process.argv.slice(2));
852
992
  if (argv.help || argv.h) return void printHelp();
853
993
  if (argv.version || argv.v) return void console.log(getVersion());
854
994
  const targetDir = argv._[0] ?? "my-extension";
@@ -856,10 +996,14 @@ async function main() {
856
996
  const cliLanguage = argv.language;
857
997
  printAddfoxLogo();
858
998
  console.log(blue("\n Create Addfox App\n"));
859
- const root = resolve(process.cwd(), targetDir);
999
+ const root = external_node_path_resolve(process.cwd(), targetDir);
860
1000
  if (existsSync(root)) {
861
1001
  const confirmed = await confirmOverwrite(targetDir);
862
1002
  if (!confirmed) process.exit(0);
1003
+ rmSync(root, {
1004
+ recursive: true,
1005
+ force: true
1006
+ });
863
1007
  }
864
1008
  const options = cliFramework && cliLanguage ? {
865
1009
  framework: cliFramework,
@@ -875,11 +1019,20 @@ async function main() {
875
1019
  if (!testSelection) process.exit(0);
876
1020
  const pm = options.packageManager;
877
1021
  const templateName = getTemplateName(options.framework, options.language);
878
- console.log(yellow("\n Downloading template...\n"));
1022
+ const templateReady = hasLocalTemplate(templateName);
1023
+ const templateLabel = "Copying template...";
1024
+ const TEMPLATE_SPINNER_MIN_MS = 800;
879
1025
  try {
880
- tryLocalTemplates(templateName, root) || await downloadTemplate(templateName, root);
1026
+ await runWithTemplateSpinner(templateLabel, async ()=>{
1027
+ await new Promise((resolveSpinner)=>{
1028
+ setImmediate(resolveSpinner);
1029
+ });
1030
+ await copyBundledTemplate(templateName, root);
1031
+ }, templateReady ? {
1032
+ minVisibleMs: TEMPLATE_SPINNER_MIN_MS
1033
+ } : void 0);
881
1034
  } catch (err) {
882
- console.error(red(`\n Failed to download template: ${err.message}\n`));
1035
+ console.error(red(`\n Failed to copy template: ${err.message}\n`));
883
1036
  process.exit(1);
884
1037
  }
885
1038
  const useAllEntries = options.entries.includes("__all__");
@@ -889,8 +1042,12 @@ async function main() {
889
1042
  ] : options.entries.filter((e)=>"__all__" !== e && existingDirs.includes(e));
890
1043
  if (!useAllEntries && effectiveEntries.length > 0) filterAppEntries(root, effectiveEntries);
891
1044
  const configExt = "ts" === options.language ? "ts" : "js";
892
- const configPath = resolve(root, `addfox.config.${configExt}`);
893
- const configContent = generateAddfoxConfig(options.framework, options.language, options.styleEngine);
1045
+ const configPath = external_node_path_resolve(root, `addfox.config.${configExt}`);
1046
+ let configContent;
1047
+ if (existsSync(configPath)) {
1048
+ const existing = readFileSync(configPath, "utf-8");
1049
+ configContent = existing.trim().length > 0 && existing.includes("defineConfig") ? mergeScaffoldIntoAddfoxConfig(existing, options.styleEngine) : generateAddfoxConfig(options.framework, options.language, options.styleEngine);
1050
+ } else configContent = generateAddfoxConfig(options.framework, options.language, options.styleEngine);
894
1051
  writeFileSync(configPath, configContent, "utf-8");
895
1052
  updatePackageName(root, targetDir);
896
1053
  applyStyleEngine(root, options.framework, options.language, options.styleEngine);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-addfox-app",
3
- "version": "0.1.1-beta.7",
3
+ "version": "0.2.0",
4
4
  "description": "Interactive scaffolder for addfox extension projects",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,19 +9,20 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "templates",
12
13
  "LICENSE"
13
14
  ],
14
15
  "dependencies": {
15
- "chalk": "^5.3.0",
16
16
  "prompts": "^2.4.2",
17
- "kolorist": "^1.8.0",
18
- "minimist": "^1.2.8",
19
- "@addfox/pkg-manager": "0.1.1-beta.4"
17
+ "@addfox/pkg-manager": "0.2.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=18.0.0"
20
21
  },
21
22
  "devDependencies": {
22
- "@rslib/core": "^0.20.0",
23
- "@rstest/core": "^0.9.4",
24
- "@rstest/coverage-istanbul": "^0.3.0",
23
+ "@rslib/core": "^0.21.5",
24
+ "@rstest/core": "^0.9.9",
25
+ "@rstest/coverage-istanbul": "^0.3.2",
25
26
  "@types/node": "^20.0.0",
26
27
  "typescript": "^5.0.0"
27
28
  },
@@ -0,0 +1,3 @@
1
+ # Scaffold templates
2
+
3
+ Templates used by **create-addfox-app**. Each subdirectory (e.g. `template-vanilla-ts`, `template-react-ts`) is a full project skeleton. They live under this package and ship with the published npm tarball; the CLI copies the chosen folder into the new project directory.
@@ -0,0 +1,54 @@
1
+ # AI Agent Guide - Preact Addfox Extension
2
+
3
+ > **Priority**: Follow `.agents/skills/` first, then this guide, then [official docs](https://addfox.dev)
4
+
5
+ ## Project Overview
6
+
7
+ Preact browser extension using Addfox build tool. Uses `@rsbuild/plugin-preact` for JSX support.
8
+
9
+ ## Key Configuration Fields (addfox.config.js)
10
+
11
+ ### Required Fields
12
+
13
+ - **`manifest`** - Extension manifest.json content
14
+ - Supports `chromium` and `firefox` variants
15
+ - Addfox auto-injects entry paths
16
+
17
+ - **`plugins`** - Rsbuild plugins array
18
+ - Must include `pluginPreact()` for JSX support
19
+
20
+ ### Optional Fields (use when needed)
21
+
22
+ - **`appDir`** - Source directory (default: "app")
23
+ - **`outDir`** - Build output directory
24
+ - **`entry`** - Manual entry mapping
25
+ - **`hotReload`** - Dev server settings
26
+ - **`debug`** - Enable debug logging
27
+ - **`zip`** - Output zip configuration
28
+ - **`rsbuild`** - Override Rsbuild configuration
29
+
30
+ ## Entry Structure
31
+
32
+ Entries auto-discovered from `appDir` subdirectories. Each needs `index.jsx` as entry.
33
+
34
+ ## Framework-Specific Notes
35
+
36
+ ### Preact
37
+
38
+ - JSX-based (`.jsx` files)
39
+ - React-compatible API with smaller bundle size
40
+
41
+ ### Browser Extension APIs
42
+
43
+ - Install `webextension-polyfill` for cross-browser compatibility
44
+
45
+ ### Styling
46
+
47
+ - Supports CSS Modules
48
+ - Content scripts: use `@addfox/utils` with Shadow DOM
49
+
50
+ ## Resources
51
+
52
+ - Addfox docs: https://addfox.dev
53
+ - Rsbuild Preact plugin: https://rsbuild.dev/plugins/list/plugin-preact
54
+ - Preact docs: https://preactjs.com
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from "addfox";
2
+ import { pluginPreact } from "@rsbuild/plugin-preact";
3
+
4
+ const manifest = {
5
+ name: "My Extension",
6
+ version: "1.0.0",
7
+ manifest_version: 3,
8
+ description: "Browser extension built with addfox",
9
+ permissions: ["activeTab"],
10
+ icons: {
11
+ "16": "icons/icon_128.png",
12
+ "48": "icons/icon_128.png",
13
+ "128": "icons/icon_128.png",
14
+ },
15
+ action: {
16
+ default_icon: {
17
+ "16": "icons/icon_128.png",
18
+ "48": "icons/icon_128.png",
19
+ "128": "icons/icon_128.png",
20
+ },
21
+ },
22
+ };
23
+
24
+ export default defineConfig({
25
+ manifest: { chromium: manifest, firefox: { ...manifest } },
26
+ plugins: [pluginPreact()],
27
+ });
@@ -0,0 +1,8 @@
1
+ function openWelcomePage() {
2
+ chrome.tabs.create({ url: chrome.runtime.getURL("welcome.html") });
3
+ }
4
+
5
+ chrome.runtime.onInstalled.addListener((details) => {
6
+ if (details.reason === "install" || details.reason === "update") openWelcomePage();
7
+ console.log("Extension installed");
8
+ });