Compare commits
9 commits
82fbf12950
...
984ae9d71e
| Author | SHA1 | Date | |
|---|---|---|---|
| 984ae9d71e | |||
| d1105bbb38 | |||
| 24515df35a | |||
| a4e3b2ccc8 | |||
| ac96921074 | |||
|
|
d4314adc52 | ||
|
|
2c6c0f1a89 | ||
|
|
4f0e284a96 | ||
|
|
ba4fcbce2d |
27 changed files with 25 additions and 193 deletions
|
|
@ -73,18 +73,6 @@ VITE_SPIRE_SEED=
|
|||
# Show "Under Service" screen and block all transactions
|
||||
# VITE_MAINTENANCE_MODE=true
|
||||
|
||||
# =============================================================================
|
||||
# Public Web Demo
|
||||
# =============================================================================
|
||||
|
||||
# Set ONLY for the browser demo build (atm.demo.aiolabs.dev). Leave blank on
|
||||
# every real machine. When set it:
|
||||
# - keeps the mouse cursor visible (kiosk builds hide it)
|
||||
# - mints one extra, never-used LNbits wallet named with this exact string,
|
||||
# so the throwaway accounts the demo creates (one per page load, each with
|
||||
# its own ephemeral identity) can be swept by name instead of guessed at.
|
||||
# VITE_DEMO_TAG=bitspire-web-demo
|
||||
|
||||
# =============================================================================
|
||||
# Mock Fallback (Production Safety)
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@
|
|||
* QR path keeps working — cash-out never depends on this.
|
||||
*/
|
||||
|
||||
import { execFile } from 'node:child_process'
|
||||
|
||||
export type NfcState = 'ready' | 'reading' | 'error' | 'card-removed' | 'unavailable'
|
||||
export interface NfcStatus {
|
||||
state: NfcState
|
||||
|
|
@ -85,11 +83,7 @@ export async function readNdefLnurlw(
|
|||
const send = (bytes: number[]) => transmit(Buffer.from(bytes), 256)
|
||||
|
||||
// Select the NDEF Tag Application (AID D2760000850101).
|
||||
if (
|
||||
!swOk(
|
||||
await send([0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00])
|
||||
)
|
||||
) {
|
||||
if (!swOk(await send([0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00]))) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -113,29 +107,6 @@ export async function readNdefLnurlw(
|
|||
|
||||
let stopFn: (() => void) | null = null
|
||||
|
||||
// ── Wedge auto-recovery ───────────────────────────────────────────────────
|
||||
// Cheap CCID readers (the Feitian R502-CL especially) occasionally wedge: they
|
||||
// keep detecting a card but every APDU returns "card absent or mute", and ONLY
|
||||
// a USB power-cycle clears it — pcscd/app restarts do NOT. When we see a run of
|
||||
// consecutive read failures we trigger nfc-reader-reset.service (a root oneshot
|
||||
// that re-binds the reader's USB device = a software replug); nfc-pcsc then
|
||||
// re-detects the reader on hotplug with no app restart. The trigger is gated by
|
||||
// a cooldown so a still-wedged reader can't reset-loop. A quality reader (e.g.
|
||||
// ACR1252U) wedges far less; this is belt-and-suspenders for any reader.
|
||||
const WEDGE_FAILURE_THRESHOLD = 3
|
||||
const RESET_COOLDOWN_MS = 30_000
|
||||
// Persist across reader re-enumerations (a reset spawns a fresh reader closure).
|
||||
let lastReaderResetAt = 0
|
||||
|
||||
/** Trigger the privileged USB power-cycle of the reader. Best-effort. */
|
||||
function resetWedgedReader(): void {
|
||||
// NixOS: the app runs unprivileged as `bitspire`; a polkit rule authorises it
|
||||
// to start this one unit. systemctl lives at a stable path on the device.
|
||||
execFile('/run/current-system/sw/bin/systemctl', ['start', 'nfc-reader-reset.service'], () => {
|
||||
/* best-effort — if it fails the reader stays wedged until a manual reset */
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening for Bolt Card taps. Idempotent. Returns a stop function.
|
||||
* Never throws — failures surface via onStatus.
|
||||
|
|
@ -188,10 +159,6 @@ export async function startNfcReader(
|
|||
// a present↔empty storm when hammered, so ignore re-detections for a beat
|
||||
// after a failure. Successful reads don't cool down.
|
||||
let cooldownUntil = 0
|
||||
// Consecutive failed reads → wedge detection (see resetWedgedReader above).
|
||||
// A completed read (Bolt Card or not) proves the reader is healthy and
|
||||
// clears the count; only a run of thrown transmits trips the reset.
|
||||
let consecutiveFailures = 0
|
||||
r.on('card', async () => {
|
||||
if (Date.now() < cooldownUntil) return
|
||||
onStatus({ state: 'reading', reader: name })
|
||||
|
|
@ -200,30 +167,13 @@ export async function startNfcReader(
|
|||
// user simply re-taps.
|
||||
try {
|
||||
const lnurlw = await readNdefLnurlw((apdu, maxLen) => r.transmit(apdu, maxLen))
|
||||
consecutiveFailures = 0
|
||||
if (lnurlw) {
|
||||
onCard(lnurlw)
|
||||
return
|
||||
}
|
||||
onStatus({ state: 'error', reader: name, message: 'not a Bolt Card' })
|
||||
} catch (e) {
|
||||
consecutiveFailures++
|
||||
if (
|
||||
consecutiveFailures >= WEDGE_FAILURE_THRESHOLD &&
|
||||
Date.now() - lastReaderResetAt > RESET_COOLDOWN_MS
|
||||
) {
|
||||
// Reader looks wedged — auto power-cycle it (only fix that works).
|
||||
lastReaderResetAt = Date.now()
|
||||
consecutiveFailures = 0
|
||||
onStatus({ state: 'error', reader: name, message: 'reader stuck — auto-resetting…' })
|
||||
resetWedgedReader()
|
||||
} else {
|
||||
onStatus({
|
||||
state: 'error',
|
||||
reader: name,
|
||||
message: 'card read failed — hold steady & retap',
|
||||
})
|
||||
}
|
||||
onStatus({ state: 'error', reader: name, message: 'card read failed — hold steady & retap' })
|
||||
void e
|
||||
}
|
||||
cooldownUntil = Date.now() + 1500
|
||||
|
|
@ -232,9 +182,7 @@ export async function startNfcReader(
|
|||
r.on('error', (err: unknown) =>
|
||||
onStatus({ state: 'error', reader: name, message: errMsg(err) })
|
||||
)
|
||||
r.on('end', () =>
|
||||
onStatus({ state: 'unavailable', reader: name, message: 'reader disconnected' })
|
||||
)
|
||||
r.on('end', () => onStatus({ state: 'unavailable', reader: name, message: 'reader disconnected' }))
|
||||
})
|
||||
nfc.on('error', (err: unknown) => onStatus({ state: 'error', message: errMsg(err) }))
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<!--
|
||||
Content Security Policy:
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss: http: https:; img-src 'self' data: blob:; font-src 'self'; frame-src 'none'; object-src 'none'"
|
||||
/>
|
||||
<title>bitSpire ATM</title>
|
||||
<title>Lamassu ATM</title>
|
||||
<style>
|
||||
/* Prevent text selection and context menu on kiosk */
|
||||
* {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
"dev:vite": "vite",
|
||||
"electron:dev": "tsc -p electron/tsconfig.json && tsc -p electron/tsconfig.preload.json && electron dist-electron/main.js",
|
||||
"build": "vue-tsc --noEmit && vite build && tsc -p electron/tsconfig.json && tsc -p electron/tsconfig.preload.json && npx esbuild electron/fund-atm.ts --bundle --platform=node --format=cjs --external:better-sqlite3 --outfile=dist-electron/fund-atm.bundle.cjs",
|
||||
"build:web": "vite build",
|
||||
"build:electron": "pnpm build && electron-builder",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
|
|
|
|||
|
|
@ -24,13 +24,6 @@ const router = createRouter({
|
|||
],
|
||||
})
|
||||
|
||||
// Kiosk chrome (hidden cursor) is the default — every real machine is a
|
||||
// touchscreen. The public web demo (VITE_DEMO_TAG) runs in a normal browser,
|
||||
// where an invisible pointer just reads as broken.
|
||||
if (!import.meta.env.VITE_DEMO_TAG) {
|
||||
document.documentElement.classList.add('kiosk')
|
||||
}
|
||||
|
||||
// Create Pinia store
|
||||
const pinia = createPinia()
|
||||
|
||||
|
|
|
|||
|
|
@ -505,31 +505,6 @@ export async function initializeLightningServices(options?: {
|
|||
}
|
||||
console.log('[Lightning] LNbits wallet:', lnbitsWalletId)
|
||||
|
||||
// ── Public web demo: stamp the throwaway account so it can be swept ──────
|
||||
// The browser demo (atm.demo.aiolabs.dev) runs with an EPHEMERAL identity —
|
||||
// a fresh keypair per page load — so LNbits mints a new account + a fresh
|
||||
// auto-credited wallet for every visitor. That isolation is the point (a
|
||||
// single baked-in key would be credited exactly once and then drain), but it
|
||||
// leaves throwaway accounts behind, and nothing in an auto-created row says
|
||||
// "demo": pubkey-set/prvkey-NULL also describes a real ATM.
|
||||
//
|
||||
// A nostr pubkey can't carry a marker (you'd have to grind a vanity prefix,
|
||||
// far too slow to do on page load), and the account/wallet the server
|
||||
// auto-creates isn't nameable by the client. So we mint one extra,
|
||||
// never-used wallet whose NAME is the tag: sweeping is then an exact string
|
||||
// match on wallet name rather than a heuristic about what looks disposable.
|
||||
//
|
||||
// Unset on every real machine, so this is inert outside the demo build. The
|
||||
// call is fire-and-forget: losing the marker degrades cleanup, not the demo.
|
||||
const demoTag = (import.meta.env.VITE_DEMO_TAG as string | undefined)?.trim()
|
||||
if (demoTag) {
|
||||
void lnbits
|
||||
.createWallet(demoTag)
|
||||
// Never log the reply — create_wallet returns adminkey/inkey.
|
||||
.then(() => console.log('[Lightning] Demo marker wallet created:', demoTag))
|
||||
.catch((e) => console.warn('[Lightning] Demo marker wallet failed:', e))
|
||||
}
|
||||
|
||||
// #70 P1: pull operator pubkey + fee config from LNbits over the authenticated
|
||||
// transport (spirekeeper#41 `get_machine_config`). A seed-only machine has no
|
||||
// VITE_OPERATOR_PUBKEYS, so without this it can't trust its fee config and sits
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
/* Hide cursor completely on touchscreen kiosk.
|
||||
Scoped to .kiosk (set on <html> by main.ts) so the public web demo, which
|
||||
runs in an ordinary browser with a mouse, keeps a visible pointer. */
|
||||
.kiosk,
|
||||
.kiosk *,
|
||||
.kiosk *::before,
|
||||
.kiosk *::after {
|
||||
/* Hide cursor completely on touchscreen kiosk */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ esac
|
|||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
echo "=== Building bitSpire ATM Live USB ISO (model: $MODEL) ==="
|
||||
echo "=== Building Lamassu ATM Live USB ISO (model: $MODEL) ==="
|
||||
echo ""
|
||||
echo "This is a pure Nix build — no local pnpm required."
|
||||
echo ""
|
||||
|
|
|
|||
|
|
@ -94,8 +94,7 @@
|
|||
|
||||
# pcscd gates client access via polkit; without a rule the sandboxed
|
||||
# `bitspire` service user is "Rejected unauthorized PC/SC client". Authorize
|
||||
# it to talk to the daemon and the card. The second rule lets the app trigger
|
||||
# the NFC reader wedge-recovery service (see nfc-reader-reset below).
|
||||
# it to talk to the daemon and the card.
|
||||
security.polkit.extraConfig = ''
|
||||
polkit.addRule(function(action, subject) {
|
||||
if ((action.id == "org.debian.pcsc-lite.access_pcsc" ||
|
||||
|
|
@ -104,47 +103,8 @@
|
|||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id == "org.freedesktop.systemd1.manage-units" &&
|
||||
action.lookup("unit") == "nfc-reader-reset.service" &&
|
||||
subject.user == "bitspire") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
'';
|
||||
|
||||
# NFC reader wedge-recovery. The Feitian R502-CL CCID reader (and, less often,
|
||||
# any CCID reader) can wedge: it keeps detecting a card but every APDU returns
|
||||
# "card absent or mute", and ONLY a USB power-cycle clears it — restarting
|
||||
# pcscd or the app does not. This oneshot re-binds the reader's USB device (a
|
||||
# software replug); pcscd + nfc-pcsc then re-detect it on hotplug with no app
|
||||
# restart (verified on-device). The app (unprivileged `bitspire`) starts it via
|
||||
# the polkit rule above when it sees repeated read failures. Reader-agnostic:
|
||||
# it matches the USB CCID interface class (0x0B), so it also covers a future
|
||||
# ACR1252U swap without a config change.
|
||||
systemd.services.nfc-reader-reset = {
|
||||
description = "Power-cycle a wedged CCID NFC reader (USB re-bind)";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = pkgs.writeShellScript "reset-nfc-reader" ''
|
||||
set -u
|
||||
found=0
|
||||
for iface in /sys/bus/usb/devices/*:*/bInterfaceClass; do
|
||||
[ -f "$iface" ] || continue
|
||||
[ "$(${pkgs.coreutils}/bin/cat "$iface" 2>/dev/null)" = "0b" ] || continue
|
||||
ifname=$(${pkgs.coreutils}/bin/basename "$(${pkgs.coreutils}/bin/dirname "$iface")")
|
||||
dev=''${ifname%%:*}
|
||||
echo "reset-nfc-reader: power-cycling CCID reader USB device $dev" >&2
|
||||
echo -n "$dev" > /sys/bus/usb/drivers/usb/unbind 2>/dev/null || true
|
||||
${pkgs.coreutils}/bin/sleep 2
|
||||
echo -n "$dev" > /sys/bus/usb/drivers/usb/bind 2>/dev/null || true
|
||||
found=1
|
||||
done
|
||||
[ "$found" = 1 ] || { echo "reset-nfc-reader: no CCID reader found" >&2; exit 1; }
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# Disable suspend/hibernate for kiosk
|
||||
systemd.targets = {
|
||||
sleep.enable = false;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# bitSpire ATM Live USB Configuration
|
||||
# Lamassu ATM Live USB Configuration
|
||||
# Bootable ISO for testing on physical hardware without installing to disk.
|
||||
#
|
||||
# Parameterized by machineModel (passed via specialArgs from flake.nix):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# bitSpire ATM Hardware udev Rules
|
||||
# Lamassu ATM Hardware udev Rules
|
||||
# Place in /etc/udev/rules.d/ or use services.udev.extraRules in NixOS
|
||||
|
||||
# ============================================
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
description = "bitSpire - Nostr-Native Lightning ATM";
|
||||
description = "Lamassu Next - Nostr-Native Lightning ATM";
|
||||
|
||||
inputs = {
|
||||
# Stable NixOS for the ATM OS base
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Pure Nix derivation for the bitSpire ATM Electron app.
|
||||
# Pure Nix derivation for the Lamassu ATM Electron app.
|
||||
#
|
||||
# Uses fetchPnpmDeps + pnpmConfigHook to build entirely inside the Nix sandbox,
|
||||
# eliminating the need for --impure or a local pnpm install.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
"scripts": {
|
||||
"dev": "turbo dev",
|
||||
"build": "turbo build",
|
||||
"build:web": "turbo build:web",
|
||||
"test": "turbo test",
|
||||
"lint": "turbo lint",
|
||||
"format": "prettier --write .",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@bitSpire/hal",
|
||||
"version": "0.1.0",
|
||||
"description": "Hardware Abstraction Layer for bitSpire ATM devices",
|
||||
"description": "Hardware Abstraction Layer for Lamassu ATM devices",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
"src"
|
||||
],
|
||||
"keywords": [
|
||||
"bitspire",
|
||||
"lamassu",
|
||||
"atm",
|
||||
"hardware",
|
||||
"bill-validator",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @bitSpire/hal - Hardware Abstraction Layer
|
||||
*
|
||||
* Provides drivers for bitSpire ATM hardware devices:
|
||||
* Provides drivers for Lamassu ATM hardware devices:
|
||||
* - Bill validators (JCM iVIZION via ID003 protocol)
|
||||
* - Bill dispensers (Fujitsu F53/F56)
|
||||
*
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import type {
|
|||
CreateInvoiceBody,
|
||||
PayInvoiceBody,
|
||||
WalletInfo,
|
||||
CreatedWallet,
|
||||
MachineConfigResponse,
|
||||
SubscribePaymentsBody,
|
||||
SubscribeAck,
|
||||
|
|
@ -212,17 +211,6 @@ export class LnbitsClient {
|
|||
return data ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an additional wallet on the calling account (`create_wallet`).
|
||||
*
|
||||
* Account-scoped (AUTH_ACCOUNT): the envelope carries no `wallet_id`, which
|
||||
* is what makes the server resolve auth to the Account rather than a Wallet.
|
||||
* NOT wrapped in `idempotent()` — a retry would mint a duplicate wallet.
|
||||
*/
|
||||
async createWallet(name: string): Promise<CreatedWallet> {
|
||||
return this.sendRpc<CreatedWallet>('create_wallet', { body: { name } })
|
||||
}
|
||||
|
||||
/** Pull server-delivered machine config (operator pubkey + fee config) over
|
||||
* the authenticated transport — spirekeeper's `get_machine_config` RPC
|
||||
* (bitspire#70 P1). Lets a seed-only ATM configure itself with no per-machine
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ export type {
|
|||
CreateInvoiceBody,
|
||||
PayInvoiceBody,
|
||||
WalletInfo,
|
||||
CreatedWallet,
|
||||
SubscribePaymentsBody,
|
||||
SubscribeAck,
|
||||
SubscribePush,
|
||||
|
|
|
|||
|
|
@ -112,15 +112,6 @@ export interface WalletInfo {
|
|||
balance: number
|
||||
}
|
||||
|
||||
/** Reply shape of the `create_wallet` RPC — unlike WalletInfo it carries the
|
||||
* fresh wallet's keys, so never log it verbatim. */
|
||||
export interface CreatedWallet {
|
||||
id: string
|
||||
name: string
|
||||
adminkey: string
|
||||
inkey: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Subscriptions
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@bitSpire/nostr-client",
|
||||
"version": "0.1.0",
|
||||
"description": "Nostr client library for bitSpire ATM",
|
||||
"description": "Nostr client library for Lamassu ATM",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Nostr client for bitSpire ATM
|
||||
* Nostr client for Lamassu ATM
|
||||
*
|
||||
* Manages connections to Nostr relays with support for:
|
||||
* - NIP-42 authentication
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Event creation utilities for bitSpire ATM
|
||||
* Event creation utilities for Lamassu ATM
|
||||
*/
|
||||
|
||||
import { type Event, type EventTemplate, type VerifiedEvent, getEventHash } from 'nostr-tools'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @bitSpire/nostr-client
|
||||
*
|
||||
* Nostr client library for bitSpire ATM communication.
|
||||
* Nostr client library for Lamassu ATM communication.
|
||||
*
|
||||
* Features:
|
||||
* - NIP-42 authentication for private relays
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Nostr client type definitions for bitSpire ATM
|
||||
* Nostr client type definitions for Lamassu ATM
|
||||
*/
|
||||
|
||||
import type { Event } from 'nostr-tools'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@bitSpire/ui-shared",
|
||||
"version": "0.1.0",
|
||||
"description": "Shared Vue 3 components for bitSpire ATM and dashboard",
|
||||
"description": "Shared Vue 3 components for Lamassu ATM and dashboard",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @bitSpire/ui-shared
|
||||
*
|
||||
* Shared Vue 3 components for bitSpire ATM and dashboard.
|
||||
* Shared Vue 3 components for Lamassu ATM and dashboard.
|
||||
* This package will contain common UI components like:
|
||||
* - QR code display
|
||||
* - Number pad
|
||||
|
|
|
|||
|
|
@ -5,11 +5,6 @@
|
|||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
||||
},
|
||||
"build:web": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"],
|
||||
"env": ["VITE_*"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue