create-arsh-tauri 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +55 -2
- package/index.js +239 -0
- package/package.json +43 -4
- package/template/.eslintrc.cjs +18 -0
- package/template/.github/workflows/ci.yml +50 -0
- package/template/AGENTS.md +50 -0
- package/template/README.md +51 -0
- package/template/gitignore +37 -0
- package/template/index.html +12 -0
- package/template/package-lock.json +4454 -0
- package/template/package.json +42 -0
- package/template/src/App.tsx +35 -0
- package/template/src/index.css +8 -0
- package/template/src/lib/tauri.ts +21 -0
- package/template/src/lib/utils.test.ts +17 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/main.tsx +10 -0
- package/template/src/ui/appLayout.tsx +35 -0
- package/template/src/ui/button.tsx +63 -0
- package/template/src/ui/dialog.tsx +71 -0
- package/template/src/ui/index.ts +8 -0
- package/template/src/ui/navItem.tsx +37 -0
- package/template/src/ui/spinner.tsx +33 -0
- package/template/src/ui/styles.css +96 -0
- package/template/src/ui/switch.tsx +77 -0
- package/template/src/ui/titleBar.tsx +157 -0
- package/template/src/ui/tooltip.tsx +141 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/src-tauri/Cargo.lock +4316 -0
- package/template/src-tauri/Cargo.toml +26 -0
- package/template/src-tauri/build.rs +3 -0
- package/template/src-tauri/capabilities/default.json +16 -0
- package/template/src-tauri/icons/128x128.png +0 -0
- package/template/src-tauri/icons/128x128@2x.png +0 -0
- package/template/src-tauri/icons/32x32.png +0 -0
- package/template/src-tauri/icons/icon.icns +0 -0
- package/template/src-tauri/icons/icon.ico +0 -0
- package/template/src-tauri/src/lib.rs +14 -0
- package/template/src-tauri/src/main.rs +6 -0
- package/template/src-tauri/tauri.conf.json +40 -0
- package/template/temp/.gitkeep +0 -0
- package/template/tsconfig.json +25 -0
- package/template/tsconfig.node.json +12 -0
- package/template/vite.config.ts +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Liz@Arshdelight
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-arsh-tauri
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/create-arsh-tauri)
|
|
4
|
+
|
|
5
|
+
The lightweight desktop stack we use for client work, one command away: **Tauri 2 + Vite + React 19 + TypeScript + Tailwind CSS 4**, a frameless window with a hand-rolled title bar, the same UI kit as `create-arsh-electron` — and Rust-side release polish (LTO, strip) done at the factory level.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create arsh-tauri@latest my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What you get
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
├── src-tauri/ # Rust backend: commands, window config, bundle settings
|
|
15
|
+
├── src/ # frontend (React + Tailwind 4)
|
|
16
|
+
│ └── ui/ # AppLayout, TitleBar, NavItem, Button, Dialog,
|
|
17
|
+
│ │ # Tooltip, Spinner, Switch — theme tokens in
|
|
18
|
+
│ │ # plain CSS variables, same kit as arsh-electron
|
|
19
|
+
├── AGENTS.md # bootstrap: the first AI session interviews you
|
|
20
|
+
│ # and writes this project's conventions
|
|
21
|
+
├── src-tauri/tauri.conf.json
|
|
22
|
+
└── vite.config.ts
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **Frameless window, custom title bar** — minimize / maximize / close via Tauri's window API, double-click to toggle maximize, an optional always-on-top pin, drag regions already wired up
|
|
26
|
+
- **Minimal single-page starter** — a counter button plus a `greet` round-trip through Rust, so the bridge is proven before you write real code
|
|
27
|
+
- **Test & CI baseline** — vitest wired up (`npm test`), a GitHub Actions workflow running lint + typecheck + tests + `cargo check` on push/PR
|
|
28
|
+
- **Packaging polish built in** — release profile with LTO + strip for small binaries, fixed Vite dev port (1420) that the Tauri CLI expects, a complete `.gitignore`, and git initialized with a first commit so AI edits are diffable from day one
|
|
29
|
+
- **Tiny output** — a scaffolded app builds to a few MB, no bundled runtime browser
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm create arsh-tauri@latest [project-name]
|
|
35
|
+
npm create arsh-tauri@latest [project-name] -- --scope <name>
|
|
36
|
+
|
|
37
|
+
# non-interactive
|
|
38
|
+
npm create arsh-tauri@latest my-app -- --yes
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Flag | Description |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `--scope <name>` | bundle identifier namespace, `com.<scope>.<app>` (default: `arshdelight`) |
|
|
44
|
+
| `--yes` | Skip prompts, use defaults (git init included) |
|
|
45
|
+
|
|
46
|
+
The CLI asks two questions — project name, git init or not — and derives everything else: `package.json` name (and `author`, from your git config), Rust crate + lib names, bundle `identifier`, `productName`, window title and title-bar brand. Answer `arsh-demo` and you get crate `arsh-demo`, lib `arsh_demo_lib`, product `ArshDemo` / `com.arshdelight.arshdemo`; add `--scope mycompany` and it becomes `com.mycompany.arshdemo`.
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
|
|
50
|
+
- Node.js ≥ 20.11
|
|
51
|
+
- Rust ≥ 1.77.2 (`rustup` — https://rustup.rs)
|
|
52
|
+
- WebView2 (preinstalled on Windows 10/11) — see https://tauri.app/start/prerequisites/
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
import * as p from '@clack/prompts'
|
|
7
|
+
|
|
8
|
+
const templateDir = path.join(import.meta.dirname, 'template')
|
|
9
|
+
|
|
10
|
+
const usage = [
|
|
11
|
+
'Usage: npm create arsh-tauri@latest [project-name] [--scope <name>] [--yes]',
|
|
12
|
+
'',
|
|
13
|
+
' --scope <name> appId namespace (default: arshdelight)',
|
|
14
|
+
' --yes Skip prompts and use defaults (git init included)',
|
|
15
|
+
].join('\n')
|
|
16
|
+
|
|
17
|
+
function slugify(name) {
|
|
18
|
+
return name.trim().toLowerCase().replace(/[^a-z\d-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toProductName(name) {
|
|
22
|
+
return name
|
|
23
|
+
.split(/[^a-zA-Z\d]+/)
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
26
|
+
.join('') || 'ArshApp'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isEmptyDir(dir) {
|
|
30
|
+
return !fs.existsSync(dir) || fs.readdirSync(dir).length === 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function emptyDir(dir) {
|
|
34
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
35
|
+
fs.rmSync(path.join(dir, entry), { recursive: true, force: true })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function gitAvailable() {
|
|
40
|
+
try {
|
|
41
|
+
execFileSync('git', ['--version'], { stdio: 'ignore' })
|
|
42
|
+
return true
|
|
43
|
+
} catch {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function gitAuthor() {
|
|
49
|
+
try {
|
|
50
|
+
const name = execFileSync('git', ['config', 'user.name'], { stdio: 'pipe' }).toString().trim()
|
|
51
|
+
if (!name) return null
|
|
52
|
+
const email = execFileSync('git', ['config', 'user.email'], { stdio: 'pipe' }).toString().trim()
|
|
53
|
+
return email ? { name, email } : { name }
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function initGit(projectDir) {
|
|
60
|
+
const run = (args) => execFileSync('git', args, { cwd: projectDir, stdio: 'ignore' })
|
|
61
|
+
run(['init'])
|
|
62
|
+
run(['add', '-A'])
|
|
63
|
+
try {
|
|
64
|
+
run(['commit', '-m', 'chore: initial commit'])
|
|
65
|
+
} catch {
|
|
66
|
+
p.log.warn('git commit skipped — set user.name / user.email, then commit manually')
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function applyRenames(projectDir, { packageName, productName, appId, libName, author }) {
|
|
71
|
+
const replace = (file, pairs) => {
|
|
72
|
+
const file_ = path.join(projectDir, file)
|
|
73
|
+
let content = fs.readFileSync(file_, 'utf8')
|
|
74
|
+
for (const [from, to] of pairs) content = content.split(from).join(to)
|
|
75
|
+
fs.writeFileSync(file_, content)
|
|
76
|
+
}
|
|
77
|
+
replace('package.json', [['"name": "arsh-tauri-app"', `"name": "${packageName}"`]])
|
|
78
|
+
if (author) {
|
|
79
|
+
replace('package.json', [['"author": "Your Name"', `"author": ${JSON.stringify(author)}`]])
|
|
80
|
+
}
|
|
81
|
+
replace('src-tauri/tauri.conf.json', [
|
|
82
|
+
['com.arshdelight.arshapp', appId],
|
|
83
|
+
['"productName": "ArshApp"', `"productName": "${productName}"`],
|
|
84
|
+
['"title": "ArshApp"', `"title": "${productName}"`],
|
|
85
|
+
])
|
|
86
|
+
replace('src-tauri/Cargo.toml', [
|
|
87
|
+
['name = "arsh-tauri-app"', `name = "${packageName}"`],
|
|
88
|
+
['arsh_tauri_app_lib', libName],
|
|
89
|
+
])
|
|
90
|
+
replace('src-tauri/src/main.rs', [['arsh_tauri_app_lib', libName]])
|
|
91
|
+
replace('index.html', [['Arsh Tauri', productName]])
|
|
92
|
+
replace('src/App.tsx', [['brand="Arsh Tauri"', `brand="${productName}"`]])
|
|
93
|
+
replace('README.md', [['# Arsh Tauri', `# ${productName}`]])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function main() {
|
|
97
|
+
const args = process.argv.slice(2)
|
|
98
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
99
|
+
console.log(usage)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const yes = args.includes('--yes')
|
|
104
|
+
const scopeIdx = args.indexOf('--scope')
|
|
105
|
+
const scopeArg = scopeIdx !== -1 ? args[scopeIdx + 1] : args.find((a) => a.startsWith('--scope='))
|
|
106
|
+
const positional = args.find((a) => !a.startsWith('-') && a !== scopeArg)
|
|
107
|
+
const nonInteractive = !process.stdout.isTTY || yes
|
|
108
|
+
|
|
109
|
+
p.intro('create-arsh-tauri')
|
|
110
|
+
|
|
111
|
+
let scopeSlug = 'arshdelight'
|
|
112
|
+
if (scopeArg !== undefined) {
|
|
113
|
+
const raw = scopeArg.startsWith('--scope=') ? scopeArg.slice('--scope='.length) : scopeArg
|
|
114
|
+
scopeSlug = slugify(raw)
|
|
115
|
+
if (!scopeSlug) {
|
|
116
|
+
p.log.error('Scope must contain letters or digits')
|
|
117
|
+
process.exitCode = 1
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let projectName = positional
|
|
123
|
+
if (!projectName) {
|
|
124
|
+
if (nonInteractive) {
|
|
125
|
+
p.log.error(`Project name is required in non-interactive mode.\n${usage}`)
|
|
126
|
+
process.exitCode = 1
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
const res = await p.text({
|
|
130
|
+
message: 'Project name',
|
|
131
|
+
placeholder: 'my-app',
|
|
132
|
+
validate: (v) => {
|
|
133
|
+
if (!v || !v.trim()) return 'Please enter a project name'
|
|
134
|
+
if (!slugify(v)) return 'Must contain letters or digits'
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
if (p.isCancel(res)) return
|
|
138
|
+
projectName = res
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const slug = slugify(projectName)
|
|
142
|
+
if (!slug) {
|
|
143
|
+
p.log.error('Project name must contain letters or digits')
|
|
144
|
+
process.exitCode = 1
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
const projectDir = path.resolve(process.cwd(), slug)
|
|
148
|
+
const packageName = slug
|
|
149
|
+
const productName = toProductName(slug)
|
|
150
|
+
const appId = `com.${scopeSlug}.${slug.replace(/-/g, '')}`
|
|
151
|
+
const libName = `${slug.replace(/-/g, '_')}_lib`
|
|
152
|
+
const author = gitAuthor()
|
|
153
|
+
|
|
154
|
+
if (!isEmptyDir(projectDir)) {
|
|
155
|
+
let overwrite = yes
|
|
156
|
+
if (process.stdout.isTTY && !yes) {
|
|
157
|
+
const res = await p.confirm({
|
|
158
|
+
message: `Directory "${slug}" is not empty. Remove existing files and continue?`,
|
|
159
|
+
initialValue: false,
|
|
160
|
+
})
|
|
161
|
+
if (p.isCancel(res)) return
|
|
162
|
+
overwrite = res
|
|
163
|
+
}
|
|
164
|
+
if (!overwrite) {
|
|
165
|
+
p.log.warn('Cancelled — directory left untouched')
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
emptyDir(projectDir)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const spinner = p.spinner()
|
|
172
|
+
spinner.start('Scaffolding project…')
|
|
173
|
+
// the published tarball never contains these dirs; skipping them here keeps
|
|
174
|
+
// local runs (template with node_modules / cargo target) identical to prod
|
|
175
|
+
fs.cpSync(templateDir, projectDir, {
|
|
176
|
+
recursive: true,
|
|
177
|
+
filter: (src) => {
|
|
178
|
+
const rel = path.relative(templateDir, src)
|
|
179
|
+
if (!rel) return true
|
|
180
|
+
const segs = rel.split(path.sep)
|
|
181
|
+
const [top] = segs
|
|
182
|
+
if (segs.includes('node_modules') || segs.includes('target')) return false
|
|
183
|
+
return !(top.startsWith('.temp') || top === 'dist' || (top === 'src-tauri' && segs[1] === 'gen'))
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
// npm publish strips .gitignore from the tarball, so the template ships it
|
|
187
|
+
// dotless and it gets its dot back here
|
|
188
|
+
for (const f of ['gitignore', 'temp']) {
|
|
189
|
+
fs.renameSync(path.join(projectDir, f), path.join(projectDir, `.${f}`))
|
|
190
|
+
}
|
|
191
|
+
applyRenames(projectDir, { packageName, productName, appId, libName, author })
|
|
192
|
+
|
|
193
|
+
let gitNote = ''
|
|
194
|
+
if (gitAvailable()) {
|
|
195
|
+
let doGit = true
|
|
196
|
+
if (process.stdout.isTTY && !yes) {
|
|
197
|
+
const res = await p.confirm({ message: 'Initialize a git repository and make the first commit?', initialValue: true })
|
|
198
|
+
if (p.isCancel(res)) return
|
|
199
|
+
doGit = res
|
|
200
|
+
}
|
|
201
|
+
if (doGit) {
|
|
202
|
+
try {
|
|
203
|
+
initGit(projectDir)
|
|
204
|
+
gitNote = 'git repository initialized with a first commit'
|
|
205
|
+
} catch {
|
|
206
|
+
gitNote = 'git initialization failed — you can run it manually'
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
gitNote = 'git not found — skipped version control setup'
|
|
211
|
+
}
|
|
212
|
+
spinner.stop('Project scaffolded')
|
|
213
|
+
|
|
214
|
+
const nextSteps = [
|
|
215
|
+
`cd ${slug}`,
|
|
216
|
+
'npm install',
|
|
217
|
+
'npm run dev',
|
|
218
|
+
]
|
|
219
|
+
p.note(
|
|
220
|
+
[
|
|
221
|
+
...nextSteps,
|
|
222
|
+
'',
|
|
223
|
+
'npm run dev compiles the Rust backend on first run —',
|
|
224
|
+
'grab a coffee, later runs are instant.',
|
|
225
|
+
'',
|
|
226
|
+
'Then open the project with any AI coding tool —',
|
|
227
|
+
'AGENTS.md is a bootstrap: the first AI session will interview you',
|
|
228
|
+
'and write the project conventions.',
|
|
229
|
+
gitNote,
|
|
230
|
+
].join('\n'),
|
|
231
|
+
'Next steps',
|
|
232
|
+
)
|
|
233
|
+
p.outro(`Done. Build something marvelous with ${productName}.`)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
main().catch((err) => {
|
|
237
|
+
p.log.error(String(err?.message ?? err))
|
|
238
|
+
process.exitCode = 1
|
|
239
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-arsh-tauri",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold an Arshdelight-style Tauri 2 + React + TypeScript desktop app — frameless window, UI primitives, and Rust-side build polish built in.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-arsh-tauri": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Arshdelight/create-arsh-tauri.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Arshdelight/create-arsh-tauri/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/Arshdelight/create-arsh-tauri#readme",
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"template",
|
|
20
|
+
"!template/node_modules",
|
|
21
|
+
"!template/src-tauri/target",
|
|
22
|
+
"!template/dist",
|
|
23
|
+
"!template/src-tauri/gen/schemas"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.11"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"tauri",
|
|
30
|
+
"react",
|
|
31
|
+
"typescript",
|
|
32
|
+
"scaffold",
|
|
33
|
+
"create",
|
|
34
|
+
"starter",
|
|
35
|
+
"template"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"author": {
|
|
39
|
+
"name": "Liz@Arshdelight",
|
|
40
|
+
"email": "cjhzisme@163.com"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@clack/prompts": "^1.8.1"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
root: true,
|
|
3
|
+
env: { browser: true, es2020: true },
|
|
4
|
+
extends: [
|
|
5
|
+
'eslint:recommended',
|
|
6
|
+
'plugin:@typescript-eslint/recommended',
|
|
7
|
+
'plugin:react-hooks/recommended',
|
|
8
|
+
],
|
|
9
|
+
ignorePatterns: ['dist', 'dist-electron', 'release', '.temp', '*.local'],
|
|
10
|
+
parser: '@typescript-eslint/parser',
|
|
11
|
+
plugins: ['react-refresh'],
|
|
12
|
+
rules: {
|
|
13
|
+
'react-refresh/only-export-components': [
|
|
14
|
+
'warn',
|
|
15
|
+
{ allowConstantExport: true },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
web:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 22
|
|
18
|
+
cache: 'npm'
|
|
19
|
+
|
|
20
|
+
- run: npm ci
|
|
21
|
+
|
|
22
|
+
- run: npm run lint
|
|
23
|
+
|
|
24
|
+
- run: npx tsc --noEmit
|
|
25
|
+
|
|
26
|
+
- run: npm test
|
|
27
|
+
|
|
28
|
+
rust:
|
|
29
|
+
runs-on: windows-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
|
|
33
|
+
- uses: actions/setup-node@v4
|
|
34
|
+
with:
|
|
35
|
+
node-version: 22
|
|
36
|
+
cache: 'npm'
|
|
37
|
+
|
|
38
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
39
|
+
|
|
40
|
+
- uses: Swatinem/rust-cache@v2
|
|
41
|
+
with:
|
|
42
|
+
workspaces: src-tauri
|
|
43
|
+
|
|
44
|
+
# generate_context! embeds the frontend assets, so dist/ must exist
|
|
45
|
+
- run: npm ci
|
|
46
|
+
|
|
47
|
+
- run: npm run build:web
|
|
48
|
+
|
|
49
|
+
- run: cargo check
|
|
50
|
+
working-directory: src-tauri
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# AGENTS.md — bootstrap
|
|
2
|
+
|
|
3
|
+
This file bootstraps the real AGENTS.md. Instead of shipping opinions the user
|
|
4
|
+
never asked for, your **first session** in this project runs a short interview
|
|
5
|
+
and rewrites this file into the project's actual conventions.
|
|
6
|
+
|
|
7
|
+
## First session: run the interview
|
|
8
|
+
|
|
9
|
+
If this file still contains the "Engineering rules" section kept verbatim, the
|
|
10
|
+
interview has not happened yet. Ask everything in **one round** (not one message
|
|
11
|
+
per question), accept short answers, and skip any question the user answers
|
|
12
|
+
with "default".
|
|
13
|
+
|
|
14
|
+
1. **Commit style** — prefix convention (`feat:` / `fix:` / `chore:` / none)?
|
|
15
|
+
Message language? Subject-line rules (length, imperative, capitalization)?
|
|
16
|
+
2. **Collaboration style** — plan first and wait for approval, or act directly?
|
|
17
|
+
Where is the line between "just do it" and "ask first"? How much refactoring
|
|
18
|
+
is welcome while touching nearby code?
|
|
19
|
+
3. **Language** — for conversation, code comments, docs, and UI strings?
|
|
20
|
+
4. **Delivery bar** — besides `npm run lint` and `npx tsc --noEmit`, what must
|
|
21
|
+
pass before work counts as done (tests, `cargo check`, packaging smoke)?
|
|
22
|
+
5. **Anything else** — pet peeves, forbidden actions, naming or structure
|
|
23
|
+
preferences worth writing down?
|
|
24
|
+
|
|
25
|
+
Then rewrite this file:
|
|
26
|
+
|
|
27
|
+
- Keep the file name `AGENTS.md` and keep the `## Engineering rules` section
|
|
28
|
+
intact — translate it only if the user asked for another language.
|
|
29
|
+
- Structure the rest around the interview answers.
|
|
30
|
+
- Zero filler. No platitudes ("write clean code", "be concise"), nothing the
|
|
31
|
+
template already enforces by construction. Every line must change how you act.
|
|
32
|
+
- The interview is one-time. Once this bootstrap section is gone, never ask again.
|
|
33
|
+
|
|
34
|
+
## Engineering rules
|
|
35
|
+
|
|
36
|
+
Three rules that normal, well-scoped tasks would otherwise silently violate.
|
|
37
|
+
Keep them intact.
|
|
38
|
+
|
|
39
|
+
- **Dependency partition**: `dependencies` in package.json stays empty — the
|
|
40
|
+
Vite frontend bundles everything, so all JS libraries go to
|
|
41
|
+
`devDependencies`. Rust dependencies live in `src-tauri/Cargo.toml` and are
|
|
42
|
+
added only when actually used.
|
|
43
|
+
- **Frontend never touches system capabilities directly**: all native calls go
|
|
44
|
+
through `@tauri-apps/api` (`invoke` / `listen`, see `src/lib/tauri.ts`).
|
|
45
|
+
New commands: `#[tauri::command]` in `src-tauri/src/lib.rs`, registered in
|
|
46
|
+
`generate_handler!`, permission entries in `src-tauri/capabilities/` when
|
|
47
|
+
needed.
|
|
48
|
+
- **Definition of done**: `npm run lint` (zero warnings), `npx tsc --noEmit`,
|
|
49
|
+
and `npm test` all pass before work counts as complete; run `cargo check`
|
|
50
|
+
in `src-tauri/` whenever Rust code or capabilities change.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Arsh Tauri
|
|
2
|
+
|
|
3
|
+
Tauri 2 + React + TypeScript desktop app.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js ≥ 20.11
|
|
8
|
+
- Rust ≥ 1.77.2(`rustup` — https://rustup.rs)
|
|
9
|
+
- WebView2(Windows 10/11 自带)
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
npm run dev # 启动桌面应用(首次会编译 Rust,之后秒开)
|
|
16
|
+
npm run dev:web # 只跑前端(UI 开发,无 Rust 桥接,自动降级)
|
|
17
|
+
npm run build # 类型检查 + 前端构建 + 打包安装器到 src-tauri/target/release/bundle/
|
|
18
|
+
npm run lint # ESLint,零警告
|
|
19
|
+
npm test # vitest
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
├── src-tauri/ # Rust 后端
|
|
26
|
+
│ ├── src/lib.rs # run() + 所有 #[tauri::command]
|
|
27
|
+
│ ├── src/main.rs # 入口
|
|
28
|
+
│ ├── capabilities/ # 权限配置(默认含无边框窗口控制)
|
|
29
|
+
│ ├── tauri.conf.json # 窗口 / 打包 / dev server 配置
|
|
30
|
+
│ └── icons/ # 应用图标(npx tauri icon <png> 重新生成)
|
|
31
|
+
├── src/
|
|
32
|
+
│ ├── ui/ # AppLayout, TitleBar, NavItem, Button, Dialog,
|
|
33
|
+
│ │ # Tooltip, Spinner, Switch + theme tokens (styles.css)
|
|
34
|
+
│ ├── lib/ # cn() 和 Tauri 桥接检测
|
|
35
|
+
│ ├── App.tsx # 入口页(含 invoke 示例)
|
|
36
|
+
│ └── index.css
|
|
37
|
+
├── AGENTS.md # project conventions for AI coding tools
|
|
38
|
+
└── vite.config.ts
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Packaging
|
|
42
|
+
|
|
43
|
+
- 自定义图标:放一张 ≥ 1024×1024 的方形 PNG,跑 `npx tauri icon <png>` 重新生成 `src-tauri/icons/`。
|
|
44
|
+
- 安装器元数据在 `src-tauri/tauri.conf.json`(`identifier`、`productName`)。
|
|
45
|
+
- 发布体积优化(LTO/strip)在 `src-tauri/Cargo.toml` 的 `[profile.release]`。
|
|
46
|
+
|
|
47
|
+
## AI coding tools
|
|
48
|
+
|
|
49
|
+
`AGENTS.md` holds the project conventions. On the first session it walks the
|
|
50
|
+
tool through a short interview and writes the final conventions for this
|
|
51
|
+
project.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
release
|
|
13
|
+
src-tauri/target
|
|
14
|
+
src-tauri/gen/schemas
|
|
15
|
+
*.local
|
|
16
|
+
|
|
17
|
+
# 本地草稿 / AI 协作产物(约定:临时文件一律进 .temp/)
|
|
18
|
+
.temp*
|
|
19
|
+
.zcode/
|
|
20
|
+
.zcodeignore
|
|
21
|
+
|
|
22
|
+
# Editor directories and files
|
|
23
|
+
.vscode/*
|
|
24
|
+
!.vscode/extensions.json
|
|
25
|
+
.idea
|
|
26
|
+
.DS_Store
|
|
27
|
+
*.suo
|
|
28
|
+
*.ntvs*
|
|
29
|
+
*.njsproj
|
|
30
|
+
*.sln
|
|
31
|
+
*.sw?
|
|
32
|
+
|
|
33
|
+
# AGENTS.md 默认随模板入库;若想保持本地不提交,取消下一行注释
|
|
34
|
+
# AGENTS.md
|
|
35
|
+
|
|
36
|
+
# test coverage
|
|
37
|
+
coverage/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Arsh Tauri</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|