agentful 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1488 -259
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -6,9 +6,16 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
9
12
|
var __commonJS = (cb, mod) => function __require() {
|
|
10
13
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
14
|
};
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
12
19
|
var __copyProps = (to, from, except, desc) => {
|
|
13
20
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
21
|
for (let key of __getOwnPropNames(from))
|
|
@@ -3026,6 +3033,72 @@ var require_commander = __commonJS({
|
|
|
3026
3033
|
}
|
|
3027
3034
|
});
|
|
3028
3035
|
|
|
3036
|
+
// src/branding.ts
|
|
3037
|
+
function banner() {
|
|
3038
|
+
return [
|
|
3039
|
+
paint.primary(paint.bold("agentful")) + paint.dim(` v${VERSION}`),
|
|
3040
|
+
paint.dim(brand.tagline)
|
|
3041
|
+
].join("\n");
|
|
3042
|
+
}
|
|
3043
|
+
var VERSION, brand, useColor, wrap, paint, sym, ui;
|
|
3044
|
+
var init_branding = __esm({
|
|
3045
|
+
"src/branding.ts"() {
|
|
3046
|
+
"use strict";
|
|
3047
|
+
VERSION = "0.2.1" ? "0.2.1" : null.version;
|
|
3048
|
+
brand = {
|
|
3049
|
+
name: "agentful",
|
|
3050
|
+
// the command users type
|
|
3051
|
+
displayName: "Agentful",
|
|
3052
|
+
// the product, in prose
|
|
3053
|
+
tagline: "Agentful in your terminal",
|
|
3054
|
+
docsUrl: "https://agentful.dev",
|
|
3055
|
+
userAgent: `agentful-cli/${VERSION} (+https://agentful.dev/cli)`
|
|
3056
|
+
};
|
|
3057
|
+
useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
3058
|
+
wrap = (code) => (s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
3059
|
+
paint = {
|
|
3060
|
+
primary: wrap("38;5;135"),
|
|
3061
|
+
// violet — agentful accent
|
|
3062
|
+
accent: wrap("36"),
|
|
3063
|
+
// cyan — URLs, code
|
|
3064
|
+
success: wrap("32"),
|
|
3065
|
+
warn: wrap("33"),
|
|
3066
|
+
error: wrap("31"),
|
|
3067
|
+
dim: wrap("2"),
|
|
3068
|
+
bold: wrap("1")
|
|
3069
|
+
};
|
|
3070
|
+
sym = {
|
|
3071
|
+
ok: paint.success("\u2714"),
|
|
3072
|
+
fail: paint.error("\u2716"),
|
|
3073
|
+
arrow: paint.primary("\u2192"),
|
|
3074
|
+
dot: paint.dim("\xB7")
|
|
3075
|
+
};
|
|
3076
|
+
ui = {
|
|
3077
|
+
info(msg) {
|
|
3078
|
+
console.log(`${sym.dot} ${msg}`);
|
|
3079
|
+
},
|
|
3080
|
+
step(msg) {
|
|
3081
|
+
console.log(`${sym.arrow} ${msg}`);
|
|
3082
|
+
},
|
|
3083
|
+
ok(msg) {
|
|
3084
|
+
console.log(`${sym.ok} ${msg}`);
|
|
3085
|
+
},
|
|
3086
|
+
warn(msg) {
|
|
3087
|
+
console.log(`${paint.warn("!")} ${msg}`);
|
|
3088
|
+
},
|
|
3089
|
+
fail(msg) {
|
|
3090
|
+
console.error(`${sym.fail} ${paint.error(msg)}`);
|
|
3091
|
+
},
|
|
3092
|
+
url(url) {
|
|
3093
|
+
return paint.accent(url);
|
|
3094
|
+
},
|
|
3095
|
+
code(s) {
|
|
3096
|
+
return paint.accent(s);
|
|
3097
|
+
}
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
});
|
|
3101
|
+
|
|
3029
3102
|
// node_modules/ignore/index.js
|
|
3030
3103
|
var require_ignore = __commonJS({
|
|
3031
3104
|
"node_modules/ignore/index.js"(exports2, module2) {
|
|
@@ -3414,6 +3487,75 @@ var require_ignore = __commonJS({
|
|
|
3414
3487
|
}
|
|
3415
3488
|
});
|
|
3416
3489
|
|
|
3490
|
+
// src/branding/logo.txt
|
|
3491
|
+
var logo_default;
|
|
3492
|
+
var init_logo = __esm({
|
|
3493
|
+
"src/branding/logo.txt"() {
|
|
3494
|
+
logo_default = " \u2584\u2588\u2588\u2588\u2588\u2584 \u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\n \u2588\u2588\u2588\u2588\u2588\u2588\u2584 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\n \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584 \u2588\u2588\u2588\u2588\n \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\n \u2588\u2588\u2588\u2588\u2580 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\n \u2588\u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\n \u2588\u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\n\u2588\u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588\u2588\u2588\n";
|
|
3495
|
+
}
|
|
3496
|
+
});
|
|
3497
|
+
|
|
3498
|
+
// src/branding/plugin.ts
|
|
3499
|
+
var plugin_exports = {};
|
|
3500
|
+
__export(plugin_exports, {
|
|
3501
|
+
brandingPluginSource: () => brandingPluginSource
|
|
3502
|
+
});
|
|
3503
|
+
function brandingPluginSource() {
|
|
3504
|
+
const logoLiteral = JSON.stringify(logo_default.replace(/\n+$/, "").split("\n"));
|
|
3505
|
+
return `// Generated by ${brand.name} v${VERSION} \u2014 do not edit; rewritten on every start.
|
|
3506
|
+
const LOGO = ${logoLiteral}
|
|
3507
|
+
|
|
3508
|
+
const plugin = {
|
|
3509
|
+
id: 'agentful-branding',
|
|
3510
|
+
tui(api) {
|
|
3511
|
+
api.slots.register({
|
|
3512
|
+
order: 1,
|
|
3513
|
+
slots: {
|
|
3514
|
+
home_logo(ctx) {
|
|
3515
|
+
const accent = ctx && ctx.theme ? ctx.theme.primary : undefined
|
|
3516
|
+
const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
|
|
3517
|
+
return (
|
|
3518
|
+
<box flexDirection="column">
|
|
3519
|
+
{LOGO.map((line) => (
|
|
3520
|
+
<text fg={accent}>{line}</text>
|
|
3521
|
+
))}
|
|
3522
|
+
<text> </text>
|
|
3523
|
+
<text fg={muted}>${brand.tagline}</text>
|
|
3524
|
+
</box>
|
|
3525
|
+
)
|
|
3526
|
+
},
|
|
3527
|
+
home_footer(ctx) {
|
|
3528
|
+
const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
|
|
3529
|
+
return <text fg={muted}>${brand.name} v${VERSION}</text>
|
|
3530
|
+
},
|
|
3531
|
+
// Bottom of the sidebar \u2014 this is where the engine prints its own
|
|
3532
|
+
// name and engine version (for example "OpenCode 1.18.18"); ours replaces it.
|
|
3533
|
+
sidebar_footer(ctx) {
|
|
3534
|
+
const accent = ctx && ctx.theme ? ctx.theme.primary : undefined
|
|
3535
|
+
const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
|
|
3536
|
+
return (
|
|
3537
|
+
<text>
|
|
3538
|
+
<span fg={accent}>${brand.displayName}</span>
|
|
3539
|
+
<span fg={muted}> v${VERSION}</span>
|
|
3540
|
+
</text>
|
|
3541
|
+
)
|
|
3542
|
+
},
|
|
3543
|
+
},
|
|
3544
|
+
})
|
|
3545
|
+
},
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
export default plugin
|
|
3549
|
+
`;
|
|
3550
|
+
}
|
|
3551
|
+
var init_plugin = __esm({
|
|
3552
|
+
"src/branding/plugin.ts"() {
|
|
3553
|
+
"use strict";
|
|
3554
|
+
init_branding();
|
|
3555
|
+
init_logo();
|
|
3556
|
+
}
|
|
3557
|
+
});
|
|
3558
|
+
|
|
3417
3559
|
// node_modules/commander/esm.mjs
|
|
3418
3560
|
var import_index = __toESM(require_commander(), 1);
|
|
3419
3561
|
var {
|
|
@@ -3431,75 +3573,21 @@ var {
|
|
|
3431
3573
|
Help
|
|
3432
3574
|
} = import_index.default;
|
|
3433
3575
|
|
|
3434
|
-
// src/
|
|
3435
|
-
|
|
3436
|
-
var brand = {
|
|
3437
|
-
name: "agentful",
|
|
3438
|
-
// the command users type
|
|
3439
|
-
displayName: "Agentful",
|
|
3440
|
-
// the product, in prose
|
|
3441
|
-
tagline: "Agentful in your terminal",
|
|
3442
|
-
docsUrl: "https://agentful.dev",
|
|
3443
|
-
userAgent: `agentful-cli/${VERSION} (+https://agentful.dev/cli)`
|
|
3444
|
-
};
|
|
3445
|
-
var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
3446
|
-
var wrap = (code) => (s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
3447
|
-
var paint = {
|
|
3448
|
-
primary: wrap("38;5;135"),
|
|
3449
|
-
// violet — agentful accent
|
|
3450
|
-
accent: wrap("36"),
|
|
3451
|
-
// cyan — URLs, code
|
|
3452
|
-
success: wrap("32"),
|
|
3453
|
-
warn: wrap("33"),
|
|
3454
|
-
error: wrap("31"),
|
|
3455
|
-
dim: wrap("2"),
|
|
3456
|
-
bold: wrap("1")
|
|
3457
|
-
};
|
|
3458
|
-
var sym = {
|
|
3459
|
-
ok: paint.success("\u2714"),
|
|
3460
|
-
fail: paint.error("\u2716"),
|
|
3461
|
-
arrow: paint.primary("\u2192"),
|
|
3462
|
-
dot: paint.dim("\xB7")
|
|
3463
|
-
};
|
|
3464
|
-
function banner() {
|
|
3465
|
-
return [
|
|
3466
|
-
paint.primary(paint.bold("agentful")) + paint.dim(` v${VERSION}`),
|
|
3467
|
-
paint.dim(brand.tagline)
|
|
3468
|
-
].join("\n");
|
|
3469
|
-
}
|
|
3470
|
-
var ui = {
|
|
3471
|
-
info(msg) {
|
|
3472
|
-
console.log(`${sym.dot} ${msg}`);
|
|
3473
|
-
},
|
|
3474
|
-
step(msg) {
|
|
3475
|
-
console.log(`${sym.arrow} ${msg}`);
|
|
3476
|
-
},
|
|
3477
|
-
ok(msg) {
|
|
3478
|
-
console.log(`${sym.ok} ${msg}`);
|
|
3479
|
-
},
|
|
3480
|
-
warn(msg) {
|
|
3481
|
-
console.log(`${paint.warn("!")} ${msg}`);
|
|
3482
|
-
},
|
|
3483
|
-
fail(msg) {
|
|
3484
|
-
console.error(`${sym.fail} ${paint.error(msg)}`);
|
|
3485
|
-
},
|
|
3486
|
-
url(url) {
|
|
3487
|
-
return paint.accent(url);
|
|
3488
|
-
},
|
|
3489
|
-
code(s) {
|
|
3490
|
-
return paint.accent(s);
|
|
3491
|
-
}
|
|
3492
|
-
};
|
|
3576
|
+
// src/index.ts
|
|
3577
|
+
init_branding();
|
|
3493
3578
|
|
|
3494
3579
|
// src/lib/api.ts
|
|
3580
|
+
init_branding();
|
|
3495
3581
|
var ApiError = class extends Error {
|
|
3496
|
-
constructor(status, code, message) {
|
|
3582
|
+
constructor(status, code, message, details) {
|
|
3497
3583
|
super(message);
|
|
3498
3584
|
this.status = status;
|
|
3499
3585
|
this.code = code;
|
|
3586
|
+
this.details = details;
|
|
3500
3587
|
}
|
|
3501
3588
|
status;
|
|
3502
3589
|
code;
|
|
3590
|
+
details;
|
|
3503
3591
|
};
|
|
3504
3592
|
async function request(url, opts = {}) {
|
|
3505
3593
|
const headers = {
|
|
@@ -3528,12 +3616,26 @@ async function request(url, opts = {}) {
|
|
|
3528
3616
|
}
|
|
3529
3617
|
if (!resp.ok) {
|
|
3530
3618
|
const code = String(data?.error || `http_${resp.status}`);
|
|
3531
|
-
|
|
3532
|
-
|
|
3619
|
+
let message = String(data?.message || data?.error || text?.slice(0, 300) || `Request failed with ${resp.status}`);
|
|
3620
|
+
let details;
|
|
3621
|
+
if (data?.details !== void 0 && data?.details !== null) {
|
|
3622
|
+
const raw = typeof data.details === "string" ? data.details : JSON.stringify(data.details);
|
|
3623
|
+
if (raw) {
|
|
3624
|
+
details = raw;
|
|
3625
|
+
const shown = raw.length > 4e3 ? `${raw.slice(0, 4e3)}
|
|
3626
|
+
\u2026 (truncated)` : raw;
|
|
3627
|
+
if (shown !== message) message += `
|
|
3628
|
+
${shown}`;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
throw new ApiError(resp.status, code, message, details);
|
|
3533
3632
|
}
|
|
3534
3633
|
return data ?? {};
|
|
3535
3634
|
}
|
|
3536
3635
|
|
|
3636
|
+
// src/commands/login.ts
|
|
3637
|
+
init_branding();
|
|
3638
|
+
|
|
3537
3639
|
// src/lib/auth.ts
|
|
3538
3640
|
var import_node_os2 = require("os");
|
|
3539
3641
|
|
|
@@ -3637,6 +3739,9 @@ function clearAuth() {
|
|
|
3637
3739
|
return true;
|
|
3638
3740
|
}
|
|
3639
3741
|
|
|
3742
|
+
// src/lib/auth.ts
|
|
3743
|
+
init_branding();
|
|
3744
|
+
|
|
3640
3745
|
// src/lib/browser.ts
|
|
3641
3746
|
var import_node_child_process2 = require("child_process");
|
|
3642
3747
|
function openInBrowser(url) {
|
|
@@ -3754,6 +3859,7 @@ async function loginCommand(opts) {
|
|
|
3754
3859
|
}
|
|
3755
3860
|
|
|
3756
3861
|
// src/commands/logout.ts
|
|
3862
|
+
init_branding();
|
|
3757
3863
|
async function logoutCommand() {
|
|
3758
3864
|
if (clearAuth()) {
|
|
3759
3865
|
ui.ok("Logged out \u2014 local credentials removed.");
|
|
@@ -3763,6 +3869,7 @@ async function logoutCommand() {
|
|
|
3763
3869
|
}
|
|
3764
3870
|
|
|
3765
3871
|
// src/commands/whoami.ts
|
|
3872
|
+
init_branding();
|
|
3766
3873
|
async function whoamiCommand() {
|
|
3767
3874
|
const auth = await ensureAuth();
|
|
3768
3875
|
console.log(auth.email);
|
|
@@ -3770,8 +3877,9 @@ async function whoamiCommand() {
|
|
|
3770
3877
|
}
|
|
3771
3878
|
|
|
3772
3879
|
// src/commands/init.ts
|
|
3773
|
-
var
|
|
3880
|
+
var import_node_path5 = require("path");
|
|
3774
3881
|
var import_promises = require("readline/promises");
|
|
3882
|
+
init_branding();
|
|
3775
3883
|
|
|
3776
3884
|
// src/lib/project.ts
|
|
3777
3885
|
var import_node_fs2 = require("fs");
|
|
@@ -3812,15 +3920,544 @@ async function createProject(auth, title, cwd = process.cwd()) {
|
|
|
3812
3920
|
return config;
|
|
3813
3921
|
}
|
|
3814
3922
|
|
|
3923
|
+
// src/lib/starters.ts
|
|
3924
|
+
var import_node_fs3 = require("fs");
|
|
3925
|
+
var import_node_path4 = require("path");
|
|
3926
|
+
|
|
3927
|
+
// src/generated/starters.json
|
|
3928
|
+
var starters_default = {
|
|
3929
|
+
$comment: "GENERATED by cli/scripts/sync-starters.mjs from templates/starters/ \u2014 do not edit by hand. platform-contract/check-parity.py guards drift.",
|
|
3930
|
+
schema_version: 1,
|
|
3931
|
+
starters: {
|
|
3932
|
+
nextjs: {
|
|
3933
|
+
framework: "nextjs",
|
|
3934
|
+
files: {
|
|
3935
|
+
".gitignore": "# Dependencies\nnode_modules/\n.pnpm-store/\n\n# Build output\ndist/\nbuild/\nout/\n.next/\n.nuxt/\n.output/\n.svelte-kit/\n.astro/\n.vercel/\n.netlify/\n.wrangler/\n.turbo/\n\n# Environment\n.env\n.env.local\n.env.*.local\n\n# IDE\n.idea/\n.vscode/\n*.swp\n*.swo\n*~\n\n# OS\n.DS_Store\nThumbs.db\n\n# Logs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Test coverage\ncoverage/\n.nyc_output/\n\n# Cache\n.cache/\n.parcel-cache/\n",
|
|
3936
|
+
"eslint.config.mjs": 'import { defineConfig, globalIgnores } from "eslint/config";\nimport nextVitals from "eslint-config-next/core-web-vitals";\nimport nextTs from "eslint-config-next/typescript";\n\nconst eslintConfig = defineConfig([\n ...nextVitals,\n ...nextTs,\n // Override default ignores of eslint-config-next.\n globalIgnores([\n // Default ignores of eslint-config-next:\n ".next/**",\n "out/**",\n "build/**",\n "next-env.d.ts",\n ]),\n]);\n\nexport default eslintConfig;\n',
|
|
3937
|
+
"next-env.d.ts": '/// <reference types="next" />\n/// <reference types="next/image-types/global" />\nimport "./.next/types/routes.d.ts";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n',
|
|
3938
|
+
"next.config.ts": "import type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n // Static export \u2014 the platform serves files, there is no Next.js server.\n output: 'export',\n // Emits `about/index.html` instead of `about.html`, which a plain static\n // host can resolve without rewrite rules.\n trailingSlash: true,\n // next/image needs a server to optimise; without this the export fails.\n images: {\n unoptimized: true,\n },\n}\n\nexport default nextConfig\n",
|
|
3939
|
+
"package.json": '{\n "name": "agentful-starter-nextjs",\n "private": true,\n "version": "0.1.0",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "verify": "tsc --noEmit && eslint . --max-warnings 0 && next build"\n },\n "dependencies": {\n "next": "16.2.9",\n "react": "19.2.4",\n "react-dom": "19.2.4"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4",\n "@types/node": "^20",\n "@types/react": "^19",\n "@types/react-dom": "^19",\n "eslint": "^9",\n "eslint-config-next": "16.2.9",\n "tailwindcss": "^4",\n "typescript": "^5"\n }\n}\n',
|
|
3940
|
+
"postcss.config.mjs": "const config = {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n\nexport default config\n",
|
|
3941
|
+
"public/favicon.svg": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Site icon">\n <rect width="32" height="32" rx="7" fill="#4f46e5"/>\n <path d="M10 22V10h5.2a4.2 4.2 0 0 1 0 8.4H13" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>\n <path d="M16 18.4 22 22" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round"/>\n</svg>\n',
|
|
3942
|
+
"src/app/globals.css": '@import "tailwindcss";\n\n/* Design tokens. Tailwind v4 turns every entry here into a utility\n (--color-brand -> bg-brand/text-brand), so restyle the project by editing\n these values rather than sprinkling hex codes through the components. */\n@theme {\n --color-brand: #0070f3;\n --color-brand-contrast: #ffffff;\n --color-surface: #ffffff;\n --color-surface-muted: #f5f5f7;\n --color-ink: #111827;\n --color-ink-muted: #6b7280;\n --radius-card: 0.75rem;\n}\n\n:root {\n color-scheme: light;\n font-family: Inter, ui-sans-serif, system-ui, sans-serif;\n --app-bg: #ffffff;\n --app-fg: #111827;\n}\n\n/* An opaque background on html, body is mandatory \u2014 the page is never\n transparent. Keep this rule when you restyle. */\nhtml, body {\n margin: 0;\n min-height: 100%;\n min-width: 320px;\n background: var(--app-bg);\n color: var(--app-fg);\n}\n\n/* Keyboard focus baseline. Components may override it, but never remove it:\n the platform appends a generic fallback to any project whose CSS contains\n no `:focus` rule at all, and a real one belongs here. */\n:focus-visible {\n outline: 2px solid var(--color-brand, currentColor);\n outline-offset: 2px;\n}\n',
|
|
3943
|
+
"src/app/layout.tsx": `import type { Metadata } from 'next'
|
|
3944
|
+
import './globals.css'
|
|
3945
|
+
|
|
3946
|
+
export const metadata: Metadata = {
|
|
3947
|
+
title: 'Replace this title',
|
|
3948
|
+
description: 'Replace this with a one-sentence description of the site.',
|
|
3949
|
+
icons: { icon: '/favicon.svg' },
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
export default function RootLayout({
|
|
3953
|
+
children,
|
|
3954
|
+
}: Readonly<{
|
|
3955
|
+
children: React.ReactNode
|
|
3956
|
+
}>) {
|
|
3957
|
+
return (
|
|
3958
|
+
<html lang="en" className="h-full antialiased">
|
|
3959
|
+
<body className="min-h-full flex flex-col">{children}</body>
|
|
3960
|
+
</html>
|
|
3961
|
+
)
|
|
3962
|
+
}
|
|
3963
|
+
`,
|
|
3964
|
+
"src/app/page.tsx": `type Feature = {
|
|
3965
|
+
title: string
|
|
3966
|
+
body: string
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
// Replace this data with the real content of the project.
|
|
3970
|
+
const FEATURES: Feature[] = [
|
|
3971
|
+
{ title: 'First point', body: 'Replace this with a benefit the visitor cares about.' },
|
|
3972
|
+
{ title: 'Second point', body: 'Keep each card to one idea and one sentence.' },
|
|
3973
|
+
{ title: 'Third point', body: 'Delete a card if the project only needs two.' },
|
|
3974
|
+
]
|
|
3975
|
+
|
|
3976
|
+
export default function Home() {
|
|
3977
|
+
return (
|
|
3978
|
+
<div className="min-h-screen flex flex-col">
|
|
3979
|
+
<header className="border-b border-black/10">
|
|
3980
|
+
<div className="mx-auto max-w-5xl px-4 sm:px-6 py-4 flex items-center justify-between gap-6">
|
|
3981
|
+
<a href="#main" className="font-semibold text-ink rounded">
|
|
3982
|
+
Brand
|
|
3983
|
+
</a>
|
|
3984
|
+
<nav aria-label="Main">
|
|
3985
|
+
<ul className="flex items-center gap-6">
|
|
3986
|
+
<li>
|
|
3987
|
+
<a href="#features" className="text-ink-muted hover:text-ink rounded">
|
|
3988
|
+
Features
|
|
3989
|
+
</a>
|
|
3990
|
+
</li>
|
|
3991
|
+
<li>
|
|
3992
|
+
<a href="#contact" className="text-ink-muted hover:text-ink rounded">
|
|
3993
|
+
Contact
|
|
3994
|
+
</a>
|
|
3995
|
+
</li>
|
|
3996
|
+
</ul>
|
|
3997
|
+
</nav>
|
|
3998
|
+
</div>
|
|
3999
|
+
</header>
|
|
4000
|
+
|
|
4001
|
+
<main id="main" className="flex-1">
|
|
4002
|
+
<section className="mx-auto max-w-5xl px-4 sm:px-6 py-16 sm:py-24">
|
|
4003
|
+
<h1 className="text-4xl sm:text-5xl font-semibold tracking-tight text-ink text-balance">
|
|
4004
|
+
Replace this headline
|
|
4005
|
+
</h1>
|
|
4006
|
+
<p className="mt-4 max-w-2xl text-lg text-ink-muted">
|
|
4007
|
+
Replace this paragraph with one sentence that says what the product does
|
|
4008
|
+
and who it is for.
|
|
4009
|
+
</p>
|
|
4010
|
+
<a
|
|
4011
|
+
href="#contact"
|
|
4012
|
+
className="mt-8 inline-flex items-center rounded-card bg-brand px-5 py-3 font-medium text-brand-contrast hover:opacity-90"
|
|
4013
|
+
>
|
|
4014
|
+
Primary action
|
|
4015
|
+
</a>
|
|
4016
|
+
</section>
|
|
4017
|
+
|
|
4018
|
+
<section id="features" className="mx-auto max-w-5xl px-4 sm:px-6 pb-16 sm:pb-24">
|
|
4019
|
+
<h2 className="text-2xl font-semibold tracking-tight text-ink">
|
|
4020
|
+
Replace this section title
|
|
4021
|
+
</h2>
|
|
4022
|
+
<ul className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
4023
|
+
{FEATURES.map((feature) => (
|
|
4024
|
+
<li key={feature.title} className="rounded-card bg-surface-muted p-6">
|
|
4025
|
+
<h3 className="font-medium text-ink">{feature.title}</h3>
|
|
4026
|
+
<p className="mt-2 text-ink-muted">{feature.body}</p>
|
|
4027
|
+
</li>
|
|
4028
|
+
))}
|
|
4029
|
+
</ul>
|
|
4030
|
+
</section>
|
|
4031
|
+
</main>
|
|
4032
|
+
|
|
4033
|
+
<footer id="contact" className="border-t border-black/10">
|
|
4034
|
+
<div className="mx-auto max-w-5xl px-4 sm:px-6 py-8 text-sm text-ink-muted">
|
|
4035
|
+
Replace this footer.
|
|
4036
|
+
</div>
|
|
4037
|
+
</footer>
|
|
4038
|
+
</div>
|
|
4039
|
+
)
|
|
4040
|
+
}
|
|
4041
|
+
`,
|
|
4042
|
+
"tsconfig.json": '{\n "compilerOptions": {\n "target": "ES2017",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "react-jsx",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": { "@/*": ["./src/*"] }\n },\n "include": [\n "next-env.d.ts",\n "**/*.ts",\n "**/*.tsx",\n "**/*.mts",\n ".next/types/**/*.ts",\n ".next/dev/types/**/*.ts"\n ],\n "exclude": ["node_modules"]\n}\n'
|
|
4043
|
+
}
|
|
4044
|
+
},
|
|
4045
|
+
react: {
|
|
4046
|
+
framework: "vite",
|
|
4047
|
+
files: {
|
|
4048
|
+
".gitignore": "# Dependencies\nnode_modules/\n.pnpm-store/\n\n# Build output\ndist/\nbuild/\nout/\n.next/\n.nuxt/\n.output/\n.svelte-kit/\n.astro/\n.vercel/\n.netlify/\n.wrangler/\n.turbo/\n\n# Environment\n.env\n.env.local\n.env.*.local\n\n# IDE\n.idea/\n.vscode/\n*.swp\n*.swo\n*~\n\n# OS\n.DS_Store\nThumbs.db\n\n# Logs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Test coverage\ncoverage/\n.nyc_output/\n\n# Cache\n.cache/\n.parcel-cache/\n",
|
|
4049
|
+
"eslint.config.mjs": "import tseslint from 'typescript-eslint'\nimport reactHooks from 'eslint-plugin-react-hooks'\n\nexport default tseslint.config(\n { ignores: ['dist'] },\n {\n files: ['**/*.{ts,tsx}'],\n extends: [tseslint.configs.base],\n plugins: { 'react-hooks': reactHooks },\n rules: {\n 'react-hooks/rules-of-hooks': 'error',\n 'react-hooks/exhaustive-deps': 'error',\n 'react-hooks/set-state-in-effect': 'error',\n 'react-hooks/no-deriving-state-in-effects': 'error',\n '@typescript-eslint/no-unused-vars': [\n 'error',\n { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },\n ],\n },\n },\n)\n",
|
|
4050
|
+
"index.html": '<!doctype html>\n<html lang="en">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <meta name="description" content="Replace this with a one-sentence description of the app." />\n <link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n <title>Replace this title</title>\n </head>\n <body>\n <div id="root"></div>\n <script type="module" src="./src/main.tsx"></script>\n </body>\n</html>\n',
|
|
4051
|
+
"package.json": '{\n "name": "agentful-starter-react",\n "private": true,\n "version": "0.1.0",\n "type": "module",\n "scripts": {\n "dev": "vite",\n "build": "vite build",\n "preview": "vite preview",\n "verify": "tsc --noEmit && eslint . --max-warnings 0 && vite build"\n },\n "dependencies": {\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/vite": "^4.0.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "@vitejs/plugin-react": "^4.3.4",\n "eslint": "^9.0.0",\n "eslint-plugin-react-hooks": "^6.0.0",\n "tailwindcss": "^4.0.0",\n "typescript": "^5.0.0",\n "typescript-eslint": "^8.0.0",\n "vite": "^6.0.0"\n }\n}\n',
|
|
4052
|
+
"public/favicon.svg": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Site icon">\n <rect width="32" height="32" rx="7" fill="#4f46e5"/>\n <path d="M10 22V10h5.2a4.2 4.2 0 0 1 0 8.4H13" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>\n <path d="M16 18.4 22 22" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round"/>\n</svg>\n',
|
|
4053
|
+
"src/App.tsx": `import { useState } from 'react'
|
|
4054
|
+
|
|
4055
|
+
type Feature = {
|
|
4056
|
+
title: string
|
|
4057
|
+
body: string
|
|
4058
|
+
}
|
|
4059
|
+
|
|
4060
|
+
// Replace this data with the real content of the project.
|
|
4061
|
+
const FEATURES: Feature[] = [
|
|
4062
|
+
{ title: 'First point', body: 'Replace this with a benefit the visitor cares about.' },
|
|
4063
|
+
{ title: 'Second point', body: 'Keep each card to one idea and one sentence.' },
|
|
4064
|
+
{ title: 'Third point', body: 'Delete a card if the project only needs two.' },
|
|
4065
|
+
]
|
|
4066
|
+
|
|
4067
|
+
const NAV = [
|
|
4068
|
+
{ href: '#features', label: 'Features' },
|
|
4069
|
+
{ href: '#contact', label: 'Contact' },
|
|
4070
|
+
]
|
|
4071
|
+
|
|
4072
|
+
function App() {
|
|
4073
|
+
const [navOpen, setNavOpen] = useState(false)
|
|
4074
|
+
|
|
4075
|
+
return (
|
|
4076
|
+
<div className="min-h-screen flex flex-col">
|
|
4077
|
+
<header className="border-b border-black/10">
|
|
4078
|
+
<div className="mx-auto max-w-5xl px-4 sm:px-6 py-4 flex items-center justify-between gap-4">
|
|
4079
|
+
<a
|
|
4080
|
+
href="#main"
|
|
4081
|
+
className="font-semibold text-ink rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
|
4082
|
+
>
|
|
4083
|
+
Brand
|
|
4084
|
+
</a>
|
|
4085
|
+
|
|
4086
|
+
<nav aria-label="Main" className="hidden sm:block">
|
|
4087
|
+
<ul className="flex items-center gap-6">
|
|
4088
|
+
{NAV.map((item) => (
|
|
4089
|
+
<li key={item.href}>
|
|
4090
|
+
<a
|
|
4091
|
+
href={item.href}
|
|
4092
|
+
className="text-ink-muted hover:text-ink rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
|
4093
|
+
>
|
|
4094
|
+
{item.label}
|
|
4095
|
+
</a>
|
|
4096
|
+
</li>
|
|
4097
|
+
))}
|
|
4098
|
+
</ul>
|
|
4099
|
+
</nav>
|
|
4100
|
+
|
|
4101
|
+
<button
|
|
4102
|
+
type="button"
|
|
4103
|
+
className="sm:hidden rounded px-3 py-2 text-ink border border-black/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
|
4104
|
+
aria-expanded={navOpen}
|
|
4105
|
+
aria-controls="mobile-nav"
|
|
4106
|
+
onClick={() => setNavOpen((open) => !open)}
|
|
4107
|
+
>
|
|
4108
|
+
Menu
|
|
4109
|
+
</button>
|
|
4110
|
+
</div>
|
|
4111
|
+
|
|
4112
|
+
{navOpen && (
|
|
4113
|
+
<nav id="mobile-nav" aria-label="Main" className="sm:hidden border-t border-black/10">
|
|
4114
|
+
<ul className="mx-auto max-w-5xl px-4 py-2 flex flex-col">
|
|
4115
|
+
{NAV.map((item) => (
|
|
4116
|
+
<li key={item.href}>
|
|
4117
|
+
<a
|
|
4118
|
+
href={item.href}
|
|
4119
|
+
className="block py-2 text-ink-muted hover:text-ink rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
|
4120
|
+
onClick={() => setNavOpen(false)}
|
|
4121
|
+
>
|
|
4122
|
+
{item.label}
|
|
4123
|
+
</a>
|
|
4124
|
+
</li>
|
|
4125
|
+
))}
|
|
4126
|
+
</ul>
|
|
4127
|
+
</nav>
|
|
4128
|
+
)}
|
|
4129
|
+
</header>
|
|
4130
|
+
|
|
4131
|
+
<main id="main" className="flex-1">
|
|
4132
|
+
<section className="mx-auto max-w-5xl px-4 sm:px-6 py-16 sm:py-24">
|
|
4133
|
+
<h1 className="text-4xl sm:text-5xl font-semibold tracking-tight text-ink text-balance">
|
|
4134
|
+
Replace this headline
|
|
4135
|
+
</h1>
|
|
4136
|
+
<p className="mt-4 max-w-2xl text-lg text-ink-muted">
|
|
4137
|
+
Replace this paragraph with one sentence that says what the product does
|
|
4138
|
+
and who it is for.
|
|
4139
|
+
</p>
|
|
4140
|
+
<a
|
|
4141
|
+
href="#contact"
|
|
4142
|
+
className="mt-8 inline-flex items-center rounded-card bg-brand px-5 py-3 font-medium text-brand-contrast hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
|
4143
|
+
>
|
|
4144
|
+
Primary action
|
|
4145
|
+
</a>
|
|
4146
|
+
</section>
|
|
4147
|
+
|
|
4148
|
+
<section id="features" className="mx-auto max-w-5xl px-4 sm:px-6 pb-16 sm:pb-24">
|
|
4149
|
+
<h2 className="text-2xl font-semibold tracking-tight text-ink">Replace this section title</h2>
|
|
4150
|
+
<ul className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
4151
|
+
{FEATURES.map((feature) => (
|
|
4152
|
+
<li key={feature.title} className="rounded-card bg-surface-muted p-6">
|
|
4153
|
+
<h3 className="font-medium text-ink">{feature.title}</h3>
|
|
4154
|
+
<p className="mt-2 text-ink-muted">{feature.body}</p>
|
|
4155
|
+
</li>
|
|
4156
|
+
))}
|
|
4157
|
+
</ul>
|
|
4158
|
+
</section>
|
|
4159
|
+
</main>
|
|
4160
|
+
|
|
4161
|
+
<footer id="contact" className="border-t border-black/10">
|
|
4162
|
+
<div className="mx-auto max-w-5xl px-4 sm:px-6 py-8 text-sm text-ink-muted">
|
|
4163
|
+
Replace this footer.
|
|
4164
|
+
</div>
|
|
4165
|
+
</footer>
|
|
4166
|
+
</div>
|
|
4167
|
+
)
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
export default App
|
|
4171
|
+
`,
|
|
4172
|
+
"src/index.css": '@import "tailwindcss";\n\n/* Design tokens. Tailwind v4 turns every entry here into a utility\n (--color-brand -> bg-brand/text-brand), so restyle the project by editing\n these values rather than sprinkling hex codes through the components. */\n@theme {\n --color-brand: #4f46e5;\n --color-brand-contrast: #ffffff;\n --color-surface: #ffffff;\n --color-surface-muted: #f5f5f7;\n --color-ink: #111827;\n --color-ink-muted: #6b7280;\n --radius-card: 0.75rem;\n}\n\n:root {\n color-scheme: light;\n font-family: Inter, ui-sans-serif, system-ui, sans-serif;\n --app-bg: #ffffff;\n --app-fg: #111827;\n}\n\n/* An opaque background on html, body is mandatory \u2014 the page is never\n transparent. Keep this rule when you restyle. */\nhtml, body {\n margin: 0;\n min-height: 100%;\n min-width: 320px;\n background: var(--app-bg);\n color: var(--app-fg);\n}\n\n/* Keyboard focus baseline. Components may override it, but never remove it:\n the platform appends a generic fallback to any project whose CSS contains\n no `:focus` rule at all, and a real one belongs here. */\n:focus-visible {\n outline: 2px solid var(--color-brand, currentColor);\n outline-offset: 2px;\n}\n',
|
|
4173
|
+
"src/main.tsx": "import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App'\nimport './index.css'\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>,\n)\n",
|
|
4174
|
+
"src/vite-env.d.ts": '/// <reference types="vite/client" />\n',
|
|
4175
|
+
"tsconfig.json": '{\n "compilerOptions": {\n "target": "ES2020",\n "useDefineForClassFields": true,\n "lib": ["ES2020", "DOM", "DOM.Iterable"],\n "module": "ESNext",\n "skipLibCheck": true,\n "moduleResolution": "bundler",\n "allowImportingTsExtensions": true,\n "resolveJsonModule": true,\n "isolatedModules": true,\n "noEmit": true,\n "jsx": "react-jsx",\n "strict": true\n },\n "include": ["src"]\n}\n',
|
|
4176
|
+
"vite.config.ts": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport tailwindcss from '@tailwindcss/vite'\n\nexport default defineConfig({\n base: '/',\n plugins: [tailwindcss(), react()],\n})\n"
|
|
4177
|
+
}
|
|
4178
|
+
},
|
|
4179
|
+
vue: {
|
|
4180
|
+
framework: "vite",
|
|
4181
|
+
files: {
|
|
4182
|
+
".gitignore": "# Dependencies\nnode_modules/\n.pnpm-store/\n\n# Build output\ndist/\nbuild/\nout/\n.next/\n.nuxt/\n.output/\n.svelte-kit/\n.astro/\n.vercel/\n.netlify/\n.wrangler/\n.turbo/\n\n# Environment\n.env\n.env.local\n.env.*.local\n\n# IDE\n.idea/\n.vscode/\n*.swp\n*.swo\n*~\n\n# OS\n.DS_Store\nThumbs.db\n\n# Logs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Test coverage\ncoverage/\n.nyc_output/\n\n# Cache\n.cache/\n.parcel-cache/\n",
|
|
4183
|
+
"eslint.config.mjs": "import tseslint from 'typescript-eslint'\nimport vue from 'eslint-plugin-vue'\n\nexport default tseslint.config(\n { ignores: ['dist'] },\n {\n files: ['**/*.ts'],\n extends: [tseslint.configs.base],\n rules: {\n '@typescript-eslint/no-unused-vars': [\n 'error',\n { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },\n ],\n },\n },\n ...vue.configs['flat/essential'],\n {\n files: ['**/*.vue'],\n languageOptions: { parserOptions: { parser: tseslint.parser } },\n rules: {\n 'vue/multi-word-component-names': 'off',\n },\n },\n)\n",
|
|
4184
|
+
"index.html": '<!doctype html>\n<html lang="en">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <meta name="description" content="Replace this with a one-sentence description of the app." />\n <link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n <title>Replace this title</title>\n </head>\n <body>\n <div id="app"></div>\n <script type="module" src="./src/main.ts"></script>\n </body>\n</html>\n',
|
|
4185
|
+
"package.json": '{\n "name": "agentful-starter-vue",\n "private": true,\n "version": "0.1.0",\n "type": "module",\n "scripts": {\n "dev": "vite",\n "build": "vite build",\n "preview": "vite preview",\n "verify": "vue-tsc --noEmit && eslint . --max-warnings 0 && vite build"\n },\n "dependencies": {\n "vue": "^3.5.0"\n },\n "devDependencies": {\n "@tailwindcss/vite": "^4.0.0",\n "@vitejs/plugin-vue": "^6.0.0",\n "eslint": "^9.0.0",\n "eslint-plugin-vue": "^10.0.0",\n "tailwindcss": "^4.0.0",\n "typescript": "^5.0.0",\n "typescript-eslint": "^8.0.0",\n "vite": "^6.0.0",\n "vue-tsc": "^2.2.0"\n }\n}\n',
|
|
4186
|
+
"public/favicon.svg": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Site icon">\n <rect width="32" height="32" rx="7" fill="#4f46e5"/>\n <path d="M10 22V10h5.2a4.2 4.2 0 0 1 0 8.4H13" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>\n <path d="M16 18.4 22 22" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round"/>\n</svg>\n',
|
|
4187
|
+
"src/App.vue": `<script setup lang="ts">
|
|
4188
|
+
import { ref } from 'vue'
|
|
4189
|
+
|
|
4190
|
+
type Feature = {
|
|
4191
|
+
title: string
|
|
4192
|
+
body: string
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
// Replace this data with the real content of the project.
|
|
4196
|
+
const features: Feature[] = [
|
|
4197
|
+
{ title: 'First point', body: 'Replace this with a benefit the visitor cares about.' },
|
|
4198
|
+
{ title: 'Second point', body: 'Keep each card to one idea and one sentence.' },
|
|
4199
|
+
{ title: 'Third point', body: 'Delete a card if the project only needs two.' },
|
|
4200
|
+
]
|
|
4201
|
+
|
|
4202
|
+
const nav = [
|
|
4203
|
+
{ href: '#features', label: 'Features' },
|
|
4204
|
+
{ href: '#contact', label: 'Contact' },
|
|
4205
|
+
]
|
|
4206
|
+
|
|
4207
|
+
const navOpen = ref(false)
|
|
4208
|
+
</script>
|
|
4209
|
+
|
|
4210
|
+
<template>
|
|
4211
|
+
<div class="min-h-screen flex flex-col">
|
|
4212
|
+
<header class="border-b border-black/10">
|
|
4213
|
+
<div class="mx-auto max-w-5xl px-4 sm:px-6 py-4 flex items-center justify-between gap-4">
|
|
4214
|
+
<a href="#main" class="font-semibold text-ink rounded">Brand</a>
|
|
4215
|
+
|
|
4216
|
+
<nav aria-label="Main" class="hidden sm:block">
|
|
4217
|
+
<ul class="flex items-center gap-6">
|
|
4218
|
+
<li v-for="item in nav" :key="item.href">
|
|
4219
|
+
<a :href="item.href" class="text-ink-muted hover:text-ink rounded">{{ item.label }}</a>
|
|
4220
|
+
</li>
|
|
4221
|
+
</ul>
|
|
4222
|
+
</nav>
|
|
4223
|
+
|
|
4224
|
+
<button
|
|
4225
|
+
type="button"
|
|
4226
|
+
class="sm:hidden rounded px-3 py-2 text-ink border border-black/10"
|
|
4227
|
+
:aria-expanded="navOpen"
|
|
4228
|
+
aria-controls="mobile-nav"
|
|
4229
|
+
@click="navOpen = !navOpen"
|
|
4230
|
+
>
|
|
4231
|
+
Menu
|
|
4232
|
+
</button>
|
|
4233
|
+
</div>
|
|
4234
|
+
|
|
4235
|
+
<nav v-if="navOpen" id="mobile-nav" aria-label="Main" class="sm:hidden border-t border-black/10">
|
|
4236
|
+
<ul class="mx-auto max-w-5xl px-4 py-2 flex flex-col">
|
|
4237
|
+
<li v-for="item in nav" :key="item.href">
|
|
4238
|
+
<a
|
|
4239
|
+
:href="item.href"
|
|
4240
|
+
class="block py-2 text-ink-muted hover:text-ink rounded"
|
|
4241
|
+
@click="navOpen = false"
|
|
4242
|
+
>{{ item.label }}</a>
|
|
4243
|
+
</li>
|
|
4244
|
+
</ul>
|
|
4245
|
+
</nav>
|
|
4246
|
+
</header>
|
|
4247
|
+
|
|
4248
|
+
<main id="main" class="flex-1">
|
|
4249
|
+
<section class="mx-auto max-w-5xl px-4 sm:px-6 py-16 sm:py-24">
|
|
4250
|
+
<h1 class="text-4xl sm:text-5xl font-semibold tracking-tight text-ink text-balance">
|
|
4251
|
+
Replace this headline
|
|
4252
|
+
</h1>
|
|
4253
|
+
<p class="mt-4 max-w-2xl text-lg text-ink-muted">
|
|
4254
|
+
Replace this paragraph with one sentence that says what the product does
|
|
4255
|
+
and who it is for.
|
|
4256
|
+
</p>
|
|
4257
|
+
<a
|
|
4258
|
+
href="#contact"
|
|
4259
|
+
class="mt-8 inline-flex items-center rounded-card bg-brand px-5 py-3 font-medium text-brand-contrast hover:opacity-90"
|
|
4260
|
+
>
|
|
4261
|
+
Primary action
|
|
4262
|
+
</a>
|
|
4263
|
+
</section>
|
|
4264
|
+
|
|
4265
|
+
<section id="features" class="mx-auto max-w-5xl px-4 sm:px-6 pb-16 sm:pb-24">
|
|
4266
|
+
<h2 class="text-2xl font-semibold tracking-tight text-ink">Replace this section title</h2>
|
|
4267
|
+
<ul class="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
4268
|
+
<li v-for="feature in features" :key="feature.title" class="rounded-card bg-surface-muted p-6">
|
|
4269
|
+
<h3 class="font-medium text-ink">{{ feature.title }}</h3>
|
|
4270
|
+
<p class="mt-2 text-ink-muted">{{ feature.body }}</p>
|
|
4271
|
+
</li>
|
|
4272
|
+
</ul>
|
|
4273
|
+
</section>
|
|
4274
|
+
</main>
|
|
4275
|
+
|
|
4276
|
+
<footer id="contact" class="border-t border-black/10">
|
|
4277
|
+
<div class="mx-auto max-w-5xl px-4 sm:px-6 py-8 text-sm text-ink-muted">
|
|
4278
|
+
Replace this footer.
|
|
4279
|
+
</div>
|
|
4280
|
+
</footer>
|
|
4281
|
+
</div>
|
|
4282
|
+
</template>
|
|
4283
|
+
`,
|
|
4284
|
+
"src/env.d.ts": '/// <reference types="vite/client" />\n',
|
|
4285
|
+
"src/main.ts": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport './style.css'\n\ncreateApp(App).mount('#app')\n",
|
|
4286
|
+
"src/shims-vue.d.ts": "/* Lets plain `tsc` resolve `*.vue` imports.\n *\n * Load-bearing: the platform's prebuild gate runs `tsc --noEmit`, never\n * `vue-tsc`. Without this shim it stops at\n * src/main.ts: error TS2307: Cannot find module './App.vue'\n * and every build fails on a phantom error.\n *\n * The shim types an SFC as a generic component, so `tsc` checks all `.ts`\n * files properly but not the internals of a `.vue` file. `npm run verify`\n * runs `vue-tsc --noEmit`, which does check those \u2014 use it before finishing.\n */\ndeclare module '*.vue' {\n import type { DefineComponent } from 'vue'\n const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>\n export default component\n}\n",
|
|
4287
|
+
"src/style.css": '@import "tailwindcss";\n\n/* Design tokens. Tailwind v4 turns every entry here into a utility\n (--color-brand -> bg-brand/text-brand), so restyle the project by editing\n these values rather than sprinkling hex codes through the components. */\n@theme {\n --color-brand: #41b883;\n --color-brand-contrast: #ffffff;\n --color-surface: #ffffff;\n --color-surface-muted: #f5f5f7;\n --color-ink: #111827;\n --color-ink-muted: #6b7280;\n --radius-card: 0.75rem;\n}\n\n:root {\n color-scheme: light;\n font-family: Inter, ui-sans-serif, system-ui, sans-serif;\n --app-bg: #ffffff;\n --app-fg: #111827;\n}\n\n/* An opaque background on html, body is mandatory \u2014 the page is never\n transparent. Keep this rule when you restyle. */\nhtml, body {\n margin: 0;\n min-height: 100%;\n min-width: 320px;\n background: var(--app-bg);\n color: var(--app-fg);\n}\n\n/* Keyboard focus baseline. Components may override it, but never remove it:\n the platform appends a generic fallback to any project whose CSS contains\n no `:focus` rule at all, and a real one belongs here. */\n:focus-visible {\n outline: 2px solid var(--color-brand, currentColor);\n outline-offset: 2px;\n}\n',
|
|
4288
|
+
"tsconfig.json": '{\n "compilerOptions": {\n "target": "ES2020",\n "useDefineForClassFields": true,\n "module": "ESNext",\n "lib": ["ES2020", "DOM", "DOM.Iterable"],\n "skipLibCheck": true,\n "moduleResolution": "bundler",\n "allowImportingTsExtensions": true,\n "resolveJsonModule": true,\n "isolatedModules": true,\n "noEmit": true,\n "jsx": "preserve",\n "strict": true\n },\n "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]\n}\n',
|
|
4289
|
+
"vite.config.ts": "import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport tailwindcss from '@tailwindcss/vite'\n\nexport default defineConfig({\n base: '/',\n plugins: [tailwindcss(), vue()],\n})\n"
|
|
4290
|
+
}
|
|
4291
|
+
},
|
|
4292
|
+
vanilla: {
|
|
4293
|
+
framework: "vanilla",
|
|
4294
|
+
files: {
|
|
4295
|
+
".gitignore": ".DS_Store\nnode_modules/\n*.log\n",
|
|
4296
|
+
"assets/favicon.svg": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Site icon">\n <rect width="32" height="32" rx="7" fill="#2f5bea"/>\n <path d="M10 21.5 16 9l6 12.5h-3.1L16 15.4l-2.9 6.1z" fill="#fff"/>\n</svg>\n',
|
|
4297
|
+
"css/style.css": "/* Site styles. Tokens live in tokens.css and are loaded before this file. */\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n min-height: 100%;\n margin: 0;\n /* Opaque, always: a transparent page adopts the host background and the\n * text can disappear entirely on a dark preview. */\n background: var(--color-bg);\n color: var(--color-fg);\n font-family: var(--font-body);\n font-size: var(--text-base);\n line-height: var(--leading-body);\n -webkit-text-size-adjust: 100%;\n}\n\nbody {\n overflow-x: hidden; /* no horizontal scroll at any breakpoint */\n}\n\nh1,\nh2,\nh3 {\n margin: 0 0 var(--space-4);\n font-family: var(--font-heading);\n line-height: var(--leading-heading);\n text-wrap: balance;\n}\n\nh1 { font-size: var(--text-h1); }\nh2 { font-size: var(--text-h2); }\nh3 { font-size: var(--text-h3); }\n\np {\n margin: 0 0 var(--space-4);\n}\n\na {\n color: var(--color-accent);\n}\n\nimg,\nsvg,\nvideo {\n max-width: 100%;\n height: auto;\n}\n\n/* Visible focus for keyboard users \u2014 never remove this without a replacement. */\n:focus-visible {\n outline: 2px solid var(--color-accent);\n outline-offset: 2px;\n border-radius: 4px;\n}\n\n.skip-link {\n position: absolute;\n left: var(--space-4);\n top: calc(var(--space-4) * -4);\n z-index: 10;\n padding: var(--space-2) var(--space-4);\n background: var(--color-accent);\n color: var(--color-accent-contrast);\n border-radius: var(--radius);\n text-decoration: none;\n transition: top 0.15s ease;\n}\n\n.skip-link:focus {\n top: var(--space-4);\n}\n\n/* \u2500\u2500 Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.container {\n width: 100%;\n max-width: var(--container-max);\n margin-inline: auto;\n padding-inline: var(--space-4);\n}\n\n.section {\n padding-block: var(--space-section);\n}\n\n.section--muted {\n background: var(--color-surface);\n}\n\n.prose {\n max-width: var(--measure);\n}\n\n.grid {\n display: grid;\n gap: var(--space-6);\n}\n\n.grid--cards {\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));\n}\n\n/* \u2500\u2500 Components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.site-header {\n border-bottom: var(--border);\n background: var(--color-bg);\n}\n\n.header-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-4);\n padding-block: var(--space-4);\n}\n\n.brand {\n font-weight: 700;\n text-decoration: none;\n color: inherit;\n}\n\n.nav-toggle {\n display: none;\n padding: var(--space-2) var(--space-3);\n font: inherit;\n color: inherit;\n background: transparent;\n border: var(--border);\n border-radius: var(--radius);\n cursor: pointer;\n}\n\n.nav-list {\n display: flex;\n gap: var(--space-6);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.nav-list a {\n color: inherit;\n text-decoration: none;\n}\n\n.nav-list a:hover {\n color: var(--color-accent);\n}\n\n.hero {\n padding-block: var(--space-section);\n}\n\n.hero__inner {\n max-width: var(--measure);\n}\n\n.lede {\n font-size: var(--text-lede);\n color: var(--color-muted);\n}\n\n.button {\n display: inline-block;\n padding: var(--space-3) var(--space-6);\n background: var(--color-accent);\n color: var(--color-accent-contrast);\n border-radius: var(--radius);\n font-weight: 600;\n text-decoration: none;\n}\n\n.button:hover {\n filter: brightness(1.08);\n}\n\n.card {\n padding: var(--space-6);\n background: var(--color-bg);\n border: var(--border);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow);\n}\n\n.card h3 {\n margin-bottom: var(--space-2);\n}\n\n.card p:last-child {\n margin-bottom: 0;\n}\n\n.site-footer {\n padding-block: var(--space-8);\n border-top: var(--border);\n color: var(--color-muted);\n}\n\n.site-footer p {\n margin: 0;\n}\n\n/* \u2500\u2500 Reveal on scroll \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* Hidden ONLY when JS is running: main.js sets the `js` class on <html>. With\n * JS off (or if the script fails) the content stays visible instead of the\n * page rendering blank. Never write `.reveal { opacity: 0 }` unguarded. */\n\n.js .reveal {\n opacity: 0;\n transform: translateY(12px);\n transition: opacity 0.5s ease, transform 0.5s ease;\n}\n\n.js .reveal.is-visible {\n opacity: 1;\n transform: none;\n}\n\n/* \u2500\u2500 Responsive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n@media (max-width: 40rem) {\n .nav-toggle {\n display: block;\n }\n\n .site-nav {\n display: none;\n flex-basis: 100%;\n }\n\n .site-nav.is-open {\n display: block;\n }\n\n .header-bar {\n flex-wrap: wrap;\n }\n\n .nav-list {\n flex-direction: column;\n gap: var(--space-3);\n padding-block: var(--space-4);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n scroll-behavior: auto !important;\n }\n}\n",
|
|
4298
|
+
"css/tokens.css": '/* Design tokens.\n *\n * One place for colour, spacing, type and radius. Change the values here and\n * the whole site follows \u2014 that is the point of this file.\n *\n * ARITHMETIC: CSS has no implicit maths. A value like\n * padding: var(--space-4) * 2 <- INVALID, the browser drops the\n * WHOLE declaration and the element\n * silently loses its padding\n * must be written with calc():\n * padding: calc(var(--space-4) * 2)\n * The derived tokens below show the correct form.\n */\n\n:root {\n /* Colour \u2014 an opaque page background is mandatory, never transparent. */\n --color-bg: #ffffff;\n --color-fg: #111827;\n --color-muted: #4b5563;\n --color-surface: #f7f8fa;\n --color-border: #e4e7ec;\n --color-accent: #2f5bea;\n --color-accent-contrast: #ffffff;\n\n /* Spacing scale. */\n --space-1: 0.25rem;\n --space-2: 0.5rem;\n --space-3: 0.75rem;\n --space-4: 1rem;\n --space-6: 1.5rem;\n --space-8: 2rem;\n --space-12: 3rem;\n /* Derived values use calc() \u2014 see the note at the top of this file. */\n --space-section: calc(var(--space-12) * 2);\n\n /* Type. */\n --font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;\n --font-heading: var(--font-body);\n --text-base: 1rem;\n --text-lede: clamp(1.05rem, 0.95rem + 0.5vw, 1.25rem);\n --text-h3: clamp(1.15rem, 1rem + 0.6vw, 1.4rem);\n --text-h2: clamp(1.5rem, 1.2rem + 1.4vw, 2.1rem);\n --text-h1: clamp(2rem, 1.4rem + 3vw, 3.5rem);\n --leading-body: 1.6;\n --leading-heading: 1.15;\n\n /* Shape. */\n --radius: 10px;\n --radius-lg: calc(var(--radius) * 2);\n --border: 1px solid var(--color-border);\n --shadow: 0 1px 2px rgb(17 24 39 / 6%), 0 8px 24px rgb(17 24 39 / 6%);\n\n /* Layout. */\n --container-max: 72rem;\n --measure: 68ch;\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --color-bg: #0f1117;\n --color-fg: #f3f4f6;\n --color-muted: #a8b0bd;\n --color-surface: #171a22;\n --color-border: #262b36;\n --color-accent: #7d9bff;\n --color-accent-contrast: #0f1117;\n --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 8px 24px rgb(0 0 0 / 35%);\n }\n}\n',
|
|
4299
|
+
"index.html": '<!doctype html>\n<html lang="en" class="no-js">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <meta name="description" content="Replace this with a one-sentence description of the site." />\n <link rel="icon" href="./assets/favicon.svg" type="image/svg+xml" />\n <link rel="stylesheet" href="./css/tokens.css" />\n <link rel="stylesheet" href="./css/style.css" />\n <title>Replace this title</title>\n </head>\n <body>\n <a class="skip-link" href="#main">Skip to content</a>\n\n <header class="site-header">\n <div class="container header-bar">\n <a class="brand" href="./index.html">Brand</a>\n <button\n class="nav-toggle"\n type="button"\n aria-expanded="false"\n aria-controls="primary-nav"\n >\n <span class="nav-toggle__label">Menu</span>\n </button>\n <nav id="primary-nav" class="site-nav" aria-label="Primary">\n <ul class="nav-list">\n <li><a href="#features">Features</a></li>\n <li><a href="#about">About</a></li>\n <li><a href="#contact">Contact</a></li>\n </ul>\n </nav>\n </div>\n </header>\n\n <main id="main">\n <section class="hero">\n <div class="container hero__inner">\n <h1>Replace this headline</h1>\n <p class="lede">\n Replace this supporting sentence with the real value proposition.\n </p>\n <a class="button" href="#contact">Primary action</a>\n </div>\n </section>\n\n <section id="features" class="section">\n <div class="container">\n <h2>Section heading</h2>\n <div class="grid grid--cards">\n <article class="card reveal">\n <h3>Card heading</h3>\n <p>Replace this card with real content.</p>\n </article>\n <article class="card reveal">\n <h3>Card heading</h3>\n <p>Replace this card with real content.</p>\n </article>\n <article class="card reveal">\n <h3>Card heading</h3>\n <p>Replace this card with real content.</p>\n </article>\n </div>\n </div>\n </section>\n\n <section id="about" class="section section--muted">\n <div class="container prose">\n <h2>Section heading</h2>\n <p>Replace this paragraph with real content.</p>\n </div>\n </section>\n\n <section id="contact" class="section">\n <div class="container prose">\n <h2>Get in touch</h2>\n <p>Replace this paragraph with real content.</p>\n </div>\n </section>\n </main>\n\n <footer class="site-footer">\n <div class="container">\n <p>© <span data-current-year>2026</span> Brand. All rights reserved.</p>\n </div>\n </footer>\n\n <script type="module" src="./js/main.js"></script>\n </body>\n</html>\n',
|
|
4300
|
+
"js/main.js": `/* Entry module.
|
|
4301
|
+
*
|
|
4302
|
+
* index.html loads this with <script type="module" src="./js/main.js"></script>.
|
|
4303
|
+
* The \`type="module"\` is what makes the import below legal \u2014 without it the
|
|
4304
|
+
* browser throws "Cannot use import statement outside a module" and NOTHING on
|
|
4305
|
+
* the page runs. Keep it on every script that imports or exports.
|
|
4306
|
+
*/
|
|
4307
|
+
|
|
4308
|
+
import { initNav, initReveal, setCurrentYear } from './ui.js';
|
|
4309
|
+
|
|
4310
|
+
// Tell CSS that JS is alive. Anything hidden by default must be hidden behind
|
|
4311
|
+
// this class, so a script failure leaves the content visible instead of blank.
|
|
4312
|
+
document.documentElement.classList.remove('no-js');
|
|
4313
|
+
document.documentElement.classList.add('js');
|
|
4314
|
+
|
|
4315
|
+
initNav();
|
|
4316
|
+
initReveal();
|
|
4317
|
+
setCurrentYear();
|
|
4318
|
+
`,
|
|
4319
|
+
"js/ui.js": `/* Small, self-contained UI behaviours.
|
|
4320
|
+
*
|
|
4321
|
+
* Every function is a NAMED EXPORT and main.js imports it by that name. When
|
|
4322
|
+
* you add a behaviour, export it here and import it there \u2014 if the two names
|
|
4323
|
+
* disagree the browser throws
|
|
4324
|
+
* "The requested module './ui.js' does not provide an export named '...'"
|
|
4325
|
+
* and the whole page stops running.
|
|
4326
|
+
*/
|
|
4327
|
+
|
|
4328
|
+
/** Mobile navigation toggle, wired to aria-expanded for screen readers. */
|
|
4329
|
+
export function initNav() {
|
|
4330
|
+
const toggle = document.querySelector('.nav-toggle');
|
|
4331
|
+
const nav = document.querySelector('#primary-nav');
|
|
4332
|
+
if (!toggle || !nav) return;
|
|
4333
|
+
|
|
4334
|
+
toggle.addEventListener('click', () => {
|
|
4335
|
+
const open = nav.classList.toggle('is-open');
|
|
4336
|
+
toggle.setAttribute('aria-expanded', String(open));
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
/** Fade sections in as they scroll into view; no-op when motion is reduced. */
|
|
4341
|
+
export function initReveal() {
|
|
4342
|
+
const targets = document.querySelectorAll('.reveal');
|
|
4343
|
+
if (!targets.length) return;
|
|
4344
|
+
|
|
4345
|
+
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
4346
|
+
if (reduced || !('IntersectionObserver' in window)) {
|
|
4347
|
+
targets.forEach((element) => element.classList.add('is-visible'));
|
|
4348
|
+
return;
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
const observer = new IntersectionObserver(
|
|
4352
|
+
(entries) => {
|
|
4353
|
+
entries.forEach((entry) => {
|
|
4354
|
+
if (!entry.isIntersecting) return;
|
|
4355
|
+
entry.target.classList.add('is-visible');
|
|
4356
|
+
observer.unobserve(entry.target);
|
|
4357
|
+
});
|
|
4358
|
+
},
|
|
4359
|
+
{ rootMargin: '0px 0px -10% 0px' },
|
|
4360
|
+
);
|
|
4361
|
+
|
|
4362
|
+
targets.forEach((element) => observer.observe(element));
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
/** Fill every [data-current-year] element, so the footer never goes stale. */
|
|
4366
|
+
export function setCurrentYear() {
|
|
4367
|
+
const year = String(new Date().getFullYear());
|
|
4368
|
+
document
|
|
4369
|
+
.querySelectorAll('[data-current-year]')
|
|
4370
|
+
.forEach((element) => {
|
|
4371
|
+
element.textContent = year;
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4374
|
+
`
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
};
|
|
4379
|
+
|
|
4380
|
+
// src/lib/starters.ts
|
|
4381
|
+
var STARTERS = starters_default.starters;
|
|
4382
|
+
var STARTER_NAMES = Object.keys(STARTERS);
|
|
4383
|
+
var IGNORABLE = /* @__PURE__ */ new Set([".git", ".agentful", ".DS_Store"]);
|
|
4384
|
+
function materializeStarter(name, targetDir, projectName) {
|
|
4385
|
+
const starter = STARTERS[name];
|
|
4386
|
+
if (!starter) {
|
|
4387
|
+
throw new ApiError(
|
|
4388
|
+
0,
|
|
4389
|
+
"unknown_template",
|
|
4390
|
+
`Unknown template '${name}'. Available: ${STARTER_NAMES.join(", ")}.`
|
|
4391
|
+
);
|
|
4392
|
+
}
|
|
4393
|
+
const blockers = (0, import_node_fs3.existsSync)(targetDir) ? (0, import_node_fs3.readdirSync)(targetDir).filter((entry) => !IGNORABLE.has(entry)) : [];
|
|
4394
|
+
if (blockers.length > 0) {
|
|
4395
|
+
throw new ApiError(
|
|
4396
|
+
0,
|
|
4397
|
+
"directory_not_empty",
|
|
4398
|
+
`This directory is not empty (found ${blockers.slice(0, 3).join(", ")}${blockers.length > 3 ? ", \u2026" : ""}). \`init --template\` scaffolds into an empty directory only \u2014 it never overwrites your files.
|
|
4399
|
+
Pass a new folder name instead: \`agentful init --template ${name} my-app\` (creates it for you).`
|
|
4400
|
+
);
|
|
4401
|
+
}
|
|
4402
|
+
let written = 0;
|
|
4403
|
+
for (const [rel, content] of Object.entries(starter.files)) {
|
|
4404
|
+
const abs = (0, import_node_path4.join)(targetDir, rel);
|
|
4405
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path4.dirname)(abs), { recursive: true });
|
|
4406
|
+
let body = content;
|
|
4407
|
+
if (rel === "package.json") {
|
|
4408
|
+
try {
|
|
4409
|
+
const pkg = JSON.parse(content);
|
|
4410
|
+
pkg.name = slugify(projectName) || pkg.name;
|
|
4411
|
+
body = JSON.stringify(pkg, null, 2) + "\n";
|
|
4412
|
+
} catch {
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
(0, import_node_fs3.writeFileSync)(abs, body);
|
|
4416
|
+
written++;
|
|
4417
|
+
}
|
|
4418
|
+
return { written, framework: starter.framework };
|
|
4419
|
+
}
|
|
4420
|
+
function slugify(value) {
|
|
4421
|
+
return value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
4422
|
+
}
|
|
4423
|
+
function prepareScaffoldDirectory(baseDir, directory) {
|
|
4424
|
+
const target = (0, import_node_path4.resolve)(baseDir, directory);
|
|
4425
|
+
(0, import_node_fs3.mkdirSync)(target, { recursive: true });
|
|
4426
|
+
return target;
|
|
4427
|
+
}
|
|
4428
|
+
|
|
3815
4429
|
// src/commands/init.ts
|
|
3816
|
-
async function initCommand(opts) {
|
|
4430
|
+
async function initCommand(opts, directory) {
|
|
3817
4431
|
console.log(banner());
|
|
4432
|
+
if (opts.template && opts.link) {
|
|
4433
|
+
throw new ApiError(
|
|
4434
|
+
0,
|
|
4435
|
+
"invalid_options",
|
|
4436
|
+
"--template scaffolds a new project and cannot be combined with --link."
|
|
4437
|
+
);
|
|
4438
|
+
}
|
|
4439
|
+
if (directory && !opts.template) {
|
|
4440
|
+
throw new ApiError(
|
|
4441
|
+
0,
|
|
4442
|
+
"invalid_options",
|
|
4443
|
+
"A target directory only makes sense with --template \u2014 plain `init` links the directory you are in."
|
|
4444
|
+
);
|
|
4445
|
+
}
|
|
4446
|
+
if (opts.template && directory) {
|
|
4447
|
+
process.chdir(prepareScaffoldDirectory(process.cwd(), directory));
|
|
4448
|
+
}
|
|
3818
4449
|
const existing = readProjectConfig();
|
|
3819
4450
|
if (existing) {
|
|
3820
4451
|
ui.info(`Already initialized (project ${existing.projectId} \u2014 "${existing.title}").`);
|
|
3821
4452
|
ui.info("Delete .agentful/project.json to re-initialize.");
|
|
3822
4453
|
return;
|
|
3823
4454
|
}
|
|
4455
|
+
if (opts.template) {
|
|
4456
|
+
const title2 = opts.title?.trim() || (0, import_node_path5.basename)(process.cwd());
|
|
4457
|
+
ui.step(`Scaffolding from the ${opts.template} starter\u2026`);
|
|
4458
|
+
const result = materializeStarter(opts.template, process.cwd(), title2);
|
|
4459
|
+
ui.ok(`${result.written} files written \u2014 compatible with the cloud build by construction.`);
|
|
4460
|
+
}
|
|
3824
4461
|
const auth = await ensureAuth();
|
|
3825
4462
|
if (opts.link) {
|
|
3826
4463
|
ui.step(`Linking existing project ${opts.link}\u2026`);
|
|
@@ -3828,7 +4465,7 @@ async function initCommand(opts) {
|
|
|
3828
4465
|
writeProjectConfig({
|
|
3829
4466
|
projectId: opts.link,
|
|
3830
4467
|
userId: auth.user_id,
|
|
3831
|
-
title: opts.title || (0,
|
|
4468
|
+
title: opts.title || (0, import_node_path5.basename)(process.cwd()),
|
|
3832
4469
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3833
4470
|
});
|
|
3834
4471
|
ui.ok(`Linked. \`agentful push\` now deploys to project ${opts.link}.`);
|
|
@@ -3837,18 +4474,26 @@ async function initCommand(opts) {
|
|
|
3837
4474
|
let title = opts.title?.trim() || "";
|
|
3838
4475
|
if (!title) {
|
|
3839
4476
|
const rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
|
|
3840
|
-
title = (await rl.question(`Project title [${(0,
|
|
4477
|
+
title = (await rl.question(`Project title [${(0, import_node_path5.basename)(process.cwd())}]: `)).trim() || (0, import_node_path5.basename)(process.cwd());
|
|
3841
4478
|
rl.close();
|
|
3842
4479
|
}
|
|
3843
4480
|
ui.step(`Creating project "${title}"\u2026`);
|
|
3844
4481
|
const config = await createProject(auth, title);
|
|
3845
4482
|
ui.ok(`Project created (${config.projectId}). Next: \`agentful push\``);
|
|
4483
|
+
if (opts.template) {
|
|
4484
|
+
if (directory) ui.info(`Next: \`cd ${directory}\` \u2014 the project lives there now.`);
|
|
4485
|
+
ui.info("Run `npm install` before local dev; `agentful push` builds in the cloud either way.");
|
|
4486
|
+
ui.info(`Or start the coding agent right away: \`agentful\` (templates: ${STARTER_NAMES.join(", ")}).`);
|
|
4487
|
+
}
|
|
3846
4488
|
ui.info("Tip: commit .agentful/project.json to keep the mapping with the repo.");
|
|
3847
4489
|
}
|
|
3848
4490
|
|
|
4491
|
+
// src/commands/push.ts
|
|
4492
|
+
init_branding();
|
|
4493
|
+
|
|
3849
4494
|
// src/lib/zip.ts
|
|
3850
|
-
var
|
|
3851
|
-
var
|
|
4495
|
+
var import_node_fs4 = require("fs");
|
|
4496
|
+
var import_node_path6 = require("path");
|
|
3852
4497
|
|
|
3853
4498
|
// node_modules/fflate/esm/index.mjs
|
|
3854
4499
|
var import_module = require("module");
|
|
@@ -4591,16 +5236,16 @@ var isEnvFile = (name) => name === ".env" || name.startsWith(".env.");
|
|
|
4591
5236
|
function collectFiles(rootDir, mode) {
|
|
4592
5237
|
const ig = (0, import_ignore.default)();
|
|
4593
5238
|
if (mode === "source") {
|
|
4594
|
-
const gitignorePath = (0,
|
|
4595
|
-
if ((0,
|
|
4596
|
-
ig.add((0,
|
|
5239
|
+
const gitignorePath = (0, import_node_path6.join)(rootDir, ".gitignore");
|
|
5240
|
+
if ((0, import_node_fs4.existsSync)(gitignorePath)) {
|
|
5241
|
+
ig.add((0, import_node_fs4.readFileSync)(gitignorePath, "utf8"));
|
|
4597
5242
|
}
|
|
4598
5243
|
}
|
|
4599
5244
|
const results = [];
|
|
4600
5245
|
const walk = (dir) => {
|
|
4601
|
-
for (const entry of (0,
|
|
4602
|
-
const abs = (0,
|
|
4603
|
-
const rel = (0,
|
|
5246
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
5247
|
+
const abs = (0, import_node_path6.join)(dir, entry.name);
|
|
5248
|
+
const rel = (0, import_node_path6.relative)(rootDir, abs).split(import_node_path6.sep).join("/");
|
|
4604
5249
|
const lower = entry.name.toLowerCase();
|
|
4605
5250
|
if (entry.isSymbolicLink()) continue;
|
|
4606
5251
|
if (entry.isDirectory()) {
|
|
@@ -4626,7 +5271,7 @@ function zipToBase64(files) {
|
|
|
4626
5271
|
}
|
|
4627
5272
|
const entries = {};
|
|
4628
5273
|
for (const f of files) {
|
|
4629
|
-
entries[f.rel] = (0,
|
|
5274
|
+
entries[f.rel] = (0, import_node_fs4.readFileSync)(f.abs);
|
|
4630
5275
|
}
|
|
4631
5276
|
const zipped = zipSync(entries, { level: 6 });
|
|
4632
5277
|
if (zipped.byteLength > MAX_ZIP_BYTES) {
|
|
@@ -4644,15 +5289,15 @@ function zipToBase64(files) {
|
|
|
4644
5289
|
}
|
|
4645
5290
|
function detectDistDir(rootDir, override) {
|
|
4646
5291
|
if (override) {
|
|
4647
|
-
const dir = (0,
|
|
4648
|
-
if (!(0,
|
|
5292
|
+
const dir = (0, import_node_path6.join)(rootDir, override);
|
|
5293
|
+
if (!(0, import_node_fs4.existsSync)(dir) || !(0, import_node_fs4.statSync)(dir).isDirectory()) {
|
|
4649
5294
|
throw new ApiError(0, "dist_not_found", `--dir ${override} does not exist or is not a directory.`);
|
|
4650
5295
|
}
|
|
4651
5296
|
return dir;
|
|
4652
5297
|
}
|
|
4653
|
-
for (const candidate of ["dist", "build", "out", (0,
|
|
4654
|
-
const dir = (0,
|
|
4655
|
-
if ((0,
|
|
5298
|
+
for (const candidate of ["dist", "build", "out", (0, import_node_path6.join)(".output", "public")]) {
|
|
5299
|
+
const dir = (0, import_node_path6.join)(rootDir, candidate);
|
|
5300
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path6.join)(dir, "index.html"))) return dir;
|
|
4656
5301
|
}
|
|
4657
5302
|
throw new ApiError(
|
|
4658
5303
|
0,
|
|
@@ -4661,6 +5306,334 @@ function detectDistDir(rootDir, override) {
|
|
|
4661
5306
|
);
|
|
4662
5307
|
}
|
|
4663
5308
|
|
|
5309
|
+
// src/lib/preflight.ts
|
|
5310
|
+
var import_node_fs5 = require("fs");
|
|
5311
|
+
var import_node_path7 = require("path");
|
|
5312
|
+
|
|
5313
|
+
// ../platform-contract/contract.json
|
|
5314
|
+
var contract_default = {
|
|
5315
|
+
$comment: "Single source of truth for the platform publish contract (PRD AP6a, planned/cli-push-to-workspace-transition-findings-and-prd-2026-08-17.md). Consumers: cli/src/lib/preflight.ts renders directly from this file; build engine (infrastructure/lambda/build-engine/handler.ts) and executor (opencode-lambda/executor/runtime/opencode_runner.py) are parity-checked against it by ./check-parity.py until AP4/AP5 move them onto it; AP0 instructions/skills render from it later. Bump schema_version on breaking shape changes. See README.md.",
|
|
5316
|
+
schema_version: 1,
|
|
5317
|
+
contract_statement: "This platform publishes static files \u2014 there is no server process and no build-time env vars.",
|
|
5318
|
+
publish: {
|
|
5319
|
+
server_process: false,
|
|
5320
|
+
build_env_vars: false,
|
|
5321
|
+
published_prefix: "dist/",
|
|
5322
|
+
canonical_output_check: "root_index_html",
|
|
5323
|
+
$comment_output: "Canonical end-to-end contract (AP4): a root-level index.html inside the framework's output dir. Deliberately stricter than the executor's historical index.htm/200.html acceptance \u2014 both builders converge on THIS, not on each other (deployment gate and publish self-heal require dist/index.html)."
|
|
5324
|
+
},
|
|
5325
|
+
limits: {
|
|
5326
|
+
max_zip_upload_bytes: 6291456,
|
|
5327
|
+
$comment_limits: "Compressed zip bytes. Enforced server-side as MAX_TEMPLATE_ZIP_UPLOAD_BYTES (aws-services/workspace-manager/handler.py); ~8 MB as base64, under the API Gateway 10 MB request cap."
|
|
5328
|
+
},
|
|
5329
|
+
frameworks: [
|
|
5330
|
+
{
|
|
5331
|
+
id: "nextjs",
|
|
5332
|
+
display_name: "Next.js (static export)",
|
|
5333
|
+
detect_dependency: "next",
|
|
5334
|
+
build_command: "npm run build",
|
|
5335
|
+
output_dir: "out",
|
|
5336
|
+
requires_config: "next.config.js or next.config.ts with output: 'export' (no config file at all is acceptable \u2014 the cloud build creates one)"
|
|
5337
|
+
},
|
|
5338
|
+
{
|
|
5339
|
+
id: "nuxt",
|
|
5340
|
+
display_name: "Nuxt (static generate)",
|
|
5341
|
+
detect_dependency: "nuxt",
|
|
5342
|
+
build_command: "npm run generate",
|
|
5343
|
+
output_dir: ".output/public",
|
|
5344
|
+
requires_config: "nuxt.config.ts or nuxt.config.js with ssr: false"
|
|
5345
|
+
},
|
|
5346
|
+
{
|
|
5347
|
+
id: "vite",
|
|
5348
|
+
display_name: "Vite",
|
|
5349
|
+
detect_dependency: "vite",
|
|
5350
|
+
build_command: "npm run build",
|
|
5351
|
+
output_dir: "dist"
|
|
5352
|
+
},
|
|
5353
|
+
{
|
|
5354
|
+
id: "cra",
|
|
5355
|
+
display_name: "Create React App",
|
|
5356
|
+
detect_dependency: "react-scripts",
|
|
5357
|
+
build_command: "npm run build",
|
|
5358
|
+
output_dir: "build"
|
|
5359
|
+
},
|
|
5360
|
+
{
|
|
5361
|
+
id: "vanilla",
|
|
5362
|
+
display_name: "plain HTML/CSS/JS without a package.json",
|
|
5363
|
+
detect_dependency: null,
|
|
5364
|
+
build_command: null,
|
|
5365
|
+
output_dir: "."
|
|
5366
|
+
}
|
|
5367
|
+
],
|
|
5368
|
+
$comment_frameworks: "Detection precedence = array order, mirroring the engine's FRAMEWORKS.find(). vanilla applies when no package.json exists; a package.json matching no detect_dependency is 'unknown' and rejected. Do NOT add frameworks the engine does not build: SvelteKit would be classified as 'vite' via its vite dependency and then fail the dist/ check (adapter-static writes build/), Astro would be 'unknown'.",
|
|
5369
|
+
forbidden_server_features: [
|
|
5370
|
+
{
|
|
5371
|
+
id: "pages_api_routes",
|
|
5372
|
+
dirs: ["pages/api", "src/pages/api"],
|
|
5373
|
+
blocking: "always"
|
|
5374
|
+
},
|
|
5375
|
+
{
|
|
5376
|
+
id: "middleware",
|
|
5377
|
+
file_basenames: ["middleware", "proxy"],
|
|
5378
|
+
file_extensions: ["js", "ts", "mjs"],
|
|
5379
|
+
blocking: "always",
|
|
5380
|
+
note: "proxy.* is the Next 16 rename of middleware.*; both run on a server/edge runtime."
|
|
5381
|
+
},
|
|
5382
|
+
{
|
|
5383
|
+
id: "get_server_side_props",
|
|
5384
|
+
blocking: "always",
|
|
5385
|
+
note: "next build rejects getServerSideProps under output: 'export'."
|
|
5386
|
+
},
|
|
5387
|
+
{
|
|
5388
|
+
id: "use_server_actions",
|
|
5389
|
+
blocking: "always",
|
|
5390
|
+
note: "next build rejects 'use server' actions under output: 'export'."
|
|
5391
|
+
},
|
|
5392
|
+
{
|
|
5393
|
+
id: "app_route_handlers",
|
|
5394
|
+
dirs: ["app", "src/app"],
|
|
5395
|
+
file_basenames: ["route"],
|
|
5396
|
+
file_extensions: ["js", "ts", "jsx", "tsx", "mjs"],
|
|
5397
|
+
blocking: "without_static_export",
|
|
5398
|
+
note: "Statically evaluable GET handlers are allowed under output: 'export' \u2014 with the config present this only warns and the cloud build decides."
|
|
5399
|
+
},
|
|
5400
|
+
{
|
|
5401
|
+
id: "server_auth_frameworks",
|
|
5402
|
+
dependencies: ["next-auth"],
|
|
5403
|
+
dependency_prefixes: ["@auth/"],
|
|
5404
|
+
blocking: "with_auth_route",
|
|
5405
|
+
note: "With an actual auth route this blocks; as a bare dependency it cannot run either but breaks nothing at build time \u2014 warn only."
|
|
5406
|
+
}
|
|
5407
|
+
],
|
|
5408
|
+
build_attempt_schema: {
|
|
5409
|
+
file: "deployments/latest-build.json",
|
|
5410
|
+
status_values: ["running", "success", "failed"],
|
|
5411
|
+
phase_codes: ["unsupported_project_type", "install", "build", "missing_output", "upload"],
|
|
5412
|
+
fields: {
|
|
5413
|
+
schema_version: "int \u2014 copy of this contract's schema_version",
|
|
5414
|
+
build_id: "string \u2014 unique per attempt",
|
|
5415
|
+
started_at: "ISO-8601 \u2014 attempt start, written together with status: running",
|
|
5416
|
+
status: "running | success | failed",
|
|
5417
|
+
timestamp: "ISO-8601 \u2014 record write time",
|
|
5418
|
+
phase: "one of phase_codes",
|
|
5419
|
+
framework: "framework id",
|
|
5420
|
+
error: "human-readable cause",
|
|
5421
|
+
details: "raw build output excerpt"
|
|
5422
|
+
},
|
|
5423
|
+
adopted_by: {
|
|
5424
|
+
build_engine: ["schema_version", "build_id", "started_at", "status", "timestamp", "phase", "error", "details", "framework"],
|
|
5425
|
+
executor: ["schema_version", "build_id", "started_at", "status", "timestamp", "phase", "error"],
|
|
5426
|
+
$comment: "Both builders write the attempt identity since AP4/AP5 (2026-08-18); failures carry phase codes and \u2014 for contract violations \u2014 code/reason/hints. The engine additionally writes a running record at start; the executor has no separate build-start moment (started_at is the turn start) and writes no running record. The CLI may graduate from timestamp freshness to identity-based polling for the ENGINE path."
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
};
|
|
5430
|
+
|
|
5431
|
+
// src/lib/preflight.ts
|
|
5432
|
+
function feature(id) {
|
|
5433
|
+
const found = contract_default.forbidden_server_features.find((f) => f.id === id);
|
|
5434
|
+
if (!found) throw new Error(`platform contract is missing forbidden_server_features entry '${id}'`);
|
|
5435
|
+
return found;
|
|
5436
|
+
}
|
|
5437
|
+
var FRAMEWORK_TABLE = contract_default.frameworks;
|
|
5438
|
+
var SUPPORTED_FRAMEWORKS = FRAMEWORK_TABLE.map((f) => f.display_name).map((name, i, all) => i === all.length - 1 ? `or ${name}` : name).join(", ");
|
|
5439
|
+
var PLATFORM_CONTRACT = contract_default.contract_statement;
|
|
5440
|
+
var MAX_ZIP_UPLOAD_BYTES = contract_default.limits.max_zip_upload_bytes;
|
|
5441
|
+
var NEXT_CONFIG_BASENAMES = ["next.config.js", "next.config.ts", "next.config.mjs", "next.config.cjs"];
|
|
5442
|
+
var ROUTES_FEATURE = feature("app_route_handlers");
|
|
5443
|
+
var ROUTE_HANDLER_FILE = new RegExp(
|
|
5444
|
+
`^(${ROUTES_FEATURE.file_basenames.join("|")})\\.(${ROUTES_FEATURE.file_extensions.join("|")})$`
|
|
5445
|
+
);
|
|
5446
|
+
var APP_DIRS = ROUTES_FEATURE.dirs;
|
|
5447
|
+
var PAGES_API_DIRS = feature("pages_api_routes").dirs;
|
|
5448
|
+
var MIDDLEWARE_FEATURE = feature("middleware");
|
|
5449
|
+
var MIDDLEWARE_FILE = new RegExp(
|
|
5450
|
+
`^(${MIDDLEWARE_FEATURE.file_basenames.join("|")})\\.(${MIDDLEWARE_FEATURE.file_extensions.join("|")})$`
|
|
5451
|
+
);
|
|
5452
|
+
var AUTH_FEATURE = feature("server_auth_frameworks");
|
|
5453
|
+
var isAuthDep = (dep) => AUTH_FEATURE.dependencies.includes(dep) || AUTH_FEATURE.dependency_prefixes.some((prefix) => dep.startsWith(prefix));
|
|
5454
|
+
var STATIC_EXPORT = /output\s*:\s*['"]export['"]/;
|
|
5455
|
+
var SOURCE_FILE = /\.(js|jsx|ts|tsx|mjs)$/;
|
|
5456
|
+
var GET_SERVER_SIDE_PROPS = /export\s+(?:async\s+)?(?:function|const|let|var)\s+getServerSideProps\b|export\s*\{[^}]*\bgetServerSideProps\b[^}]*\}/;
|
|
5457
|
+
var USE_SERVER = /^\s*(['"])use server\1\s*;?\s*$/m;
|
|
5458
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "dist", "build", "out"]);
|
|
5459
|
+
var SCAN_FILE_BUDGET = 5e3;
|
|
5460
|
+
function detectFramework(rootDir) {
|
|
5461
|
+
const pkgPath = (0, import_node_path7.join)(rootDir, "package.json");
|
|
5462
|
+
if (!(0, import_node_fs5.existsSync)(pkgPath)) return "vanilla";
|
|
5463
|
+
let pkg;
|
|
5464
|
+
try {
|
|
5465
|
+
pkg = JSON.parse((0, import_node_fs5.readFileSync)(pkgPath, "utf8"));
|
|
5466
|
+
} catch {
|
|
5467
|
+
return "unknown";
|
|
5468
|
+
}
|
|
5469
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5470
|
+
const hit = FRAMEWORK_TABLE.find((f) => f.detect_dependency && deps[f.detect_dependency]);
|
|
5471
|
+
return hit ? hit.id : "unknown";
|
|
5472
|
+
}
|
|
5473
|
+
function readDeps(rootDir) {
|
|
5474
|
+
try {
|
|
5475
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path7.join)(rootDir, "package.json"), "utf8"));
|
|
5476
|
+
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5477
|
+
} catch {
|
|
5478
|
+
return {};
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
function findFiles(rootDir, dir, pattern, out, cap = 20) {
|
|
5482
|
+
if (out.length >= cap || !(0, import_node_fs5.existsSync)(dir)) return;
|
|
5483
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
5484
|
+
if (out.length >= cap) return;
|
|
5485
|
+
const abs = (0, import_node_path7.join)(dir, entry.name);
|
|
5486
|
+
if (entry.isDirectory()) {
|
|
5487
|
+
if (!SKIP_DIRS.has(entry.name)) findFiles(rootDir, abs, pattern, out, cap);
|
|
5488
|
+
} else if (entry.isFile() && pattern.test(entry.name)) {
|
|
5489
|
+
out.push((0, import_node_path7.relative)(rootDir, abs).split(import_node_path7.sep).join("/"));
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
var sample = (list) => list.slice(0, 3).join(", ") + (list.length > 3 ? ", \u2026" : "");
|
|
5494
|
+
function collectSourceFiles(dir, out, budget) {
|
|
5495
|
+
if (budget.left <= 0 || !(0, import_node_fs5.existsSync)(dir)) return;
|
|
5496
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
5497
|
+
if (budget.left <= 0) return;
|
|
5498
|
+
const abs = (0, import_node_path7.join)(dir, entry.name);
|
|
5499
|
+
if (entry.isDirectory()) {
|
|
5500
|
+
if (!SKIP_DIRS.has(entry.name)) collectSourceFiles(abs, out, budget);
|
|
5501
|
+
} else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {
|
|
5502
|
+
budget.left--;
|
|
5503
|
+
out.push(abs);
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
function checkNextjs(rootDir) {
|
|
5508
|
+
const issues = [];
|
|
5509
|
+
const warnings = [];
|
|
5510
|
+
const configName = NEXT_CONFIG_BASENAMES.find((name) => (0, import_node_fs5.existsSync)((0, import_node_path7.join)(rootDir, name)));
|
|
5511
|
+
let hasStaticExport = true;
|
|
5512
|
+
if (configName) {
|
|
5513
|
+
const config = (0, import_node_fs5.readFileSync)((0, import_node_path7.join)(rootDir, configName), "utf8");
|
|
5514
|
+
hasStaticExport = STATIC_EXPORT.test(config);
|
|
5515
|
+
if (!hasStaticExport) {
|
|
5516
|
+
issues.push(
|
|
5517
|
+
`${configName} must set output: 'export' \u2014 the platform serves a static export and the cloud build does not reliably rewrite configs.`
|
|
5518
|
+
);
|
|
5519
|
+
}
|
|
5520
|
+
if (configName.endsWith(".mjs") || configName.endsWith(".cjs")) {
|
|
5521
|
+
issues.push(`${configName}: the cloud build only reads next.config.js or next.config.ts \u2014 rename the config file.`);
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
const hard = [];
|
|
5525
|
+
const pagesApi = [];
|
|
5526
|
+
for (const apiDir of PAGES_API_DIRS) {
|
|
5527
|
+
findFiles(rootDir, (0, import_node_path7.join)(rootDir, apiDir), /\.(js|ts|jsx|tsx|mjs)$/, pagesApi);
|
|
5528
|
+
}
|
|
5529
|
+
if (pagesApi.length > 0) hard.push(`pages/api/* routes (${pagesApi.length} file(s))`);
|
|
5530
|
+
const middlewareFiles = [];
|
|
5531
|
+
for (const base of ["", "src"]) {
|
|
5532
|
+
const dir = base ? (0, import_node_path7.join)(rootDir, base) : rootDir;
|
|
5533
|
+
if (!(0, import_node_fs5.existsSync)(dir)) continue;
|
|
5534
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir)) {
|
|
5535
|
+
if (MIDDLEWARE_FILE.test(entry)) middlewareFiles.push((0, import_node_path7.join)(base, entry).split(import_node_path7.sep).join("/"));
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
5538
|
+
hard.push(...middlewareFiles);
|
|
5539
|
+
const sourceFiles = [];
|
|
5540
|
+
collectSourceFiles(rootDir, sourceFiles, { left: SCAN_FILE_BUDGET });
|
|
5541
|
+
const gssp = [];
|
|
5542
|
+
const useServer = [];
|
|
5543
|
+
for (const abs of sourceFiles) {
|
|
5544
|
+
if (gssp.length >= 20 && useServer.length >= 20) break;
|
|
5545
|
+
let text;
|
|
5546
|
+
try {
|
|
5547
|
+
text = (0, import_node_fs5.readFileSync)(abs, "utf8");
|
|
5548
|
+
} catch {
|
|
5549
|
+
continue;
|
|
5550
|
+
}
|
|
5551
|
+
const rel = (0, import_node_path7.relative)(rootDir, abs).split(import_node_path7.sep).join("/");
|
|
5552
|
+
if (gssp.length < 20 && GET_SERVER_SIDE_PROPS.test(text)) gssp.push(rel);
|
|
5553
|
+
if (useServer.length < 20 && USE_SERVER.test(text)) useServer.push(rel);
|
|
5554
|
+
}
|
|
5555
|
+
if (gssp.length > 0) hard.push(`getServerSideProps (${sample(gssp)})`);
|
|
5556
|
+
if (useServer.length > 0) hard.push(`'use server' actions (${sample(useServer)})`);
|
|
5557
|
+
const routeFiles = [];
|
|
5558
|
+
for (const appDir of APP_DIRS) {
|
|
5559
|
+
findFiles(rootDir, (0, import_node_path7.join)(rootDir, appDir), ROUTE_HANDLER_FILE, routeFiles);
|
|
5560
|
+
}
|
|
5561
|
+
if (routeFiles.length > 0) {
|
|
5562
|
+
const label = `${routeFiles.length} route handler(s) (${sample(routeFiles)})`;
|
|
5563
|
+
if (hasStaticExport) {
|
|
5564
|
+
warnings.push(
|
|
5565
|
+
`${label}: with output: 'export' only route handlers that are statically evaluable at build time survive \u2014 the cloud build will fail on any dynamic handler.`
|
|
5566
|
+
);
|
|
5567
|
+
} else {
|
|
5568
|
+
hard.push(label);
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
5571
|
+
const authDeps = Object.keys(readDeps(rootDir)).filter(isAuthDep);
|
|
5572
|
+
if (authDeps.length > 0) {
|
|
5573
|
+
const isAuthPath = (p) => p.toLowerCase().split("/").some((seg) => seg === "auth" || seg.includes("nextauth"));
|
|
5574
|
+
const authRoutes = [...routeFiles, ...pagesApi].filter(isAuthPath);
|
|
5575
|
+
if (authRoutes.length > 0) {
|
|
5576
|
+
hard.push(`${authDeps.join(", ")} with its server-side auth route (${sample(authRoutes)})`);
|
|
5577
|
+
} else {
|
|
5578
|
+
warnings.push(
|
|
5579
|
+
`${authDeps.join(", ")} is in dependencies but no auth route was found \u2014 server-side auth cannot run on the static platform. Remove the dependency, or keep auth in an external backend called from the client.`
|
|
5580
|
+
);
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
if (hard.length > 0) {
|
|
5584
|
+
issues.push(
|
|
5585
|
+
`${PLATFORM_CONTRACT}
|
|
5586
|
+
Your project needs a server runtime (found: ${hard.join("; ")}).
|
|
5587
|
+
Options: (a) convert to a static export and move server logic to Managed Server Actions,
|
|
5588
|
+
(b) keep the backend elsewhere and call it from the client, (c) don't push this project.`
|
|
5589
|
+
);
|
|
5590
|
+
}
|
|
5591
|
+
return { issues, warnings };
|
|
5592
|
+
}
|
|
5593
|
+
function runPreflight(rootDir) {
|
|
5594
|
+
const framework = detectFramework(rootDir);
|
|
5595
|
+
const issues = [];
|
|
5596
|
+
const warnings = [];
|
|
5597
|
+
if (framework === "unknown") {
|
|
5598
|
+
issues.push(
|
|
5599
|
+
`No supported framework found in package.json. The cloud build supports: ${SUPPORTED_FRAMEWORKS}.`
|
|
5600
|
+
);
|
|
5601
|
+
}
|
|
5602
|
+
if (framework === "nextjs") {
|
|
5603
|
+
const next = checkNextjs(rootDir);
|
|
5604
|
+
issues.push(...next.issues);
|
|
5605
|
+
warnings.push(...next.warnings);
|
|
5606
|
+
}
|
|
5607
|
+
return { framework, issues, warnings };
|
|
5608
|
+
}
|
|
5609
|
+
|
|
5610
|
+
// src/lib/buildStatus.ts
|
|
5611
|
+
init_branding();
|
|
5612
|
+
var DIAGNOSIS_MAX_AGE_MS = 15 * 60 * 1e3;
|
|
5613
|
+
var CLOCK_SKEW_MS = 60 * 1e3;
|
|
5614
|
+
function parseTimestamp(ts) {
|
|
5615
|
+
if (!ts) return null;
|
|
5616
|
+
const ms = Date.parse(ts);
|
|
5617
|
+
return Number.isNaN(ms) ? null : ms;
|
|
5618
|
+
}
|
|
5619
|
+
async function fetchBuildStatus(token, userId, projectId) {
|
|
5620
|
+
return request(
|
|
5621
|
+
`${API_URL}/api/workspaces/${userId}/${projectId}/build-status`,
|
|
5622
|
+
{ token }
|
|
5623
|
+
);
|
|
5624
|
+
}
|
|
5625
|
+
function renderBuildFailure(b) {
|
|
5626
|
+
const head = [
|
|
5627
|
+
"Cloud build failed",
|
|
5628
|
+
b.framework && `framework: ${b.framework}`,
|
|
5629
|
+
b.phase && `phase: ${b.phase}`,
|
|
5630
|
+
b.timestamp && `recorded: ${b.timestamp}`
|
|
5631
|
+
].filter(Boolean).join(" \u2014 ");
|
|
5632
|
+
ui.warn(head);
|
|
5633
|
+
if (b.error) ui.info(b.error);
|
|
5634
|
+
if (b.details && b.details !== b.error) ui.info(String(b.details).slice(0, 4e3));
|
|
5635
|
+
}
|
|
5636
|
+
|
|
4664
5637
|
// src/commands/push.ts
|
|
4665
5638
|
function brandPreviewUrl(previewUrl, projectId) {
|
|
4666
5639
|
const fallback = `https://preview-${projectId}.${PREVIEW_DOMAIN}/`;
|
|
@@ -4677,6 +5650,24 @@ function brandPreviewUrl(previewUrl, projectId) {
|
|
|
4677
5650
|
}
|
|
4678
5651
|
async function pushCommand(opts) {
|
|
4679
5652
|
console.log(banner());
|
|
5653
|
+
const mode = opts.prebuilt ? "dist" : "source";
|
|
5654
|
+
const rootDir = opts.prebuilt ? detectDistDir(process.cwd(), opts.dir) : process.cwd();
|
|
5655
|
+
if (mode === "source") {
|
|
5656
|
+
const pre = runPreflight(process.cwd());
|
|
5657
|
+
for (const warning of pre.warnings) ui.warn(warning);
|
|
5658
|
+
if (pre.issues.length > 0) {
|
|
5659
|
+
if (opts.force) {
|
|
5660
|
+
ui.warn(`Preflight found ${pre.issues.length} blocking issue(s) \u2014 continuing because of --force:`);
|
|
5661
|
+
for (const issue of pre.issues) ui.info(issue);
|
|
5662
|
+
} else {
|
|
5663
|
+
throw new ApiError(
|
|
5664
|
+
0,
|
|
5665
|
+
"preflight_failed",
|
|
5666
|
+
"This project cannot run on the platform as pushed:\n" + pre.issues.map((issue) => `\u2022 ${issue}`).join("\n") + "\nPush with --force to attempt the cloud build anyway."
|
|
5667
|
+
);
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
4680
5671
|
const auth = await ensureAuth();
|
|
4681
5672
|
let project = readProjectConfig();
|
|
4682
5673
|
if (!project) {
|
|
@@ -4692,12 +5683,17 @@ async function pushCommand(opts) {
|
|
|
4692
5683
|
`This project belongs to another account (${userId}); you are logged in as ${auth.email}.`
|
|
4693
5684
|
);
|
|
4694
5685
|
}
|
|
4695
|
-
const mode = opts.prebuilt ? "dist" : "source";
|
|
4696
|
-
const rootDir = opts.prebuilt ? detectDistDir(process.cwd(), opts.dir) : process.cwd();
|
|
4697
5686
|
ui.step(mode === "source" ? "Packing source tree\u2026" : `Packing built output (${rootDir})\u2026`);
|
|
4698
5687
|
const files = collectFiles(rootDir, mode);
|
|
4699
5688
|
const zip = zipToBase64(files);
|
|
4700
5689
|
ui.info(`${zip.count} files, ${(zip.bytes / 1024).toFixed(0)} KB compressed`);
|
|
5690
|
+
if (zip.bytes > MAX_ZIP_UPLOAD_BYTES) {
|
|
5691
|
+
throw new ApiError(
|
|
5692
|
+
0,
|
|
5693
|
+
"zip_too_large",
|
|
5694
|
+
`Packed ${(zip.bytes / 1024 / 1024).toFixed(1)} MB compressed \u2014 the platform caps uploads at ${Math.floor(MAX_ZIP_UPLOAD_BYTES / (1024 * 1024))} MB. Remove large media or generated files from the source tree (node_modules, dist and .git are excluded automatically); large assets belong in the platform asset flow.`
|
|
5695
|
+
);
|
|
5696
|
+
}
|
|
4701
5697
|
ui.step("Uploading to your workspace\u2026");
|
|
4702
5698
|
const upload = await request(
|
|
4703
5699
|
`${API_URL}/api/workspaces/${userId}/${projectId}/source-zip`,
|
|
@@ -4706,6 +5702,7 @@ async function pushCommand(opts) {
|
|
|
4706
5702
|
ui.info(`${upload.files_written} files written`);
|
|
4707
5703
|
if (mode === "source") {
|
|
4708
5704
|
ui.step("Building in the cloud (this can take a few minutes)\u2026");
|
|
5705
|
+
const buildStartedAt = Date.now();
|
|
4709
5706
|
try {
|
|
4710
5707
|
await request(`${PB_URL}/api/workspaces/${userId}/${projectId}/build`, {
|
|
4711
5708
|
method: "POST",
|
|
@@ -4717,8 +5714,11 @@ async function pushCommand(opts) {
|
|
|
4717
5714
|
} catch (err2) {
|
|
4718
5715
|
if (err2 instanceof ApiError && err2.code === "network_error") {
|
|
4719
5716
|
ui.warn("Connection dropped during the build \u2014 polling for the result\u2026");
|
|
4720
|
-
await pollBuild(auth.pb_token, userId, projectId);
|
|
5717
|
+
await pollBuild(auth.pb_token, userId, projectId, buildStartedAt);
|
|
4721
5718
|
} else {
|
|
5719
|
+
if (err2 instanceof ApiError && !err2.details) {
|
|
5720
|
+
await printBuildDiagnosis(auth.pb_token, userId, projectId);
|
|
5721
|
+
}
|
|
4722
5722
|
throw err2;
|
|
4723
5723
|
}
|
|
4724
5724
|
}
|
|
@@ -4733,22 +5733,28 @@ async function pushCommand(opts) {
|
|
|
4733
5733
|
ui.ok(`Live preview: ${ui.url(url)}`);
|
|
4734
5734
|
ui.info(`Manage in the browser: ${ui.url(`https://app.agentful.dev/workspace/${userId}/${projectId}`)}`);
|
|
4735
5735
|
}
|
|
4736
|
-
async function pollBuild(token, userId, projectId) {
|
|
5736
|
+
async function pollBuild(token, userId, projectId, startedAtMs) {
|
|
4737
5737
|
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
5738
|
+
const cutoff = startedAtMs - CLOCK_SKEW_MS;
|
|
4738
5739
|
while (Date.now() < deadline) {
|
|
4739
5740
|
await new Promise((r) => setTimeout(r, 5e3));
|
|
4740
5741
|
try {
|
|
4741
|
-
const status = await
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
if (s === "success" || status.dist_ready && !s) {
|
|
5742
|
+
const status = await fetchBuildStatus(token, userId, projectId);
|
|
5743
|
+
const b = status.build;
|
|
5744
|
+
const recordTs = parseTimestamp(b?.timestamp);
|
|
5745
|
+
const fresh = recordTs !== null && recordTs >= cutoff;
|
|
5746
|
+
if (fresh && b?.status === "success") {
|
|
4747
5747
|
ui.info("Build finished");
|
|
4748
5748
|
return;
|
|
4749
5749
|
}
|
|
4750
|
-
if (
|
|
4751
|
-
|
|
5750
|
+
if (fresh && b?.status === "failed") {
|
|
5751
|
+
renderBuildFailure(b);
|
|
5752
|
+
throw new ApiError(0, "build_failed", `Cloud build failed: ${b?.error || "see workspace logs"}`);
|
|
5753
|
+
}
|
|
5754
|
+
const distTs = parseTimestamp(status.dist_last_modified);
|
|
5755
|
+
if (!b?.status && status.dist_ready && distTs !== null && distTs >= cutoff) {
|
|
5756
|
+
ui.info("Build finished");
|
|
5757
|
+
return;
|
|
4752
5758
|
}
|
|
4753
5759
|
} catch (err2) {
|
|
4754
5760
|
if (err2 instanceof ApiError && err2.code === "build_failed") throw err2;
|
|
@@ -4756,8 +5762,63 @@ async function pollBuild(token, userId, projectId) {
|
|
|
4756
5762
|
}
|
|
4757
5763
|
throw new ApiError(0, "build_timeout", "Build did not finish within 10 minutes. Check the workspace in the browser.");
|
|
4758
5764
|
}
|
|
5765
|
+
async function printBuildDiagnosis(token, userId, projectId) {
|
|
5766
|
+
try {
|
|
5767
|
+
const status = await fetchBuildStatus(token, userId, projectId);
|
|
5768
|
+
const b = status.build;
|
|
5769
|
+
if (!b || b.status && b.status !== "failed") return;
|
|
5770
|
+
const recordTs = parseTimestamp(b.timestamp);
|
|
5771
|
+
if (recordTs !== null && Date.now() - recordTs > DIAGNOSIS_MAX_AGE_MS) {
|
|
5772
|
+
ui.info(`Last recorded cloud build result is from ${b.timestamp} \u2014 too old to explain this failure. Check the workspace in the browser.`);
|
|
5773
|
+
return;
|
|
5774
|
+
}
|
|
5775
|
+
renderBuildFailure(b);
|
|
5776
|
+
} catch {
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
|
|
5780
|
+
// src/commands/buildStatus.ts
|
|
5781
|
+
init_branding();
|
|
5782
|
+
async function buildStatusCommand() {
|
|
5783
|
+
console.log(banner());
|
|
5784
|
+
const project = readProjectConfig();
|
|
5785
|
+
if (!project) {
|
|
5786
|
+
throw new ApiError(
|
|
5787
|
+
0,
|
|
5788
|
+
"not_linked",
|
|
5789
|
+
"No Agentful project here \u2014 nothing has been deployed from this directory yet. Run `agentful push` first."
|
|
5790
|
+
);
|
|
5791
|
+
}
|
|
5792
|
+
const auth = await ensureAuth();
|
|
5793
|
+
const status = await fetchBuildStatus(auth.pb_token, project.userId, project.projectId);
|
|
5794
|
+
const b = status.build;
|
|
5795
|
+
if (!b) {
|
|
5796
|
+
ui.info("No cloud build recorded for this project yet.");
|
|
5797
|
+
if (status.dist_ready) ui.info("A built site exists (dist/ is present), so the preview should work.");
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5800
|
+
if (b.status === "failed") {
|
|
5801
|
+
renderBuildFailure(b);
|
|
5802
|
+
} else {
|
|
5803
|
+
const head = [
|
|
5804
|
+
`Last cloud build: ${b.status || "unknown"}`,
|
|
5805
|
+
b.framework && `framework: ${b.framework}`,
|
|
5806
|
+
b.phase && `phase: ${b.phase}`,
|
|
5807
|
+
b.timestamp && `recorded: ${b.timestamp}`
|
|
5808
|
+
].filter(Boolean).join(" \u2014 ");
|
|
5809
|
+
ui.ok(head);
|
|
5810
|
+
}
|
|
5811
|
+
const recordTs = parseTimestamp(b.timestamp);
|
|
5812
|
+
if (recordTs !== null && Date.now() - recordTs > DIAGNOSIS_MAX_AGE_MS) {
|
|
5813
|
+
ui.info(paint.dim("Note: this record is older than 15 minutes \u2014 it describes an earlier build, not one still running."));
|
|
5814
|
+
}
|
|
5815
|
+
if (status.dist_ready && status.dist_last_modified) {
|
|
5816
|
+
ui.info(paint.dim(`Published output last updated: ${status.dist_last_modified}`));
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
4759
5819
|
|
|
4760
5820
|
// src/commands/open.ts
|
|
5821
|
+
init_branding();
|
|
4761
5822
|
async function openCommand() {
|
|
4762
5823
|
const project = requireProject();
|
|
4763
5824
|
const auth = await ensureAuth();
|
|
@@ -4772,15 +5833,15 @@ async function openCommand() {
|
|
|
4772
5833
|
|
|
4773
5834
|
// src/commands/tui.ts
|
|
4774
5835
|
var import_node_child_process5 = require("child_process");
|
|
4775
|
-
var
|
|
5836
|
+
var import_node_path16 = require("path");
|
|
4776
5837
|
|
|
4777
5838
|
// src/lib/binWrapper.ts
|
|
4778
|
-
var
|
|
4779
|
-
var
|
|
5839
|
+
var import_node_fs6 = require("fs");
|
|
5840
|
+
var import_node_path8 = require("path");
|
|
4780
5841
|
var import_node_os3 = require("os");
|
|
4781
5842
|
function ensureCliOnPath() {
|
|
4782
|
-
const binDir = (0,
|
|
4783
|
-
(0,
|
|
5843
|
+
const binDir = (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".local", "share", "agentful", "bin");
|
|
5844
|
+
(0, import_node_fs6.mkdirSync)(binDir, { recursive: true });
|
|
4784
5845
|
const entry = process.argv[1];
|
|
4785
5846
|
if (!entry) return binDir;
|
|
4786
5847
|
const restore = (name) => {
|
|
@@ -4789,8 +5850,8 @@ function ensureCliOnPath() {
|
|
|
4789
5850
|
` : `export ${name}=${JSON.stringify(original)}
|
|
4790
5851
|
`;
|
|
4791
5852
|
};
|
|
4792
|
-
const wrapper = (0,
|
|
4793
|
-
(0,
|
|
5853
|
+
const wrapper = (0, import_node_path8.join)(binDir, "agentful");
|
|
5854
|
+
(0, import_node_fs6.writeFileSync)(
|
|
4794
5855
|
wrapper,
|
|
4795
5856
|
`#!/bin/sh
|
|
4796
5857
|
# Generated by the Agentful CLI so the coding agent can run \`agentful \u2026\`.
|
|
@@ -4800,22 +5861,26 @@ function ensureCliOnPath() {
|
|
|
4800
5861
|
"utf8"
|
|
4801
5862
|
);
|
|
4802
5863
|
try {
|
|
4803
|
-
(0,
|
|
5864
|
+
(0, import_node_fs6.chmodSync)(wrapper, 493);
|
|
4804
5865
|
} catch {
|
|
4805
5866
|
}
|
|
4806
5867
|
return binDir;
|
|
4807
5868
|
}
|
|
4808
5869
|
|
|
5870
|
+
// src/commands/tui.ts
|
|
5871
|
+
init_branding();
|
|
5872
|
+
|
|
4809
5873
|
// src/lib/engine.ts
|
|
4810
|
-
var
|
|
5874
|
+
var import_node_fs7 = require("fs");
|
|
4811
5875
|
var import_node_crypto = require("crypto");
|
|
4812
|
-
var
|
|
5876
|
+
var import_node_path9 = require("path");
|
|
4813
5877
|
var import_node_os4 = require("os");
|
|
4814
5878
|
var import_node_child_process3 = require("child_process");
|
|
4815
5879
|
var import_node_stream = require("stream");
|
|
4816
5880
|
var import_promises2 = require("stream/promises");
|
|
5881
|
+
init_branding();
|
|
4817
5882
|
var ENGINE_BINARY = "agentful-engine";
|
|
4818
|
-
var FALLBACK_ENGINE_VERSION = "v1.18.
|
|
5883
|
+
var FALLBACK_ENGINE_VERSION = "v1.18.18";
|
|
4819
5884
|
function compareVersions(a, b) {
|
|
4820
5885
|
const pa = a.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
4821
5886
|
const pb = b.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -4849,8 +5914,8 @@ function platformKey() {
|
|
|
4849
5914
|
);
|
|
4850
5915
|
}
|
|
4851
5916
|
function cacheDir(version) {
|
|
4852
|
-
const base = process.env.XDG_CACHE_HOME || (0,
|
|
4853
|
-
return (0,
|
|
5917
|
+
const base = process.env.XDG_CACHE_HOME || (0, import_node_path9.join)((0, import_node_os4.homedir)(), ".cache");
|
|
5918
|
+
return (0, import_node_path9.join)(base, "agentful", "engine", version);
|
|
4854
5919
|
}
|
|
4855
5920
|
async function fetchManifest() {
|
|
4856
5921
|
try {
|
|
@@ -4861,7 +5926,7 @@ async function fetchManifest() {
|
|
|
4861
5926
|
}
|
|
4862
5927
|
async function sha256File(path) {
|
|
4863
5928
|
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
4864
|
-
await (0, import_promises2.pipeline)((0,
|
|
5929
|
+
await (0, import_promises2.pipeline)((0, import_node_fs7.createReadStream)(path), hash);
|
|
4865
5930
|
return hash.digest("hex");
|
|
4866
5931
|
}
|
|
4867
5932
|
async function ensureEngine() {
|
|
@@ -4870,12 +5935,12 @@ async function ensureEngine() {
|
|
|
4870
5935
|
noticeIfOutdated(manifest);
|
|
4871
5936
|
const version = manifest?.engine_version || FALLBACK_ENGINE_VERSION;
|
|
4872
5937
|
const dir = cacheDir(version);
|
|
4873
|
-
const binPath = (0,
|
|
4874
|
-
if ((0,
|
|
5938
|
+
const binPath = (0, import_node_path9.join)(dir, ENGINE_BINARY);
|
|
5939
|
+
if ((0, import_node_fs7.existsSync)(binPath)) return binPath;
|
|
4875
5940
|
const artifact = manifest?.artifacts?.[key];
|
|
4876
5941
|
const url = artifact?.url || `${APP_URL}/cli/engine/${version}/${key}${key.startsWith("darwin") ? ".zip" : ".tar.gz"}`;
|
|
4877
|
-
(0,
|
|
4878
|
-
const archivePath = (0,
|
|
5942
|
+
(0, import_node_fs7.mkdirSync)(dir, { recursive: true });
|
|
5943
|
+
const archivePath = (0, import_node_path9.join)(dir, key.startsWith("darwin") ? "engine.zip" : "engine.tar.gz");
|
|
4879
5944
|
ui.step(`Preparing the ${brand.displayName} engine (first run only)\u2026`);
|
|
4880
5945
|
const resp = await fetch(url, {
|
|
4881
5946
|
redirect: "follow",
|
|
@@ -4889,11 +5954,11 @@ async function ensureEngine() {
|
|
|
4889
5954
|
`Could not download the engine (HTTP ${resp.status}). Check your connection and try again.`
|
|
4890
5955
|
);
|
|
4891
5956
|
}
|
|
4892
|
-
await (0, import_promises2.pipeline)(import_node_stream.Readable.fromWeb(resp.body), (0,
|
|
5957
|
+
await (0, import_promises2.pipeline)(import_node_stream.Readable.fromWeb(resp.body), (0, import_node_fs7.createWriteStream)(archivePath));
|
|
4893
5958
|
if (artifact?.sha256) {
|
|
4894
5959
|
const actual = await sha256File(archivePath);
|
|
4895
5960
|
if (actual !== artifact.sha256) {
|
|
4896
|
-
(0,
|
|
5961
|
+
(0, import_node_fs7.rmSync)(archivePath, { force: true });
|
|
4897
5962
|
throw new ApiError(
|
|
4898
5963
|
0,
|
|
4899
5964
|
"engine_checksum_mismatch",
|
|
@@ -4902,7 +5967,7 @@ async function ensureEngine() {
|
|
|
4902
5967
|
}
|
|
4903
5968
|
}
|
|
4904
5969
|
const extract = archivePath.endsWith(".zip") ? (0, import_node_child_process3.spawnSync)("unzip", ["-oq", archivePath], { cwd: dir, stdio: "ignore" }) : (0, import_node_child_process3.spawnSync)("tar", ["-xzf", archivePath], { cwd: dir, stdio: "ignore" });
|
|
4905
|
-
(0,
|
|
5970
|
+
(0, import_node_fs7.rmSync)(archivePath, { force: true });
|
|
4906
5971
|
if (extract.status !== 0) {
|
|
4907
5972
|
throw new ApiError(
|
|
4908
5973
|
0,
|
|
@@ -4910,24 +5975,24 @@ async function ensureEngine() {
|
|
|
4910
5975
|
`Could not unpack the engine (needs ${archivePath.endsWith(".zip") ? "unzip" : "tar"} on PATH).`
|
|
4911
5976
|
);
|
|
4912
5977
|
}
|
|
4913
|
-
const extracted = (0,
|
|
4914
|
-
if ((0,
|
|
4915
|
-
if (!(0,
|
|
5978
|
+
const extracted = (0, import_node_path9.join)(dir, "opencode");
|
|
5979
|
+
if ((0, import_node_fs7.existsSync)(extracted) && !(0, import_node_fs7.existsSync)(binPath)) (0, import_node_fs7.renameSync)(extracted, binPath);
|
|
5980
|
+
if (!(0, import_node_fs7.existsSync)(binPath)) {
|
|
4916
5981
|
throw new ApiError(0, "engine_extract_failed", "Engine archive did not contain the expected binary.");
|
|
4917
5982
|
}
|
|
4918
|
-
(0,
|
|
5983
|
+
(0, import_node_fs7.chmodSync)(binPath, 493);
|
|
4919
5984
|
ui.ok(`Engine ready.`);
|
|
4920
5985
|
console.log(paint.dim(" powered by opencode (MIT) \xB7 run `agentful licenses` for details"));
|
|
4921
5986
|
return binPath;
|
|
4922
5987
|
}
|
|
4923
5988
|
|
|
4924
5989
|
// src/lib/gatewayToken.ts
|
|
4925
|
-
var
|
|
4926
|
-
var
|
|
4927
|
-
var cachePath = () => (0,
|
|
5990
|
+
var import_node_fs8 = require("fs");
|
|
5991
|
+
var import_node_path10 = require("path");
|
|
5992
|
+
var cachePath = () => (0, import_node_path10.join)(configDir(), "gateway.json");
|
|
4928
5993
|
async function ensureGatewaySession(auth, projectId) {
|
|
4929
5994
|
try {
|
|
4930
|
-
const cached = JSON.parse((0,
|
|
5995
|
+
const cached = JSON.parse((0, import_node_fs8.readFileSync)(cachePath(), "utf8"));
|
|
4931
5996
|
if (cached?.token && cached.expires_at - Date.now() / 1e3 > 3600) {
|
|
4932
5997
|
return cached;
|
|
4933
5998
|
}
|
|
@@ -4938,18 +6003,18 @@ async function ensureGatewaySession(auth, projectId) {
|
|
|
4938
6003
|
body: projectId ? { project_id: projectId } : {},
|
|
4939
6004
|
timeoutMs: 3e4
|
|
4940
6005
|
});
|
|
4941
|
-
(0,
|
|
4942
|
-
(0,
|
|
6006
|
+
(0, import_node_fs8.mkdirSync)(configDir(), { recursive: true });
|
|
6007
|
+
(0, import_node_fs8.writeFileSync)(cachePath(), JSON.stringify(res, null, 2) + "\n", "utf8");
|
|
4943
6008
|
try {
|
|
4944
|
-
(0,
|
|
6009
|
+
(0, import_node_fs8.chmodSync)(cachePath(), 384);
|
|
4945
6010
|
} catch {
|
|
4946
6011
|
}
|
|
4947
6012
|
return res;
|
|
4948
6013
|
}
|
|
4949
6014
|
|
|
4950
6015
|
// src/lib/engineConfig.ts
|
|
4951
|
-
var
|
|
4952
|
-
var
|
|
6016
|
+
var import_node_fs10 = require("fs");
|
|
6017
|
+
var import_node_path12 = require("path");
|
|
4953
6018
|
var import_node_os5 = require("os");
|
|
4954
6019
|
|
|
4955
6020
|
// src/branding/agentful-theme.json
|
|
@@ -5042,60 +6107,8 @@ var agentful_theme_default = {
|
|
|
5042
6107
|
}
|
|
5043
6108
|
};
|
|
5044
6109
|
|
|
5045
|
-
// src/branding/logo.txt
|
|
5046
|
-
var logo_default = " \u2584\u2588\u2588\u2588\u2588\u2584 \u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\n \u2588\u2588\u2588\u2588\u2588\u2588\u2584 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\n \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584 \u2588\u2588\u2588\u2588\n \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\n \u2588\u2588\u2588\u2588\u2580 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\n \u2588\u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\n \u2588\u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\n\u2588\u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588\u2588\u2588\n";
|
|
5047
|
-
|
|
5048
|
-
// src/branding/plugin.ts
|
|
5049
|
-
function brandingPluginSource() {
|
|
5050
|
-
const logoLiteral = JSON.stringify(logo_default.replace(/\n+$/, "").split("\n"));
|
|
5051
|
-
return `// Generated by ${brand.name} v${VERSION} \u2014 do not edit; rewritten on every start.
|
|
5052
|
-
const LOGO = ${logoLiteral}
|
|
5053
|
-
|
|
5054
|
-
const plugin = {
|
|
5055
|
-
id: 'agentful-branding',
|
|
5056
|
-
tui(api) {
|
|
5057
|
-
api.slots.register({
|
|
5058
|
-
order: 1,
|
|
5059
|
-
slots: {
|
|
5060
|
-
home_logo(ctx) {
|
|
5061
|
-
const accent = ctx && ctx.theme ? ctx.theme.primary : undefined
|
|
5062
|
-
const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
|
|
5063
|
-
return (
|
|
5064
|
-
<box flexDirection="column">
|
|
5065
|
-
{LOGO.map((line) => (
|
|
5066
|
-
<text fg={accent}>{line}</text>
|
|
5067
|
-
))}
|
|
5068
|
-
<text> </text>
|
|
5069
|
-
<text fg={muted}>${brand.tagline}</text>
|
|
5070
|
-
</box>
|
|
5071
|
-
)
|
|
5072
|
-
},
|
|
5073
|
-
home_footer(ctx) {
|
|
5074
|
-
const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
|
|
5075
|
-
return <text fg={muted}>${brand.name} v${VERSION}</text>
|
|
5076
|
-
},
|
|
5077
|
-
// Bottom of the sidebar \u2014 this is where the engine prints its own
|
|
5078
|
-
// name and version ("OpenCode 1.18.3"); ours replaces it.
|
|
5079
|
-
sidebar_footer(ctx) {
|
|
5080
|
-
const accent = ctx && ctx.theme ? ctx.theme.primary : undefined
|
|
5081
|
-
const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
|
|
5082
|
-
return (
|
|
5083
|
-
<text>
|
|
5084
|
-
<span fg={accent}>${brand.displayName}</span>
|
|
5085
|
-
<span fg={muted}> v${VERSION}</span>
|
|
5086
|
-
</text>
|
|
5087
|
-
)
|
|
5088
|
-
},
|
|
5089
|
-
},
|
|
5090
|
-
})
|
|
5091
|
-
},
|
|
5092
|
-
}
|
|
5093
|
-
|
|
5094
|
-
export default plugin
|
|
5095
|
-
`;
|
|
5096
|
-
}
|
|
5097
|
-
|
|
5098
6110
|
// src/branding/commands.ts
|
|
6111
|
+
init_branding();
|
|
5099
6112
|
var NEEDS_A_PUSH = 'If the command reports "No Agentful project here", nothing has been deployed from this directory yet \u2014 run `agentful push` first (it creates the project and a preview), then retry.';
|
|
5100
6113
|
var SLASH_COMMANDS = {
|
|
5101
6114
|
setup: {
|
|
@@ -5104,7 +6117,11 @@ var SLASH_COMMANDS = {
|
|
|
5104
6117
|
},
|
|
5105
6118
|
push: {
|
|
5106
6119
|
description: `${brand.displayName}: deploy this project and get a preview URL`,
|
|
5107
|
-
template: "Run `agentful push` from the project root using the bash tool. It creates the project automatically if this directory is not linked yet. Stream what it prints, and when it finishes report the preview URL on its own line. If it fails
|
|
6120
|
+
template: "Run `agentful push` from the project root using the bash tool. It creates the project automatically if this directory is not linked yet. Stream what it prints, and when it finishes report the preview URL on its own line. If it fails: quote the printed error verbatim, and if the failure happened in the cloud (not in the pre-upload preflight), also run `agentful build-status` and quote its phase/error/details as the cause. Diagnose ONLY from those records \u2014 never invent causes, env vars, or settings they do not show. Do not modify project files unless I ask."
|
|
6121
|
+
},
|
|
6122
|
+
"build-status": {
|
|
6123
|
+
description: `${brand.displayName}: show the last cloud build result`,
|
|
6124
|
+
template: "Run `agentful build-status` from the project root using the bash tool and report what it prints: status, phase, framework, when it was recorded, and the error/details verbatim if it failed. That record is the only source of truth about the cloud build \u2014 do not speculate beyond it. " + NEEDS_A_PUSH
|
|
5108
6125
|
},
|
|
5109
6126
|
preview: {
|
|
5110
6127
|
description: `${brand.displayName}: open the live preview in the browser`,
|
|
@@ -5128,7 +6145,7 @@ function commandMarkdown(name) {
|
|
|
5128
6145
|
const description = JSON.stringify(cmd.description);
|
|
5129
6146
|
return `---
|
|
5130
6147
|
description: ${description}
|
|
5131
|
-
agent: build
|
|
6148
|
+
agent: ${cmd.agent ?? "agentful-build"}
|
|
5132
6149
|
---
|
|
5133
6150
|
|
|
5134
6151
|
${cmd.template}
|
|
@@ -5136,36 +6153,171 @@ ${cmd.template}
|
|
|
5136
6153
|
}
|
|
5137
6154
|
|
|
5138
6155
|
// src/lib/tools.ts
|
|
5139
|
-
var
|
|
5140
|
-
var
|
|
6156
|
+
var import_node_fs9 = require("fs");
|
|
6157
|
+
var import_node_path11 = require("path");
|
|
5141
6158
|
function assetsDir() {
|
|
5142
|
-
const here = (0,
|
|
5143
|
-
for (const candidate of [(0,
|
|
5144
|
-
if ((0,
|
|
6159
|
+
const here = (0, import_node_path11.dirname)(process.argv[1] || "");
|
|
6160
|
+
for (const candidate of [(0, import_node_path11.join)(here, "..", "assets"), (0, import_node_path11.join)(here, "assets")]) {
|
|
6161
|
+
if ((0, import_node_fs9.existsSync)((0, import_node_path11.join)(candidate, "tools", "imagegen.ts"))) return candidate;
|
|
5145
6162
|
}
|
|
5146
6163
|
return null;
|
|
5147
6164
|
}
|
|
5148
6165
|
function installEngineTools(configDir2) {
|
|
5149
6166
|
const assets = assetsDir();
|
|
5150
6167
|
if (!assets) return false;
|
|
5151
|
-
const target = (0,
|
|
5152
|
-
(0,
|
|
6168
|
+
const target = (0, import_node_path11.join)(configDir2, "tools");
|
|
6169
|
+
(0, import_node_fs9.mkdirSync)(target, { recursive: true });
|
|
5153
6170
|
try {
|
|
5154
|
-
(0,
|
|
6171
|
+
(0, import_node_fs9.cpSync)((0, import_node_path11.join)(assets, "tools"), target, { recursive: true });
|
|
5155
6172
|
return true;
|
|
5156
6173
|
} catch {
|
|
5157
6174
|
return false;
|
|
5158
6175
|
}
|
|
5159
6176
|
}
|
|
5160
6177
|
|
|
6178
|
+
// src/lib/cloudTarget.ts
|
|
6179
|
+
var CLOUD_INSTRUCTIONS_FILENAME = "AGENTFUL_CLOUD.md";
|
|
6180
|
+
var FRAMEWORKS = contract_default.frameworks;
|
|
6181
|
+
function frameworkDisplayName(framework) {
|
|
6182
|
+
if (framework === "unknown") return "no supported framework";
|
|
6183
|
+
return FRAMEWORKS.find((f) => f.id === framework)?.display_name ?? framework;
|
|
6184
|
+
}
|
|
6185
|
+
function frameworkTable() {
|
|
6186
|
+
const rows = FRAMEWORKS.map((f) => {
|
|
6187
|
+
const build = f.build_command ? `\`${f.build_command}\`` : "no build step";
|
|
6188
|
+
const req = f.requires_config ? ` \u2014 requires ${f.requires_config}` : "";
|
|
6189
|
+
return `- **${f.display_name}** (build: ${build}, output: \`${f.output_dir}\`)${req}`;
|
|
6190
|
+
});
|
|
6191
|
+
return rows.join("\n");
|
|
6192
|
+
}
|
|
6193
|
+
function sessionLine(framework) {
|
|
6194
|
+
const name = frameworkDisplayName(framework);
|
|
6195
|
+
switch (framework) {
|
|
6196
|
+
case "nextjs":
|
|
6197
|
+
return `Detected framework in this directory: **${name}** \u2014 next.config must keep \`output: 'export'\`; no route handlers/middleware/server actions.`;
|
|
6198
|
+
case "unknown":
|
|
6199
|
+
return "Detected framework in this directory: **none of the supported ones** \u2014 a push would be rejected. Fine for local-only work; for a deployable project use one of the frameworks above.";
|
|
6200
|
+
default:
|
|
6201
|
+
return `Detected framework in this directory: **${name}**.`;
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
function renderCloudInstructions(framework) {
|
|
6205
|
+
return `# Agentful Cloud target (platform contract)
|
|
6206
|
+
|
|
6207
|
+
${contract_default.contract_statement}
|
|
6208
|
+
|
|
6209
|
+
This directory can be deployed to Agentful Cloud with \`agentful push\`. When
|
|
6210
|
+
the user wants this project on the platform, everything you build must satisfy
|
|
6211
|
+
this contract. If the user explicitly wants local-only development, any stack
|
|
6212
|
+
is fine \u2014 but then say clearly that \`agentful push\` cannot deploy it.
|
|
6213
|
+
|
|
6214
|
+
## What the platform ships (hard rules)
|
|
6215
|
+
|
|
6216
|
+
- Published apps are **static files** (the framework's build output, published
|
|
6217
|
+
under \`${contract_default.publish.published_prefix}\`) served over HTTPS. There is no server process, no
|
|
6218
|
+
cron, and no native binaries at runtime.
|
|
6219
|
+
- The canonical build output is a root-level \`index.html\` inside the
|
|
6220
|
+
framework's output directory.
|
|
6221
|
+
- There are **no build-time env vars**: the cloud build injects nothing beyond
|
|
6222
|
+
NODE_ENV. There is no settings tab for build secrets \u2014 do not invent one.
|
|
6223
|
+
Backend-tab secrets are runtime-only for Managed Server Actions and never
|
|
6224
|
+
reach a build; the workspace executor injects only allowlisted PUBLIC client
|
|
6225
|
+
vars (\`database.anon_key\`/\`publishable_key\` expanded as
|
|
6226
|
+
\`VITE_\`/\`PUBLIC_\`/\`NEXT_PUBLIC_\`). Never rely on \`.env*\` files reaching the
|
|
6227
|
+
cloud \u2014 the CLI and server both strip them.
|
|
6228
|
+
|
|
6229
|
+
## Supported frameworks (the cloud build table)
|
|
6230
|
+
|
|
6231
|
+
${frameworkTable()}
|
|
6232
|
+
|
|
6233
|
+
Anything else (SvelteKit, Astro, Express, a custom server, \u2026) is **not
|
|
6234
|
+
buildable** on the platform \u2014 do not scaffold it for a cloud-target project.
|
|
6235
|
+
|
|
6236
|
+
## Forbidden in cloud-target Next.js projects
|
|
6237
|
+
|
|
6238
|
+
\`pages/api/**\` routes, \`middleware.*\`/\`proxy.*\`, \`getServerSideProps\`, and
|
|
6239
|
+
\`'use server'\` actions all need a server runtime \u2014 the push preflight rejects
|
|
6240
|
+
them. App-Router route handlers survive \`output: 'export'\` only when they are
|
|
6241
|
+
statically evaluable; prefer client-side fetch or Managed Server Actions.
|
|
6242
|
+
|
|
6243
|
+
## Where server logic belongs
|
|
6244
|
+
|
|
6245
|
+
(a) Managed Server Actions on the platform (runtime secrets live there), or
|
|
6246
|
+
(b) an external backend called from the client. Never a bundled server
|
|
6247
|
+
process, and never a script the user must run locally to produce the result.
|
|
6248
|
+
|
|
6249
|
+
## Diagnosing failed pushes and cloud builds (hard rule)
|
|
6250
|
+
|
|
6251
|
+
Diagnose **only from evidence**: the preflight output of \`agentful push\`, and
|
|
6252
|
+
\`agentful build-status\` (the cloud's own build record \u2014 phase, error, details,
|
|
6253
|
+
timestamp). Quote the record; never invent causes, env vars, or settings that
|
|
6254
|
+
the record does not show. If there is no evidence yet, fetch it first.
|
|
6255
|
+
|
|
6256
|
+
## This session
|
|
6257
|
+
|
|
6258
|
+
${sessionLine(framework)}
|
|
6259
|
+
`;
|
|
6260
|
+
}
|
|
6261
|
+
|
|
5161
6262
|
// src/lib/engineConfig.ts
|
|
5162
6263
|
var PROVIDER_ID = "agentful";
|
|
6264
|
+
var CLOUD_AGENT = "agentful-build";
|
|
6265
|
+
var LOCAL_AGENT = "local-only";
|
|
6266
|
+
var CLOUD_AGENT_PROMPT = `You are the Agentful Cloud build agent: everything you build in this directory targets deployment on Agentful Cloud via \`agentful push\`. Follow the platform contract in AGENTFUL_CLOUD.md strictly \u2014 static files only, no server process, no build-time env vars or secrets. When a push or cloud build fails, diagnose ONLY from the \`agentful push\` output and \`agentful build-status\`; quote the record and never invent causes or settings it does not show. If the user wants development that is not meant for the platform, suggest the ${LOCAL_AGENT} agent (Tab).`;
|
|
6267
|
+
var LOCAL_AGENT_PROMPT = "You are in local-only mode: free local development with no cloud deployment target \u2014 any stack is fine. Be clear that this directory will NOT be deployable with `agentful push` unless it satisfies the platform contract in AGENTFUL_CLOUD.md (static files only). If the user asks to deploy or push, check the contract first and say honestly whether the project can ship on the platform; never claim cloud deployability without evidence.";
|
|
6268
|
+
function buildEngineConfig(opts) {
|
|
6269
|
+
const models = {};
|
|
6270
|
+
for (const entry of opts.catalog.models) models[entry.id] = { name: entry.name };
|
|
6271
|
+
return {
|
|
6272
|
+
model: `${PROVIDER_ID}/${opts.catalog.defaultModel}`,
|
|
6273
|
+
permission: { imagegen: opts.imagegenAvailable ? "allow" : "deny" },
|
|
6274
|
+
theme: "agentful",
|
|
6275
|
+
// We gate engine versions through our own manifest — the engine must not
|
|
6276
|
+
// check for or announce upstream releases behind our back.
|
|
6277
|
+
autoupdate: false,
|
|
6278
|
+
// The platform contract rides `instructions` (ABSOLUTE paths — relative
|
|
6279
|
+
// entries silently load nothing) so it survives agent switches, session
|
|
6280
|
+
// recovery and compaction. Project-side AGENTS.md files are combined by
|
|
6281
|
+
// the engine, never overridden.
|
|
6282
|
+
instructions: opts.instructionPaths,
|
|
6283
|
+
agent: {
|
|
6284
|
+
// The built-in build agent would sit ambiguously next to the cloud
|
|
6285
|
+
// agent ("which one is right?") — hide it. If an older engine ignores
|
|
6286
|
+
// `disable`, it stays visible, which is harmless.
|
|
6287
|
+
build: { disable: true },
|
|
6288
|
+
[CLOUD_AGENT]: {
|
|
6289
|
+
description: "Agentful Cloud \u2014 builds deployable for the platform (default)",
|
|
6290
|
+
mode: "primary",
|
|
6291
|
+
prompt: CLOUD_AGENT_PROMPT
|
|
6292
|
+
},
|
|
6293
|
+
[LOCAL_AGENT]: {
|
|
6294
|
+
description: "Local only \u2014 free local development, not deployable via push",
|
|
6295
|
+
mode: "primary",
|
|
6296
|
+
prompt: LOCAL_AGENT_PROMPT
|
|
6297
|
+
}
|
|
6298
|
+
},
|
|
6299
|
+
// Verified against engine v1.18.18 (binary carries the key; an engine
|
|
6300
|
+
// without it falls back to the built-in default with a warning).
|
|
6301
|
+
default_agent: CLOUD_AGENT,
|
|
6302
|
+
provider: {
|
|
6303
|
+
// User-owned providers first, so the reserved Agentful entry below wins
|
|
6304
|
+
// if a local file tries to claim the same id.
|
|
6305
|
+
...opts.localProviders,
|
|
6306
|
+
[PROVIDER_ID]: {
|
|
6307
|
+
npm: "@ai-sdk/openai-compatible",
|
|
6308
|
+
name: "Agentful",
|
|
6309
|
+
options: { baseURL: opts.session.gateway_url, apiKey: opts.session.token },
|
|
6310
|
+
models
|
|
6311
|
+
}
|
|
6312
|
+
}
|
|
6313
|
+
};
|
|
6314
|
+
}
|
|
5163
6315
|
function engineXdg() {
|
|
5164
|
-
const root = (0,
|
|
6316
|
+
const root = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".local", "share", "agentful");
|
|
5165
6317
|
return {
|
|
5166
|
-
configHome: (0,
|
|
5167
|
-
dataHome: (0,
|
|
5168
|
-
stateHome: (0,
|
|
6318
|
+
configHome: (0, import_node_path12.join)(root, "engine-config"),
|
|
6319
|
+
dataHome: (0, import_node_path12.join)(root, "engine-data"),
|
|
6320
|
+
stateHome: (0, import_node_path12.join)(root, "engine-state")
|
|
5169
6321
|
};
|
|
5170
6322
|
}
|
|
5171
6323
|
async function resolveTheme() {
|
|
@@ -5182,61 +6334,119 @@ async function resolveTheme() {
|
|
|
5182
6334
|
}
|
|
5183
6335
|
return agentful_theme_default;
|
|
5184
6336
|
}
|
|
5185
|
-
async function writeEngineSession(session, catalog, localProviders = {}) {
|
|
6337
|
+
async function writeEngineSession(session, catalog, localProviders = {}, framework = "unknown", skillsInstruction = null) {
|
|
5186
6338
|
const xdg = engineXdg();
|
|
5187
|
-
const configDir2 = (0,
|
|
5188
|
-
const dataDir = (0,
|
|
5189
|
-
const themesDir = (0,
|
|
5190
|
-
const pluginsDir = (0,
|
|
5191
|
-
const commandsDir = (0,
|
|
5192
|
-
for (const dir of [themesDir, pluginsDir, commandsDir, dataDir, (0,
|
|
5193
|
-
(0,
|
|
6339
|
+
const configDir2 = (0, import_node_path12.join)(xdg.configHome, "opencode");
|
|
6340
|
+
const dataDir = (0, import_node_path12.join)(xdg.dataHome, "opencode");
|
|
6341
|
+
const themesDir = (0, import_node_path12.join)(configDir2, "themes");
|
|
6342
|
+
const pluginsDir = (0, import_node_path12.join)(configDir2, "plugins");
|
|
6343
|
+
const commandsDir = (0, import_node_path12.join)(configDir2, "commands");
|
|
6344
|
+
for (const dir of [themesDir, pluginsDir, commandsDir, dataDir, (0, import_node_path12.join)(xdg.stateHome, "opencode")]) {
|
|
6345
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
5194
6346
|
}
|
|
5195
|
-
const models = {};
|
|
5196
|
-
for (const entry of catalog.models) models[entry.id] = { name: entry.name };
|
|
5197
6347
|
const imagegenAvailable = installEngineTools(configDir2) && session.img_on !== false;
|
|
5198
|
-
const
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
(0, import_node_fs8.writeFileSync)((0, import_node_path10.join)(configDir2, "config.json"), JSON.stringify(config, null, 2));
|
|
5218
|
-
(0, import_node_fs8.writeFileSync)((0, import_node_path10.join)(dataDir, "auth.json"), JSON.stringify({
|
|
6348
|
+
const cloudInstructionsPath = (0, import_node_path12.join)(configDir2, CLOUD_INSTRUCTIONS_FILENAME);
|
|
6349
|
+
(0, import_node_fs10.writeFileSync)(cloudInstructionsPath, renderCloudInstructions(framework));
|
|
6350
|
+
const skillsPath = (0, import_node_path12.join)(configDir2, "AGENTFUL_SKILLS.md");
|
|
6351
|
+
const instructionPaths = [cloudInstructionsPath];
|
|
6352
|
+
if (skillsInstruction) {
|
|
6353
|
+
(0, import_node_fs10.writeFileSync)(skillsPath, skillsInstruction);
|
|
6354
|
+
instructionPaths.push(skillsPath);
|
|
6355
|
+
} else {
|
|
6356
|
+
(0, import_node_fs10.rmSync)(skillsPath, { force: true });
|
|
6357
|
+
}
|
|
6358
|
+
const config = buildEngineConfig({
|
|
6359
|
+
session,
|
|
6360
|
+
catalog,
|
|
6361
|
+
localProviders,
|
|
6362
|
+
imagegenAvailable,
|
|
6363
|
+
instructionPaths
|
|
6364
|
+
});
|
|
6365
|
+
(0, import_node_fs10.writeFileSync)((0, import_node_path12.join)(configDir2, "config.json"), JSON.stringify(config, null, 2));
|
|
6366
|
+
(0, import_node_fs10.writeFileSync)((0, import_node_path12.join)(dataDir, "auth.json"), JSON.stringify({
|
|
5219
6367
|
[PROVIDER_ID]: { apiKey: session.token }
|
|
5220
6368
|
}, null, 2));
|
|
5221
|
-
const
|
|
5222
|
-
(0,
|
|
5223
|
-
(0,
|
|
6369
|
+
const { brandingPluginSource: brandingPluginSource2 } = await Promise.resolve().then(() => (init_plugin(), plugin_exports));
|
|
6370
|
+
const pluginPath = (0, import_node_path12.join)(pluginsDir, "agentful-branding.tsx");
|
|
6371
|
+
(0, import_node_fs10.writeFileSync)(pluginPath, brandingPluginSource2());
|
|
6372
|
+
(0, import_node_fs10.writeFileSync)((0, import_node_path12.join)(configDir2, "tui.json"), JSON.stringify({
|
|
5224
6373
|
theme: "agentful",
|
|
5225
6374
|
plugin: [`file://${pluginPath}`]
|
|
5226
6375
|
}, null, 2));
|
|
5227
|
-
(0,
|
|
6376
|
+
(0, import_node_fs10.writeFileSync)((0, import_node_path12.join)(themesDir, "agentful.json"), JSON.stringify(await resolveTheme(), null, 2));
|
|
5228
6377
|
for (const name of Object.keys(SLASH_COMMANDS)) {
|
|
5229
|
-
(0,
|
|
6378
|
+
(0, import_node_fs10.writeFileSync)((0, import_node_path12.join)(commandsDir, `${name}.md`), commandMarkdown(name));
|
|
5230
6379
|
}
|
|
5231
6380
|
for (const legacy of ["xdg-config", "xdg-data", "xdg-state"]) {
|
|
5232
6381
|
try {
|
|
5233
|
-
(0,
|
|
6382
|
+
(0, import_node_fs10.rmSync)((0, import_node_path12.join)((0, import_node_os5.homedir)(), ".local", "share", "agentful", legacy), { recursive: true, force: true });
|
|
5234
6383
|
} catch {
|
|
5235
6384
|
}
|
|
5236
6385
|
}
|
|
5237
6386
|
return xdg;
|
|
5238
6387
|
}
|
|
5239
6388
|
|
|
6389
|
+
// src/lib/skills.ts
|
|
6390
|
+
var import_node_fs11 = require("fs");
|
|
6391
|
+
var import_node_path13 = require("path");
|
|
6392
|
+
|
|
6393
|
+
// src/generated/skills.json
|
|
6394
|
+
var skills_default = {
|
|
6395
|
+
$comment: "GENERATED by cli/scripts/sync-skills.mjs from opencode-lambda/executor/runtime/.opencode/skills/ \u2014 do not edit by hand. platform-contract/check-parity.py guards drift.",
|
|
6396
|
+
schema_version: 1,
|
|
6397
|
+
skills: {
|
|
6398
|
+
"agentful-template-contract": "---\nname: agentful-template-contract\ndescription: Agentful generated-project and template contract for static preview/publish compatibility, scaffold-safe file structure, host-agnostic assets, backend honesty, and no unsupported dependencies. Use when creating a new project, customizing a scaffold/template, editing generated app structure, or fixing preview/publish issues.\n---\n\n# Agentful Template Contract\n\n## Contract\n\nGenerate and edit projects for Agentful's existing platform:\n\n- Static preview and publish serve built files from S3/CloudFront/Cloudflare.\n- Package projects must build to `dist/`, `out/`, or `build/`.\n- Vanilla projects must work without a package install or build step.\n- Managed database and managed actions are configured in the Backend tab, not invented in code.\n\nDo not add server-first framework runtime dependencies (for example Remix / `@remix-run/*`, or a Next.js server). A referenced example repo may inform structure or styling, but this platform serves static builds and does not run those servers.\n\n## File Shape\n\n- Preserve the current stack and file layout unless the user explicitly asks for a migration.\n- For empty workspaces, follow the loaded scaffold skill exactly.\n- Put route- or feature-local UI near the feature. Create shared folders only after real reuse exists.\n- Avoid dumping grounds such as `helpers`, `misc`, or broad `lib` folders when ownership is clear.\n- Keep generated documentation short and accurate; do not describe features that do not exist.\n\n## Hosting Rules\n\n- Asset paths for project-owned files: SPAs with a client-side (history-mode)\n router use root-absolute paths (`base: '/'`, `/assets/...`); router-less or\n multi-page projects use relative paths.\n- Use `/api/...` only for platform runtime APIs.\n- Do not hardcode `mainmvp.com`, `agentful.dev`, preview domains, or user subdomains into app code.\n- Do not add a `<base>` tag.\n- For canonical, Open Graph, sitemap, and manifest URLs, use relative URLs or omit the origin.\n- For SPAs on static hosting, use history-mode routing with `base: '/'` (never\n hash routing) \u2014 the platform falls unknown deep-links back to `index.html`, so\n routes resolve on hard refresh with clean URLs (no `#`).\n\n## Backend Honesty\n\n- When a backend is configured (`status: active`), use it for real data, auth, and actions.\n- When no backend is configured, still build the requested UI from clearly-labeled sample/demo data (one replaceable module). Do not refuse or stop \u2014 surface the Backend-tab note in your final response instead.\n- Sample data must read as sample. Do NOT fake auth that \"logs in\", saves that claim to persist across reloads, or payment/webhook flows that pretend to fire \u2014 those mislead the user. Presentation data (example metrics, sample listings) is fine; a fake real-backend contract is not.\n- Never store secret keys in frontend code, templates, `.env`, or committed files.\n- Public client keys may be placeholders only when the target integration actually uses public keys.\n\n## Content Honesty\n\nDo not invent:\n\n- customer logos, testimonials, reviews, awards, revenue, user counts, certifications, compliance claims, or legal assurances\n- real prices, policies, medical/financial claims, or guarantees unless the user supplies them\n\nUse neutral placeholder copy or proof-ready sections instead.\n\n## Before Finishing\n\n- Verify imports, references, asset paths, and routes are defined.\n- Run the relevant build when a build script exists.\n- Confirm the expected output folder contains an `index.html`.\n- For UI work, include responsive behavior and basic loading, empty, error, and success states where the feature implies them.\n",
|
|
6399
|
+
"nextjs-scaffold": '---\nname: nextjs-scaffold\ndescription: Next.js 15 static export scaffold with React 19, TypeScript, and Tailwind CSS v4 generated manually without create-next-app. Use only for explicit Next.js requests or justified static export app needs.\n---\n\n# Next.js Scaffold\n\n## Stack Briefing\n\nNext.js 15 **static export** (`output: \'export\'`) + React 19 + TypeScript +\nTailwind v4, written manually (never `create-next-app`). Static export only \u2014\nno server actions, API routes, dynamic server rendering, or image optimization\ndependency. Build produces `out/` (not `dist/`). Use only for explicit Next or\njustified static multi-page React needs.\n\n## When To Use\n\nUse this skill when `selected_stack` is Next.js. Prefer Next.js only when the user explicitly asks for Next.js, asks for Next-specific features, or needs a React app architecture with static export and file-based routing.\n\nDo not use Next.js for a simple landing page or portfolio unless requested.\n\n## When Not To Use\n\n- Simple landing pages/portfolios \u2192 `vanilla-scaffold`.\n- General React app UI without Next-specific needs \u2192 `react-scaffold`.\n- Anything needing server actions, API routes, or SSR \u2014 unsupported here.\n\n## HARD STOP: Never Re-Scaffold An Existing App\n\nBefore writing ANY scaffold file, check the workspace. If `package.json` (or\nan existing `index.html` app shell for vanilla projects) and source files\nalready exist, this project already has an app \u2014 scaffolding is DONE and this\nskill must not overwrite it. Read the existing entry points and source tree\nfirst, then build ON TOP of the existing files: keep the entry points,\nrouting, and dependency choices already in place. Overwriting the scaffold\nfiles on an existing project destroys the user\'s app (this happened in\nproduction). If the existing code seems inconsistent with the request, ask\nthe user \u2014 never replace silently.\n\n## Required File Shape\n\nThis is the shape a Next.js project has here. Depending on how the project\nstarted, these files may already exist in the workspace \u2014 check first:\n\n```text\npackage.json build = `next build`; `npm run verify` runs the full gate\nnext.config.ts output: \'export\', trailingSlash, images.unoptimized\ntsconfig.json paths "@/*" -> ./src/*\neslint.config.mjs the only ESLint config in the project\npostcss.config.mjs\nnext-env.d.ts\npublic/\n favicon.svg keep it \u2014 without a favicon every page logs a 404\nsrc/\n app/\n layout.tsx owns <html>/<body> and the metadata\n page.tsx the root route: this IS the landing page\n globals.css THE global stylesheet \u2014 Tailwind by default\n.gitignore\n```\n\n**Empty workspace** \u2014 create exactly this shape.\n\n**Files already present** \u2014 **edit them, do not recreate them.** A project\nfrom `create-next-app` is close to this already; the usual gaps are the\n`Create Next App` metadata in `layout.tsx`, the demo SVGs in `public/`, and\nan `app/` directory at the root instead of under `src/`.\n\nThe build output is `out/`, not `dist/` \u2014 that is expected and the platform\naccepts it. Do not rename it.\n\nDo not use `next/font/google` by default because it can add build-time network dependency. Use system fonts unless the user explicitly requests custom fonts.\n\n## package.json\n\n```json\n{\n "name": "project-name",\n "private": true,\n "version": "0.1.0",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "verify": "tsc --noEmit && eslint . --max-warnings 0 && next build"\n },\n "dependencies": {\n "next": "16.2.9",\n "react": "19.2.4",\n "react-dom": "19.2.4"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4",\n "@types/node": "^20",\n "@types/react": "^19",\n "@types/react-dom": "^19",\n "eslint": "^9",\n "eslint-config-next": "16.2.9",\n "tailwindcss": "^4",\n "typescript": "^5"\n }\n}\n```\n\n## next.config.ts\n\n```ts\nimport type { NextConfig } from \'next\'\n\nconst nextConfig: NextConfig = {\n // Static export \u2014 the platform serves files, there is no Next.js server.\n output: \'export\',\n // Emits `about/index.html` instead of `about.html`, which a plain static\n // host can resolve without rewrite rules.\n trailingSlash: true,\n // next/image needs a server to optimise; without this the export fails.\n images: {\n unoptimized: true,\n },\n}\n\nexport default nextConfig\n```\n\n## postcss.config.mjs\n\n```js\nconst config = {\n plugins: {\n \'@tailwindcss/postcss\': {},\n },\n}\n\nexport default config\n```\n\n## App Files\n\n`src/app/layout.tsx`:\n\n```tsx\nimport type { Metadata } from \'next\'\nimport \'./globals.css\'\n\nexport const metadata: Metadata = {\n title: \'Replace this title\',\n description: \'Replace this with a one-sentence description of the site.\',\n icons: { icon: \'/favicon.svg\' },\n}\n\nexport default function RootLayout({\n children,\n}: Readonly<{\n children: React.ReactNode\n}>) {\n return (\n <html lang="en" className="h-full antialiased">\n <body className="min-h-full flex flex-col">{children}</body>\n </html>\n )\n}\n```\n\n`src/app/globals.css`:\n\n```css\n@import "tailwindcss";\n\n/* Design tokens. Tailwind v4 turns every entry here into a utility\n (--color-brand -> bg-brand/text-brand), so restyle the project by editing\n these values rather than sprinkling hex codes through the components. */\n@theme {\n --color-brand: #0070f3;\n --color-brand-contrast: #ffffff;\n --color-surface: #ffffff;\n --color-surface-muted: #f5f5f7;\n --color-ink: #111827;\n --color-ink-muted: #6b7280;\n --radius-card: 0.75rem;\n}\n\n:root {\n color-scheme: light;\n font-family: Inter, ui-sans-serif, system-ui, sans-serif;\n --app-bg: #ffffff;\n --app-fg: #111827;\n}\n\n/* An opaque background on html, body is mandatory \u2014 the page is never\n transparent. Keep this rule when you restyle. */\nhtml, body {\n margin: 0;\n min-height: 100%;\n min-width: 320px;\n background: var(--app-bg);\n color: var(--app-fg);\n}\n\n/* Keyboard focus baseline. Components may override it, but never remove it:\n the platform appends a generic fallback to any project whose CSS contains\n no `:focus` rule at all, and a real one belongs here. */\n:focus-visible {\n outline: 2px solid var(--color-brand, currentColor);\n outline-offset: 2px;\n}\n```\n\n## TypeScript Config\n\n`tsconfig.json`:\n\n```json\n{\n "compilerOptions": {\n "target": "ES2017",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "react-jsx",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": { "@/*": ["./src/*"] }\n },\n "include": [\n "next-env.d.ts",\n "**/*.ts",\n "**/*.tsx",\n "**/*.mts",\n ".next/types/**/*.ts",\n ".next/dev/types/**/*.ts"\n ],\n "exclude": ["node_modules"]\n}\n```\n\n`next-env.d.ts`:\n\n```ts\n/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n```\n\n## Routing Rules\n\nUse file-based routing under `src/app/` with static export. Do not add\n`middleware`, dynamic server routes, or `generateStaticParams` that depend on a\nserver. All routes must be statically renderable.\n\n## Data And Backend Rules\n\n- No API routes or server actions (static export forbids them). For data/auth,\n use the platform backend via client `fetch()` \u2014 respect the STOP gate.\n- `images.unoptimized: true` is required; do not add the image optimization\n server dependency.\n\n## Asset Path Rules\n\n- Keep output host-agnostic; reference the project\'s own assets/routes\n relatively. Do not set an absolute `assetPrefix`/`basePath`. No `<base>` tag.\n\n## Common Failure Modes\n\n- Adding server actions/API routes \u2192 static export build fails.\n- Using `next/font/google` \u2192 build-time network dependency.\n- Renaming `out/` to `dist/` or adding postbuild move scripts.\n- Optimized `<Image>` without `unoptimized: true`.\n\n## ESLint Config\n\nEvery scaffold ships `eslint.config.mjs` (flat config) so the platform lint\ngate and the live ESLint diagnostics work from the very first turn. Next uses\n`eslint-config-next` rather than the hand-rolled rule set of `react-scaffold`\n\u2014 measured 2026-08-04, it covers strictly more:\n\n| canary | `eslint-config-next` |\n| --- | --- |\n| `useEffect(() => setV(item), [item])` | error \u2014 *"Calling setState synchronously within an effect"* (the react-scaffold class) |\n| `<img src="/x.png">` | error \u2014 `@next/next/no-img-element` (which the hand-rolled config never sees) |\n\n`eslint.config.mjs`:\n\n```js\nimport { defineConfig, globalIgnores } from "eslint/config";\nimport nextVitals from "eslint-config-next/core-web-vitals";\nimport nextTs from "eslint-config-next/typescript";\n\nconst eslintConfig = defineConfig([\n ...nextVitals,\n ...nextTs,\n // Override default ignores of eslint-config-next.\n globalIgnores([\n // Default ignores of eslint-config-next:\n ".next/**",\n "out/**",\n "build/**",\n "next-env.d.ts",\n ]),\n]);\n\nexport default eslintConfig;\n```\n\nThe platform runs `npx eslint . --max-warnings 0` before every build, so any\nviolation blocks the build. Do not add or remove rules, and never "fix" a\nviolation with `eslint-disable` or config edits \u2014 fix the code.\n\n## Verification\n\nRun install, then the full platform verification contract (see the build\nagent\'s "Build And Preview Rules"): typecheck, `npx eslint . --max-warnings 0`\n(the scaffold ships `eslint.config.mjs`, so this check ALWAYS applies and\ndecides acceptance), `npm run\nbuild`, and finally verify that `out/index.html` exists. Do not rename `out/`\nto `dist/`. `npm run build` alone is not what the platform gate checks \u2014 and\nNext\'s own build only surfaces lint when configured to. Never make the lint\nstep pass by disabling rules.\n',
|
|
6400
|
+
"react-scaffold": '---\nname: react-scaffold\ndescription: React 19 with Vite, TypeScript, and Tailwind CSS v4 scaffold generated manually without npm create. Use for app UIs, dashboards, auth flows, CRUD interfaces, or explicit React requests.\n---\n\n# React Scaffold\n\n## Stack Briefing\n\nReact 19 + Vite + TypeScript, written manually (never `npm create`), with\nTailwind v4 as the default styling layer. Use it for app-like UIs where\ncomponent state and interaction justify a framework. Output must stay\nstatic-hostable: `base: \'/\'`, history routing (clean URLs, no `#`), build to\n`dist/`. Do not over-split into dozens of trivial components \u2014 keep the tree\npragmatic and typed.\n\n## When To Use\n\nUse this skill when `selected_stack` is React. React is appropriate for app-like interfaces, dashboard/admin UIs, authenticated user flows, complex client state, CRUD screens, and explicit React requests.\n\nDo not use React just because the workspace is empty. Static marketing/content sites should usually use `vanilla-scaffold`.\n\n## When Not To Use\n\n- Static marketing/content/portfolio sites \u2192 `vanilla-scaffold`.\n- Content-heavy multi-page sites better served by Astro \u2192 `astro-scaffold`.\n- Anything requiring SSR/server rendering \u2014 output here is static export only.\n\n## HARD STOP: Never Re-Scaffold An Existing App\n\nBefore writing ANY scaffold file, check the workspace. If `package.json` and\n`src/` already exist, this project already has an app \u2014 scaffolding is DONE\nand this skill must not overwrite it. Read the existing `package.json`,\n`src/App.tsx` and the `src/` tree first, then build ON TOP of the existing\nfiles: keep the entry points, routing, and dependency choices already in\nplace. Overwriting `package.json`/`App.tsx`/`main.tsx` on an existing project\ndestroys the user\'s app (this happened in production). If the existing code\nseems inconsistent with the request, ask the user \u2014 never replace silently.\n\n**Rewriting a file must never drop an `import "./x.css"` it carried.** Nothing\ncatches it: tsc/eslint do not read CSS, an unimported stylesheet is legal so\nthe bundler is silent, and the page renders unstyled (prod 2026-08-01: 38 of\n85 class names left the bundle, all gates green). Prefer `edit` over a full\n`write`; if you rewrite, carry the original import block over.\n\n## Required File Shape\n\nThis is the shape a React project has here. Depending on how the project\nstarted, these files may already exist in the workspace \u2014 check first:\n\n```text\nindex.html entry: favicon link, #root, module script\npackage.json build = `vite build`; `npm run verify` runs the full gate\nvite.config.ts base: \'/\', tailwindcss() + react()\ntsconfig.json ONE flat config, include: ["src"]\neslint.config.mjs the only ESLint config in the project\npublic/\n favicon.svg keep it \u2014 without a favicon every page logs a 404\nsrc/\n App.tsx placeholder shell: header/nav, hero, cards, footer\n main.tsx entry, imports ./index.css\n index.css THE stylesheet \u2014 Tailwind by default, see Styling\n vite-env.d.ts\n.gitignore\n```\n\n**Empty workspace** \u2014 create exactly this shape.\n\n**Files already present** \u2014 **edit them, do not recreate them.** Rewrite the\ncopy, restyle the `@theme` tokens, add components and routes, but keep the\nshape. Each part of it prevents a defect that has shipped to real users.\n\n**A project that came from an older `create-vite` starter** carries config\nthat silently disables checks the platform believes it ran. Repair config\nonly \u2014 never a full-file `write` of `App.tsx`, `main.tsx` or `package.json`,\nand never as a pretext to re-scaffold:\n\n| found | do |\n| --- | --- |\n| `tsconfig.json` with `"files": []` + `references` | always replace with the one flat config below and delete `tsconfig.app.json`/`tsconfig.node.json` \u2014 otherwise nothing is type-checked at all |\n| `eslint.config.js` | leave it alone if it is the only config. If you need the rule set below, move its contents into `eslint.config.mjs` and **delete the `.js`** \u2014 never let both exist |\n| `src/App.css` | leave a working import alone. If you rewrite `App.tsx`, carry `import \'./App.css\'` over, or fold the rules into `index.css` in the same edit \u2014 never drop it silently |\n\nSay in your summary which of these you changed and why.\n\n### One Stylesheet, Not Two\n\n`src/index.css` is the only stylesheet. Do **not** add `src/App.css`.\n\nA second stylesheet has to be imported from a component, and that import is\nthe single most fragile line in the project: one full-file `write` of\n`App.tsx` drops it, and nothing notices (prod `0njsblk0lye2vsx`, 2026-08-01 \u2014\nthe layout stylesheet left the bundle, every gate stayed green). With one\nstylesheet imported once from `main.tsx`, the failure has nowhere to happen.\n\nPut component styles wherever the chosen approach puts them \u2014 Tailwind\nclasses by default \u2014 and shared values in `index.css`.\n\n## package.json\n\n```json\n{\n "name": "project-name",\n "private": true,\n "version": "0.1.0",\n "type": "module",\n "scripts": {\n "dev": "vite",\n "build": "vite build",\n "preview": "vite preview",\n "verify": "tsc --noEmit && eslint . --max-warnings 0 && vite build"\n },\n "dependencies": {\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/vite": "^4.0.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "@vitejs/plugin-react": "^4.3.4",\n "eslint": "^9.0.0",\n "eslint-plugin-react-hooks": "^6.0.0",\n "tailwindcss": "^4.0.0",\n "typescript": "^5.0.0",\n "typescript-eslint": "^8.0.0",\n "vite": "^6.0.0"\n }\n}\n```\n\n`build` is `vite build` alone \u2014 no `tsc` in front of it. The platform runs the\ntype check itself before the build, and a `tsc` inside the build script also\nblocks the one-time build that runs when a starter is imported. `verify` is\nthe gate in one command; run it, not just `npm run build`.\n\n## vite.config.ts\n\n```ts\nimport { defineConfig } from \'vite\'\nimport react from \'@vitejs/plugin-react\'\nimport tailwindcss from \'@tailwindcss/vite\'\n\nexport default defineConfig({\n base: \'/\',\n plugins: [tailwindcss(), react()],\n})\n```\n\n## Entry Files\n\n`index.html`:\n\n```html\n<!doctype html>\n<html lang="en">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <meta name="description" content="Replace this with a one-sentence description of the app." />\n <link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n <title>Replace this title</title>\n </head>\n <body>\n <div id="root"></div>\n <script type="module" src="./src/main.tsx"></script>\n </body>\n</html>\n```\n\nKeep the favicon link and keep `public/favicon.svg`. Without them the browser\nrequests `/favicon.ico` on every page load and the render check records a\nfailed request on an otherwise healthy project.\n\n`src/main.tsx`:\n\n```tsx\nimport React from \'react\'\nimport ReactDOM from \'react-dom/client\'\nimport App from \'./App\'\nimport \'./index.css\'\n\nReactDOM.createRoot(document.getElementById(\'root\')!).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>,\n)\n```\n\n`src/index.css` as the starter ships it \u2014 Tailwind first, then tokens, then\nthe opaque page baseline:\n\n```css\n@import "tailwindcss";\n\n/* Design tokens. Tailwind v4 turns every entry here into a utility\n (--color-brand -> bg-brand/text-brand), so restyle the project by editing\n these values rather than sprinkling hex codes through the components. */\n@theme {\n --color-brand: #4f46e5;\n --color-brand-contrast: #ffffff;\n --color-surface: #ffffff;\n --color-surface-muted: #f5f5f7;\n --color-ink: #111827;\n --color-ink-muted: #6b7280;\n --radius-card: 0.75rem;\n}\n\n:root {\n color-scheme: light;\n font-family: Inter, ui-sans-serif, system-ui, sans-serif;\n --app-bg: #ffffff;\n --app-fg: #111827;\n}\n\n/* An opaque background on html, body is mandatory \u2014 the page is never\n transparent. Keep this rule when you restyle. */\nhtml, body {\n margin: 0;\n min-height: 100%;\n min-width: 320px;\n background: var(--app-bg);\n color: var(--app-fg);\n}\n\n/* Keyboard focus baseline. Components may override it, but never remove it:\n the platform appends a generic fallback to any project whose CSS contains\n no `:focus` rule at all, and a real one belongs here. */\n:focus-visible {\n outline: 2px solid var(--color-brand, currentColor);\n outline-offset: 2px;\n}\n```\n\nEvery `@theme` entry becomes a utility (`--color-brand` \u2192 `bg-brand`), so\nrestyle by editing tokens instead of scattering hex codes through components.\nIf the user chose a non-Tailwind approach, keep `:root` and the `html, body`\nrule and replace the rest \u2014 the opaque background is not optional.\n\n`src/vite-env.d.ts`:\n\n```ts\n/// <reference types="vite/client" />\n```\n\n## TypeScript Config\n\nOne flat `tsconfig.json` with `include`, no project references and no\n`tsconfig.app.json`/`tsconfig.node.json`.\n\nThis is not a style preference. The platform gate runs `tsc --noEmit` against\nthe root `tsconfig.json`. A `create-vite`-style root \u2014 `"files": []` plus\n`references` \u2014 makes that command compile **zero files**: it exits 0 on a\nproject full of type errors, and the gate reports a pass it never performed.\nMeasured 2026-08-04: a deliberate type error passes the referenced shape and\nfails the flat one. `vite.config.ts` is deliberately not type-checked.\n\n`tsconfig.json`:\n\n```json\n{\n "compilerOptions": {\n "target": "ES2020",\n "useDefineForClassFields": true,\n "lib": ["ES2020", "DOM", "DOM.Iterable"],\n "module": "ESNext",\n "skipLibCheck": true,\n "moduleResolution": "bundler",\n "allowImportingTsExtensions": true,\n "resolveJsonModule": true,\n "isolatedModules": true,\n "noEmit": true,\n "jsx": "react-jsx",\n "strict": true\n },\n "include": ["src"]\n}\n```\n\n## ESLint Config\n\nEvery scaffold ships **exactly one** ESLint config, `eslint.config.mjs` (flat\nconfig), so the platform lint gate and the live ESLint diagnostics work from\nthe first turn. Keep it exactly this minimal \u2014 correctness rules only, no\nstylistic rules, nothing that fights Prettier:\n\n`eslint.config.mjs`:\n\n```js\nimport tseslint from \'typescript-eslint\'\nimport reactHooks from \'eslint-plugin-react-hooks\'\n\nexport default tseslint.config(\n { ignores: [\'dist\'] },\n {\n files: [\'**/*.{ts,tsx}\'],\n extends: [tseslint.configs.base],\n plugins: { \'react-hooks\': reactHooks },\n rules: {\n \'react-hooks/rules-of-hooks\': \'error\',\n \'react-hooks/exhaustive-deps\': \'error\',\n \'react-hooks/set-state-in-effect\': \'error\',\n \'react-hooks/no-deriving-state-in-effects\': \'error\',\n \'@typescript-eslint/no-unused-vars\': [\n \'error\',\n { argsIgnorePattern: \'^_\', varsIgnorePattern: \'^_\' },\n ],\n },\n },\n)\n```\n\n**Never add a second config file.** ESLint resolves `eslint.config.js` before\n`eslint.config.mjs` and uses only the first one it finds, silently. A stray\n`.js` next to the `.mjs` therefore disables every rule above without a\nwarning \u2014 measured on ESLint 9: a violation that fails with the `.mjs` alone\npasses when both files exist. So in an existing project, either keep its\n`eslint.config.js` as the single config, or migrate it into\n`eslint.config.mjs` and delete the `.js` \u2014 never leave both behind.\n\nThe platform runs `npx eslint . --max-warnings 0` before every build, so any\nviolation blocks the build. Do not add rules, do not remove rules, and never\n"fix" a violation with `eslint-disable` or config edits \u2014 fix the code (see\nthe hooks patterns below).\n\n## Allowed Complexity\n\n- Add `src/components/`, `src/pages/`, `src/lib/`, `src/hooks/` only when a\n feature needs them \u2014 do not scaffold empty folders.\n- Introduce state libraries or data-fetching only when real shared state or\n server data exists; local `useState`/`useReducer` covers most cases.\n- Keep components typed (props/return types); avoid `any`.\n\n## CSS And Styling Expectations\n\nThe starter ships Tailwind v4 via `@tailwindcss/vite`, and that is the\ndefault: keep it and tokenize theme values in `@theme`.\n\n**The user\'s request wins.** If they ask for a different styling approach,\nfollow them and adjust the setup in the same turn:\n\n- **shadcn/ui** \u2014 is built ON Tailwind. Keep Tailwind, add the components.\n Never remove Tailwind to "make room" for it.\n- **A CSS-in-JS library** (Chakra, MUI, styled-components, Emotion) \u2014 add it\n and build with it. Leaving Tailwind installed is harmless (v4 emits only\n the classes you use), so remove it only if the user asks.\n- **Plain CSS / CSS Modules** \u2014 drop `@tailwindcss/vite` and `tailwindcss`\n from `package.json`, remove the plugin from `vite.config.ts`, and replace\n `@import "tailwindcss"` in `index.css` with your own base styles. Keep the\n `:root` tokens and the opaque `html, body` rule.\n\nWhatever the approach, these hold:\n\n- **One stylesheet** \u2014 the rule below is about losing an import, not about\n Tailwind. It applies to plain CSS just as much.\n- Mobile-first, responsive layouts; visible focus states; WCAG AA contrast.\n- The loaded `style-*` skill governs the visual language \u2014 apply its recipe.\n It is written to be independent of the styling technology.\n\n## Routing Rules\n\nIf adding React Router, use `BrowserRouter` (history mode) with `base: \'/\'` in\n`vite.config.ts`. The platform serves at the domain root and falls unknown\ndeep-links back to `index.html`, so routes resolve on hard refresh with clean\nURLs (no `#`). Never use `HashRouter`. Add `react-router-dom` only when routing\nis actually needed.\n\n## Data And Backend Rules\n\n- No database/server calls unless a backend is configured (respect the build\n agent\'s STOP gate). Do not invent mock backends or fake data arrays.\n- Keep secrets out of client code; only public keys via `.env.local`.\n\n## Asset Path Rules\n\n- Set `base: \'/\'` in `vite.config.ts`. The platform serves the project at the\n domain root, and history-mode routes need root-absolute assets.\n- Reference the project\'s own assets/routes with root-absolute paths (`/assets/...`);\n no `<base>` tag, no hardcoded platform URLs.\n\n## Verification\n\nRun install, then the full platform verification contract (see the build\nagent\'s "Build And Preview Rules"), not just `npm run build`:\n\n1. `npx tsc --noEmit`\n2. `npx eslint . --max-warnings 0` \u2014 a project scaffolded from this skill\n ships `eslint.config.mjs`, so this check ALWAYS applies and decides\n acceptance.\n3. `npm run build`\n4. `dist/index.html` exists\n\n`npm run verify` runs 1\u20133 in order. Never make step 2 pass by disabling rules\nor adding `eslint-disable`.\n\n## React Hooks Rules That Fail The Gate\n\nThe scaffold\'s `eslint.config.mjs` enables `eslint-plugin-react-hooks` rules\nthat reject patterns which compile and build fine. These are the ones that\nactually show up \u2014 avoid them while writing, not after:\n\n- **Never sync props into state inside an effect**\n (`useEffect(() => setForm(props.item), [props.item])` \u2192 `set-state-in-effect`).\n Derive the value during render, lift the state up, or remount the subtree\n with a `key` when the edited record changes \u2014 that is usually the intended\n "reset the form" semantic anyway.\n- **Do not open dialogs/editors from an effect that watches the URL.** Derive\n the open state from the route during render, or set it in the event handler\n that triggered the navigation.\n- **Do not bootstrap an external store from a render-time side effect.** Read\n it with `useSyncExternalStore`, or initialize it in the store module itself.\n- **Async loads:** only update state after an await when the effect has not\n been cleaned up (cancellation flag or `AbortController`).\n- **No impure render calls** \u2014 no `Math.random()`, `Date.now()`, or mutation\n during render. Move them into a lazy initializer or an event handler.\n- **Fast Refresh:** a module that exports a component must not also export\n unrelated non-component values (`react-refresh/only-export-components`).\n\nA `setTimeout`/microtask wrapper that only hides the violation from the linter\nis not a fix \u2014 it hides the same bug behind a race.\n\n## Common Failure Modes\n\n- Recreating the starter\'s files instead of editing them.\n- Adding `src/App.css` (or any second stylesheet) \u2014 see "One Stylesheet".\n- Adding `eslint.config.js` beside `eslint.config.mjs`, which silently\n disables the rule set.\n- Restoring the `create-vite` tsconfig trio, which makes the type check inert.\n- Putting `tsc` back into the `build` script.\n- Deleting `public/favicon.svg` or its `<link rel="icon">`.\n- Using `HashRouter` \u2192 ugly `#` URLs; use `BrowserRouter` (history mode).\n- Keeping relative `base: \'./\'` with history routing \u2192 assets 404 on a deep-link\n hard refresh; use `base: \'/\'`.\n- Leaving the starter\'s placeholder copy ("Replace this headline", "Brand",\n "First point") in the shipped page.\n- Over-splitting into trivial components; untyped `any` props.\n- Treating pre-install JSX type errors as source bugs (install deps first).\n',
|
|
6401
|
+
"vue-scaffold": '---\nname: vue-scaffold\ndescription: Vue 3.5 with Vite, TypeScript, and Tailwind CSS v4 scaffold generated manually without npm create. Use for explicit Vue requests or existing Vue projects.\n---\n\n# Vue Scaffold\n\n## Stack Briefing\n\nVue 3.5 + Vite + TypeScript + Tailwind v4, written manually (never `npm create`).\nUse it for app-like Vue UIs. Output must stay static-hostable: `base: \'/\'`,\nhistory mode (clean URLs, no `#`), build to `dist/`. Use the Composition API; add Pinia or Vue Router\nonly when real shared state or routing actually exists.\n\n## When To Use\n\nUse this skill when `selected_stack` is Vue. Prefer Vue only when requested, when the existing project uses Vue, or when the user clearly wants a Vue-style app.\n\n## When Not To Use\n\n- Static marketing/content/portfolio sites \u2192 `vanilla-scaffold`.\n- Content-heavy multi-page sites \u2192 `astro-scaffold`.\n- Anything needing SSR \u2014 output here is static only.\n\n## HARD STOP: Never Re-Scaffold An Existing App\n\nBefore writing ANY scaffold file, check the workspace. If `package.json` (or\nan existing `index.html` app shell for vanilla projects) and source files\nalready exist, this project already has an app \u2014 scaffolding is DONE and this\nskill must not overwrite it. Read the existing entry points and source tree\nfirst, then build ON TOP of the existing files: keep the entry points,\nrouting, and dependency choices already in place. Overwriting the scaffold\nfiles on an existing project destroys the user\'s app (this happened in\nproduction). If the existing code seems inconsistent with the request, ask\nthe user \u2014 never replace silently.\n\n## Required File Shape\n\nThis is the shape a Vue project has here. Depending on how the project\nstarted, these files may already exist in the workspace \u2014 check first:\n\n```text\nindex.html entry: favicon link, #app, module script\npackage.json build = `vite build`; `npm run verify` runs the full gate\nvite.config.ts base: \'/\', tailwindcss() + vue()\ntsconfig.json ONE flat config, no references\neslint.config.mjs the only ESLint config in the project\npublic/\n favicon.svg keep it \u2014 without a favicon every page logs a 404\nsrc/\n App.vue placeholder shell: header/nav, hero, cards, footer\n main.ts entry, imports ./style.css\n style.css THE stylesheet \u2014 Tailwind by default\n env.d.ts vite/client types (inside src/, so the include glob covers it)\n shims-vue.d.ts lets plain `tsc` resolve *.vue \u2014 see below, load-bearing\n.gitignore\n```\n\n**Empty workspace** \u2014 create exactly this shape.\n\n**Files already present** \u2014 **edit them, do not recreate them.** Rewrite the\ncopy, restyle the `@theme` tokens, add components and routes, but keep the\nshape.\n\n**A project from an older `create-vue` starter** ships config that silently\ndisables checks the platform believes it ran. Repair config only \u2014 never a\nfull-file `write` of `App.vue`, `main.ts` or `package.json`:\n\n| found | do |\n| --- | --- |\n| `tsconfig.json` with `"files": []` + `references` | replace with the flat config below, delete `tsconfig.app.json`/`tsconfig.node.json`, and add `src/shims-vue.d.ts` \u2014 the trio makes the type check inert, but removing it without the shim makes it fail |\n| no ESLint config at all | add `eslint.config.mjs` below \u2014 without it the lint gate is inert and never sees a template bug |\n| `src/router/index.ts` with `routes: []`, an unused store | delete them, or give them real routes/state |\n\nCreate `src/components/`, `src/views/`, `src/router/`, or `src/stores/` only when needed.\n\n## package.json\n\n```json\n{\n "name": "project-name",\n "private": true,\n "version": "0.1.0",\n "type": "module",\n "scripts": {\n "dev": "vite",\n "build": "vite build",\n "preview": "vite preview",\n "verify": "vue-tsc --noEmit && eslint . --max-warnings 0 && vite build"\n },\n "dependencies": {\n "vue": "^3.5.0"\n },\n "devDependencies": {\n "@tailwindcss/vite": "^4.0.0",\n "@vitejs/plugin-vue": "^6.0.0",\n "eslint": "^9.0.0",\n "eslint-plugin-vue": "^10.0.0",\n "tailwindcss": "^4.0.0",\n "typescript": "^5.0.0",\n "typescript-eslint": "^8.0.0",\n "vite": "^6.0.0",\n "vue-tsc": "^2.2.0"\n }\n}\n```\n\n## vite.config.ts\n\n```ts\nimport { defineConfig } from \'vite\'\nimport vue from \'@vitejs/plugin-vue\'\nimport tailwindcss from \'@tailwindcss/vite\'\n\nexport default defineConfig({\n base: \'/\',\n plugins: [tailwindcss(), vue()],\n})\n```\n\n## Entry Files\n\n`index.html`:\n\n```html\n<!doctype html>\n<html lang="en">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <meta name="description" content="Replace this with a one-sentence description of the app." />\n <link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n <title>Replace this title</title>\n </head>\n <body>\n <div id="app"></div>\n <script type="module" src="./src/main.ts"></script>\n </body>\n</html>\n```\n\n`src/main.ts`:\n\n```ts\nimport { createApp } from \'vue\'\nimport App from \'./App.vue\'\nimport \'./style.css\'\n\ncreateApp(App).mount(\'#app\')\n```\n\n`src/style.css`:\n\n```css\n@import "tailwindcss";\n\n/* Design tokens. Tailwind v4 turns every entry here into a utility\n (--color-brand -> bg-brand/text-brand), so restyle the project by editing\n these values rather than sprinkling hex codes through the components. */\n@theme {\n --color-brand: #41b883;\n --color-brand-contrast: #ffffff;\n --color-surface: #ffffff;\n --color-surface-muted: #f5f5f7;\n --color-ink: #111827;\n --color-ink-muted: #6b7280;\n --radius-card: 0.75rem;\n}\n\n:root {\n color-scheme: light;\n font-family: Inter, ui-sans-serif, system-ui, sans-serif;\n --app-bg: #ffffff;\n --app-fg: #111827;\n}\n\n/* An opaque background on html, body is mandatory \u2014 the page is never\n transparent. Keep this rule when you restyle. */\nhtml, body {\n margin: 0;\n min-height: 100%;\n min-width: 320px;\n background: var(--app-bg);\n color: var(--app-fg);\n}\n\n/* Keyboard focus baseline. Components may override it, but never remove it:\n the platform appends a generic fallback to any project whose CSS contains\n no `:focus` rule at all, and a real one belongs here. */\n:focus-visible {\n outline: 2px solid var(--color-brand, currentColor);\n outline-offset: 2px;\n}\n```\n\nTailwind is the **default**, not a mandate \u2014 the same rule as elsewhere: if\nthe user asks for a different styling approach, follow them and adjust the\nsetup in the same turn. Vue SFC `<style scoped>` blocks are idiomatic and fine\nalongside it; the thing to avoid is a second global stylesheet imported from a\ncomponent, because one full-file rewrite drops that import silently.\n\n## TypeScript Config\n\nOne flat `tsconfig.json` with `include`, no project references and no\n`tsconfig.app.json`/`tsconfig.node.json`.\n\nThis is not a style preference. The platform gate runs `tsc --noEmit` against\nthe root `tsconfig.json`. A `create-vue`-style root \u2014 `"files": []` plus\n`references` \u2014 makes that command compile **zero files**: it exits 0 on a\nproject full of type errors and the gate reports a pass it never performed.\n\n`vite.config.ts` is deliberately not type-checked.\n\n### `src/shims-vue.d.ts` Is Load-Bearing\n\nThe gate runs `tsc`, **never `vue-tsc`** \u2014 and plain `tsc` cannot resolve an\nimport of a `.vue` file. Without the shim the very first line of `main.ts`\nfails the gate on a phantom error (measured 2026-08-04):\n\n```text\nsrc/main.ts(2,17): error TS2307: Cannot find module \'./App.vue\'\n```\n\nSo the flat tsconfig and the shim are one change: applying either alone is\nworse than the broken state it replaces. Ship both.\n\n`src/shims-vue.d.ts`:\n\n```ts\n/* Lets plain `tsc` resolve `*.vue` imports.\n *\n * Load-bearing: the platform\'s prebuild gate runs `tsc --noEmit`, never\n * `vue-tsc`. Without this shim it stops at\n * src/main.ts: error TS2307: Cannot find module \'./App.vue\'\n * and every build fails on a phantom error.\n *\n * The shim types an SFC as a generic component, so `tsc` checks all `.ts`\n * files properly but not the internals of a `.vue` file. `npm run verify`\n * runs `vue-tsc --noEmit`, which does check those \u2014 use it before finishing.\n */\ndeclare module \'*.vue\' {\n import type { DefineComponent } from \'vue\'\n const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>\n export default component\n}\n```\n\nWhat this buys and what it does not \u2014 all three measured:\n\n| | `tsc` (the gate) | `vue-tsc` (`npm run verify`) |\n| --- | --- | --- |\n| type error in a `.ts` file | caught | caught |\n| type error inside a `.vue` SFC | **missed** \u2014 the shim types it generically | caught |\n\nThat is why `verify` runs `vue-tsc` and why passing the gate is not the same\nas being done.\n\n`tsconfig.json`:\n\n```json\n{\n "compilerOptions": {\n "target": "ES2020",\n "useDefineForClassFields": true,\n "module": "ESNext",\n "lib": ["ES2020", "DOM", "DOM.Iterable"],\n "skipLibCheck": true,\n "moduleResolution": "bundler",\n "allowImportingTsExtensions": true,\n "resolveJsonModule": true,\n "isolatedModules": true,\n "noEmit": true,\n "jsx": "preserve",\n "strict": true\n },\n "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]\n}\n```\n\n## Routing Rules\n\nIf adding Vue Router, use `createWebHistory()` (history mode) with `base: \'/\'` in\n`vite.config.ts`. The platform serves at the domain root and falls unknown\ndeep-links back to `index.html`, so routes resolve on hard refresh with clean\nURLs (no `#`). Never use `createWebHashHistory()`. Add `vue-router` only when\nrouting is needed.\n\n## Data And Backend Rules\n\n- No database/server calls unless a backend is configured (respect the build\n agent\'s STOP gate). Do not invent mock backends or fake data arrays.\n- Keep secrets out of client code; only public keys via `.env.local`.\n\n## Asset Path Rules\n\n- Set `base: \'/\'`. The platform serves the project at the domain root, and\n history-mode routes need root-absolute assets. Reference the project\'s own\n assets/routes with root-absolute paths. No `<base>` tag, no hardcoded platform URLs.\n\n## Common Failure Modes\n\n- Using `createWebHashHistory()` \u2192 ugly `#` URLs; use `createWebHistory()`.\n- Keeping relative `base: \'./\'` with history routing \u2192 assets 404 on a deep-link\n hard refresh; use `base: \'/\'`.\n- Leaving starter boilerplate in `App.vue`.\n- Adding Pinia/Router with no real need.\n\n## ESLint Config\n\nEvery scaffold ships `eslint.config.mjs` (flat config) so the platform lint\ngate and the live ESLint diagnostics work from the very first turn. Verified\nempirically 2026-08-02: `flat/essential` catches real template bugs\n(`vue/require-v-for-key` etc.); `vue/multi-word-component-names` is switched\nOFF because with `--max-warnings 0` it would block every `Hero.vue`-style\nsingle-word component \u2014 a naming convention, not a correctness rule:\n\n`eslint.config.mjs`:\n\n```js\nimport tseslint from \'typescript-eslint\'\nimport vue from \'eslint-plugin-vue\'\n\nexport default tseslint.config(\n { ignores: [\'dist\'] },\n {\n files: [\'**/*.ts\'],\n extends: [tseslint.configs.base],\n rules: {\n \'@typescript-eslint/no-unused-vars\': [\n \'error\',\n { argsIgnorePattern: \'^_\', varsIgnorePattern: \'^_\' },\n ],\n },\n },\n ...vue.configs[\'flat/essential\'],\n {\n files: [\'**/*.vue\'],\n languageOptions: { parserOptions: { parser: tseslint.parser } },\n rules: {\n \'vue/multi-word-component-names\': \'off\',\n },\n },\n)\n```\n\nThe platform runs `npx eslint . --max-warnings 0` before every build, so any\nviolation blocks the build. Do not add or remove rules, and never "fix" a\nviolation with `eslint-disable` or config edits \u2014 fix the code.\n\n## Verification\n\nRun install, then the full platform verification contract (see the build\nagent\'s "Build And Preview Rules"): typecheck, `npx eslint . --max-warnings 0`\n(the scaffold ships `eslint.config.mjs`, so this check ALWAYS applies and\ndecides acceptance), `npm run build`, and finally verify that\n`dist/index.html` exists. `npm run build` alone is not what the platform gate\nchecks. Never make the lint step pass by disabling rules.\n',
|
|
6402
|
+
"vanilla-scaffold": '---\nname: vanilla-scaffold\ndescription: Vanilla HTML, CSS, and JavaScript scaffold for static landing pages, portfolios, brochure sites, personal brands, simple editorial/media sites, and static catalogs without a build step.\n---\n\n# Vanilla Scaffold\n\n## Stack Briefing\n\nVanilla means static HTML/CSS/JS served as-is \u2014 no framework, no bundler, no\nbuild step. It can still look premium: modern CSS (custom properties, grid,\n`clamp()`, container queries) and small progressive JS carry polished, fully\nresponsive sites. Reach for a framework only when real app state, routing,\nauth, or data demands it.\n\n## When To Use\n\nUse this skill when `selected_stack` is Vanilla or when the request is a static\nwebsite that does not need framework state, routing, auth, database, cart,\ncheckout, CMS, or admin UI.\n\nCommon matches:\n\n- landing page\n- portfolio\n- personal brand\n- service or brochure website\n- waitlist page\n- event page\n- simple static news/media/editorial site without CMS/admin/feed\n- static product catalog without checkout/cart/accounts\n\n## When Not To Use\n\n- Dashboards, admin panels, or CRUD screens \u2192 `react-scaffold`.\n- Authenticated user flows or complex client state \u2192 a framework scaffold.\n- Cart/checkout/accounts/inventory \u2192 framework + backend guidance.\n- Many generated pages from a data source/CMS \u2192 Astro or a framework.\n\n## HARD STOP: Never Re-Scaffold An Existing App\n\nBefore writing ANY scaffold file, check the workspace. If `package.json` (or\nan existing `index.html` app shell for vanilla projects) and source files\nalready exist, this project already has an app \u2014 scaffolding is DONE and this\nskill must not overwrite it. Read the existing entry points and source tree\nfirst, then build ON TOP of the existing files: keep the entry points,\nrouting, and dependency choices already in place. Overwriting the scaffold\nfiles on an existing project destroys the user\'s app (this happened in\nproduction). If the existing code seems inconsistent with the request, ask\nthe user \u2014 never replace silently.\n\n## Required File Shape\n\nA vanilla project is created from a starter, so these files usually ALREADY\nEXIST in the workspace:\n\n```text\nindex.html entry page: skip link, header/main/footer, module script\ncss/\n tokens.css design tokens (colour, spacing, type, radius) + dark mode\n style.css reset, base type, layout primitives, components\njs/\n main.js entry MODULE: sets the `js` flag, calls the init functions\n ui.js named exports: initNav, initReveal, setCurrentYear\nassets/\n favicon.svg\n.gitignore\n```\n\n**Edit these files. Do not recreate them.** Rewrite the copy, restyle the\ntokens, add sections and pages \u2014 but keep the shape: the token/style split,\nthe `type="module"` entry script, the `js`-flag reveal guard. They are there\nbecause each one prevents a defect that has shipped to real users.\n\nIf the workspace is empty (no `index.html`), create the same shape yourself.\n\nDo not create:\n\n- `package.json`\n- `src/`\n- `vite.config.*`\n- `node_modules/`\n- a build script\n\n## Allowed Complexity\n\n- Multiple `.html` pages when the user asks for more than one page.\n- Small JS modules for navigation, filtering, validation, and enhancement.\n- No framework, no bundler, no TypeScript build. If the project starts needing\n real app state, routing, or backend data, reconsider the stack rather than\n bolting complexity onto Vanilla.\n\n## HTML Rules\n\n- Use semantic landmarks: `header`, `main`, `section`, `article`, `footer`.\n- Use relative paths: `./css/style.css`, `./js/main.js`, `./assets/...`.\n- Include real content from the user prompt. Do not leave generic framework starter text.\n- Include SEO basics: title, description, viewport, social metadata when useful.\n- Use forms only as static/client-side UI unless a backend is configured.\n\nMinimal entry:\n\n```html\n<!doctype html>\n<html lang="en">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <meta name="description" content="Project description" />\n <link rel="icon" href="./assets/favicon.svg" type="image/svg+xml" />\n <link rel="stylesheet" href="./css/tokens.css" />\n <link rel="stylesheet" href="./css/style.css" />\n <title>Project Name</title>\n </head>\n <body>\n <header>...</header>\n <main>...</main>\n <footer>...</footer>\n <script type="module" src="./js/main.js"></script>\n </body>\n</html>\n```\n\n### `type="module"` Is Not Optional\n\nAny script that uses `import` or `export` MUST be loaded with\n`type="module"`. A classic `<script src>` throws\n\n```text\nSyntaxError: Cannot use import statement outside a module\n```\n\nbefore its first statement runs \u2014 the whole page stops working. This shipped\nto a real user: the product grid stayed empty and every other check was green.\n`type="module"` implies `defer`, so you do not need both.\n\nSplit JavaScript across files by exporting and importing NAMES, and keep the\ntwo sides in agreement:\n\n```js\n// js/ui.js\nexport function initNav() { /* \u2026 */ }\n\n// js/main.js\nimport { initNav } from \'./ui.js\';\ninitNav();\n```\n\nIf the name on one side does not exist on the other, the browser throws\n`does not provide an export named \'\u2026\'` and, again, nothing runs.\n\n### Never Hand-Roll Cache-Busting\n\nReference local assets by their plain path \u2014 exactly as above. Do **not** append\na version or timestamp query (`?v=`, `?t=`, `?v=1.0`), and never build one from\nan expression.\n\nThe platform already serves un-hashed assets with `Cache-Control: no-cache`, so\na rebuilt file is picked up on the next load. A hand-rolled buster is redundant\nat best; the attempts that get written are HTML-invalid:\n\n```html\n<!-- WRONG: JS syntax cannot execute inside an HTML attribute. The browser\n reads src="js/main.js?v=" and turns the rest into junk attributes. -->\n<script src="js/main.js?v=" + new Date().getTime()></script>\n\n<!-- RIGHT -->\n<script src="./js/main.js" defer></script>\n```\n\nHTML attributes are inert text: there is no scope in which `+ new Date()...`\nruns. If you ever want a value computed at runtime, set it from JavaScript in\n`js/main.js`, never inline in the tag.\n\n## CSS And Styling Expectations\n\n- Start `css/style.css` with the design-system baseline (see ui-generation-core):\n ```css\n :root { --app-bg: #ffffff; --app-fg: #111827; }\n html, body { min-height: 100%; margin: 0; background: var(--app-bg); color: var(--app-fg); }\n ```\n An opaque `--app-bg` on `html, body` is mandatory \u2014 the page is never transparent.\n- Use modern CSS: custom properties, grid/flex, `clamp()`, container-friendly sizing.\n- Tokenize color, spacing, radius, and type scale with custom properties \u2014\n `css/tokens.css` in the starter is that file; put derived values there too.\n\n### Arithmetic Needs `calc()`\n\nCSS has no implicit maths. A token multiplied inline is not "mostly right",\nit is INVALID \u2014 the browser discards the entire declaration and the element\nsilently loses that property:\n\n```css\n/* WRONG: the whole rule\'s padding is dropped, the button renders flush */\n.button { padding: var(--space-3) var(--space-3) * 2; }\n\n/* RIGHT */\n.button { padding: var(--space-3) calc(var(--space-3) * 2); }\n```\n\nThis shipped to a real user across ten declarations: navigation, call-to-action\nand hero spacing all vanished while the stylesheet loaded fine and every gate\nstayed green. Nothing catches it \u2014 a linter cannot check inside `var()`.\nWrap every `*`, `/`, `+`, `-` between values in `calc()`.\n\n### Never Hide Content Without A JS Guard\n\nIf something starts at `opacity: 0` and only JavaScript reveals it, a script\nfailure leaves a blank page. Gate the hidden state on the `js` class that\n`js/main.js` sets:\n\n```css\n.js .reveal { opacity: 0; transition: opacity .5s ease; }\n.js .reveal.is-visible { opacity: 1; }\n```\n- Keep the visual language specific to the prompt, not generic SaaS boilerplate.\n- Mobile-first responsive rules; no horizontal scroll at any breakpoint.\n- Provide visible focus states and sufficient color contrast (WCAG AA).\n- Avoid unused CSS frameworks or resets unless needed.\n\n## JavaScript Rules\n\n- Keep JavaScript small and progressive.\n- Use it for navigation toggles, small filters, form validation, animations, and enhancement.\n- If the project starts needing complex state, routing, or backend data, reconsider the stack before expanding Vanilla too far.\n\n## Routing Rules\n\n- Prefer real multi-page navigation with separate `.html` files and normal links.\n- Use in-page anchor links for single-page sites.\n- Do not build a client-side SPA router in Vanilla; if that is needed, the stack\n is wrong.\n\n## Data And Backend Rules\n\n- No database or server calls unless a backend is configured (see the STOP gate\n in the build agent). Do not fabricate mock backends or fake data arrays.\n- Forms are static/client-side only unless a managed backend exists.\n\n## Asset Path Rules\n\n- Reference the project\'s own assets with paths relative to the file containing\n the reference so output works under any host. From root `index.html`, use\n `./css/...`, `./js/...`, and `./assets/...`. Inside `css/style.css`, an asset\n under the project-root `assets/` directory is `../assets/...` because CSS\n `url(...)` resolves relative to the stylesheet, not to the HTML document.\n- No `<base>` tag; no absolute platform URLs for the project\'s own content.\n\n## Verification\n\nNo dependency install or `npm run build` is required. Verify every local HTML\nand CSS reference by resolving it from the file that contains the reference.\n\n## Common Failure Modes\n\n- Leaving the starter\'s placeholder copy ("Replace this headline", "Card\n heading", "Brand") in the shipped page.\n- Recreating the starter\'s files instead of editing them.\n- `import`/`export` in a script loaded without `type="module"`.\n- Arithmetic between values without `calc()`.\n- Creating `package.json`/`src/`/`vite.config.*` out of habit.\n- Low-contrast text or missing focus states.\n- Horizontal scroll on mobile from fixed widths.\n- Absolute/hardcoded platform URLs for the project\'s own assets.\n'
|
|
6403
|
+
}
|
|
6404
|
+
};
|
|
6405
|
+
|
|
6406
|
+
// src/lib/skills.ts
|
|
6407
|
+
var SKILLS = skills_default.skills;
|
|
6408
|
+
var CONTRACT_SKILL = "agentful-template-contract";
|
|
6409
|
+
var SCAFFOLD_BY_FRAMEWORK = {
|
|
6410
|
+
nextjs: "nextjs-scaffold",
|
|
6411
|
+
vanilla: "vanilla-scaffold"
|
|
6412
|
+
// vite resolves per dependency below (react vs vue).
|
|
6413
|
+
// cra/nuxt get no scaffold skill: the react/vue skills are Vite-specific
|
|
6414
|
+
// and would teach the wrong shape — the template contract still applies.
|
|
6415
|
+
};
|
|
6416
|
+
function hasVueDependency(rootDir) {
|
|
6417
|
+
try {
|
|
6418
|
+
const pkg = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path13.join)(rootDir, "package.json"), "utf8"));
|
|
6419
|
+
return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["vue"]);
|
|
6420
|
+
} catch {
|
|
6421
|
+
return false;
|
|
6422
|
+
}
|
|
6423
|
+
}
|
|
6424
|
+
function selectSkills(framework, rootDir) {
|
|
6425
|
+
const names = [CONTRACT_SKILL];
|
|
6426
|
+
if (framework === "vite") {
|
|
6427
|
+
names.push(hasVueDependency(rootDir) ? "vue-scaffold" : "react-scaffold");
|
|
6428
|
+
} else {
|
|
6429
|
+
const scaffold = SCAFFOLD_BY_FRAMEWORK[framework];
|
|
6430
|
+
if (scaffold) names.push(scaffold);
|
|
6431
|
+
}
|
|
6432
|
+
return names.filter((name) => name in SKILLS);
|
|
6433
|
+
}
|
|
6434
|
+
function skillBody(name) {
|
|
6435
|
+
const raw = SKILLS[name];
|
|
6436
|
+
const match = raw.match(/^---\n[\s\S]*?\n---\n/);
|
|
6437
|
+
return (match ? raw.slice(match[0].length) : raw).trim();
|
|
6438
|
+
}
|
|
6439
|
+
function renderSkillsInstruction(names) {
|
|
6440
|
+
const known = names.filter((name) => name in SKILLS);
|
|
6441
|
+
if (known.length === 0) return null;
|
|
6442
|
+
const sections = known.map((name) => skillBody(name));
|
|
6443
|
+
return "# Platform deployment skills (mandatory)\n\nThese are the same skills the cloud build agent loads for this stack.\nFollow them when scaffolding or editing \u2014 they are not optional hints.\n\n" + sections.join("\n\n---\n\n") + "\n";
|
|
6444
|
+
}
|
|
6445
|
+
function skillsInstructionFor(framework, rootDir) {
|
|
6446
|
+
if (!(0, import_node_fs11.existsSync)(rootDir)) return null;
|
|
6447
|
+
return renderSkillsInstruction(selectSkills(framework, rootDir));
|
|
6448
|
+
}
|
|
6449
|
+
|
|
5240
6450
|
// src/lib/models.ts
|
|
5241
6451
|
async function fetchModelCatalog(auth, fallback) {
|
|
5242
6452
|
const regionDefaultsToBedrock = fallback.ai_region === "eu" || fallback.ai_region === "de";
|
|
@@ -5348,15 +6558,15 @@ function decideLocalProviders(policy) {
|
|
|
5348
6558
|
}
|
|
5349
6559
|
|
|
5350
6560
|
// src/lib/localProviders.ts
|
|
5351
|
-
var
|
|
5352
|
-
var
|
|
6561
|
+
var import_node_fs12 = require("fs");
|
|
6562
|
+
var import_node_path14 = require("path");
|
|
5353
6563
|
var LOCAL_PROVIDERS_FILE = "local-providers.json";
|
|
5354
6564
|
function localProvidersPath() {
|
|
5355
|
-
return (0,
|
|
6565
|
+
return (0, import_node_path14.join)(configDir(), LOCAL_PROVIDERS_FILE);
|
|
5356
6566
|
}
|
|
5357
6567
|
function readLocalProviders() {
|
|
5358
6568
|
try {
|
|
5359
|
-
const raw = JSON.parse((0,
|
|
6569
|
+
const raw = JSON.parse((0, import_node_fs12.readFileSync)(localProvidersPath(), "utf8"));
|
|
5360
6570
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
5361
6571
|
const map = raw.providers && typeof raw.providers === "object" ? raw.providers : raw;
|
|
5362
6572
|
const usable = {};
|
|
@@ -5391,8 +6601,8 @@ function resolveLocalProviders(policy) {
|
|
|
5391
6601
|
}
|
|
5392
6602
|
function writeLocalProvidersExample() {
|
|
5393
6603
|
const path = localProvidersPath();
|
|
5394
|
-
if ((0,
|
|
5395
|
-
(0,
|
|
6604
|
+
if ((0, import_node_fs12.existsSync)(path)) return path;
|
|
6605
|
+
(0, import_node_fs12.mkdirSync)(configDir(), { recursive: true });
|
|
5396
6606
|
const example = {
|
|
5397
6607
|
_comment: 'Providers with your own API key, running locally. Requests go directly to the provider \u2014 no Agentful credits, no platform region guarantee. Remove the leading underscore from "_anthropic" and fill in your key to enable it.',
|
|
5398
6608
|
_anthropic: {
|
|
@@ -5402,21 +6612,21 @@ function writeLocalProvidersExample() {
|
|
|
5402
6612
|
models: { "claude-sonnet-4-5": { name: "Claude Sonnet 4.5 (my key)" } }
|
|
5403
6613
|
}
|
|
5404
6614
|
};
|
|
5405
|
-
(0,
|
|
6615
|
+
(0, import_node_fs12.writeFileSync)(path, JSON.stringify(example, null, 2) + "\n", "utf8");
|
|
5406
6616
|
try {
|
|
5407
|
-
(0,
|
|
6617
|
+
(0, import_node_fs12.chmodSync)(path, 384);
|
|
5408
6618
|
} catch {
|
|
5409
6619
|
}
|
|
5410
6620
|
return path;
|
|
5411
6621
|
}
|
|
5412
6622
|
|
|
5413
6623
|
// src/lib/sessions.ts
|
|
5414
|
-
var
|
|
5415
|
-
var
|
|
6624
|
+
var import_node_fs13 = require("fs");
|
|
6625
|
+
var import_node_path15 = require("path");
|
|
5416
6626
|
var import_node_child_process4 = require("child_process");
|
|
5417
6627
|
function lastSessionForDirectory(cwd = process.cwd()) {
|
|
5418
|
-
const dbPath = (0,
|
|
5419
|
-
if (!(0,
|
|
6628
|
+
const dbPath = (0, import_node_path15.join)(engineXdg().dataHome, "opencode", "opencode.db");
|
|
6629
|
+
if (!(0, import_node_fs13.existsSync)(dbPath)) return null;
|
|
5420
6630
|
const escaped = cwd.replace(/'/g, "''");
|
|
5421
6631
|
const res = (0, import_node_child_process4.spawnSync)(
|
|
5422
6632
|
"sqlite3",
|
|
@@ -5456,10 +6666,21 @@ async function tuiCommand(opts = {}) {
|
|
|
5456
6666
|
fetchLocalProviderPolicy(auth)
|
|
5457
6667
|
]);
|
|
5458
6668
|
const local = resolveLocalProviders(policy);
|
|
5459
|
-
const
|
|
6669
|
+
const framework = detectFramework(process.cwd());
|
|
6670
|
+
const skills = skillsInstructionFor(framework, process.cwd());
|
|
6671
|
+
const xdg = await writeEngineSession(session, catalog, local.providers, framework, skills);
|
|
5460
6672
|
const binDir = ensureCliOnPath();
|
|
5461
6673
|
ui.info(`Signed in as ${auth.email} \u2014 usage runs on your Agentful credits.`);
|
|
5462
6674
|
if (project) ui.info(`Linked project: ${project.title} (\`agentful push\` for a live preview)`);
|
|
6675
|
+
if (framework === "unknown") {
|
|
6676
|
+
ui.info(paint.dim(
|
|
6677
|
+
`Mode: ${CLOUD_AGENT} (cloud target) \u2014 no supported framework detected here; a push would be rejected. Tab switches to ${LOCAL_AGENT} for free local work.`
|
|
6678
|
+
));
|
|
6679
|
+
} else {
|
|
6680
|
+
ui.info(paint.dim(
|
|
6681
|
+
`Mode: ${CLOUD_AGENT} \u2014 building for Agentful Cloud (${frameworkDisplayName(framework)}). Tab switches to ${LOCAL_AGENT} for work that is not meant to deploy.`
|
|
6682
|
+
));
|
|
6683
|
+
}
|
|
5463
6684
|
const localCount = Object.keys(local.providers).length;
|
|
5464
6685
|
ui.info(`${catalog.models.length + localCount} models available \u2014 switch with /model.`);
|
|
5465
6686
|
if (localCount) {
|
|
@@ -5484,7 +6705,7 @@ async function tuiCommand(opts = {}) {
|
|
|
5484
6705
|
}
|
|
5485
6706
|
console.log("");
|
|
5486
6707
|
if (process.stdout.isTTY) process.stdout.write(`\x1B]0;${brand.displayName}\x07`);
|
|
5487
|
-
const code = await new Promise((
|
|
6708
|
+
const code = await new Promise((resolve2) => {
|
|
5488
6709
|
const engineArgs = opts.session ? ["--session", opts.session] : [];
|
|
5489
6710
|
const child = (0, import_node_child_process5.spawn)(binPath, engineArgs, {
|
|
5490
6711
|
stdio: "inherit",
|
|
@@ -5493,7 +6714,7 @@ async function tuiCommand(opts = {}) {
|
|
|
5493
6714
|
...process.env,
|
|
5494
6715
|
// Slash commands run `agentful …` through the agent's bash tool, which
|
|
5495
6716
|
// has no access to the user's shell aliases.
|
|
5496
|
-
PATH: `${binDir}${
|
|
6717
|
+
PATH: `${binDir}${import_node_path16.delimiter}${process.env.PATH || ""}`,
|
|
5497
6718
|
XDG_CONFIG_HOME: xdg.configHome,
|
|
5498
6719
|
XDG_DATA_HOME: xdg.dataHome,
|
|
5499
6720
|
XDG_STATE_HOME: xdg.stateHome,
|
|
@@ -5511,10 +6732,10 @@ async function tuiCommand(opts = {}) {
|
|
|
5511
6732
|
...session.image_model ? { IMAGE_MODEL: session.image_model } : {}
|
|
5512
6733
|
}
|
|
5513
6734
|
});
|
|
5514
|
-
child.on("exit", (c) =>
|
|
6735
|
+
child.on("exit", (c) => resolve2(c ?? 0));
|
|
5515
6736
|
child.on("error", (err2) => {
|
|
5516
6737
|
ui.fail(`Could not start the engine: ${err2.message}`);
|
|
5517
|
-
|
|
6738
|
+
resolve2(1);
|
|
5518
6739
|
});
|
|
5519
6740
|
});
|
|
5520
6741
|
try {
|
|
@@ -5540,6 +6761,7 @@ async function tuiCommand(opts = {}) {
|
|
|
5540
6761
|
}
|
|
5541
6762
|
|
|
5542
6763
|
// src/commands/publish.ts
|
|
6764
|
+
init_branding();
|
|
5543
6765
|
var SUBDOMAIN_RE = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])?$/;
|
|
5544
6766
|
async function publishCommand(subdomain) {
|
|
5545
6767
|
console.log(banner());
|
|
@@ -5578,6 +6800,7 @@ async function publishCommand(subdomain) {
|
|
|
5578
6800
|
}
|
|
5579
6801
|
|
|
5580
6802
|
// src/commands/share.ts
|
|
6803
|
+
init_branding();
|
|
5581
6804
|
async function shareCommand(opts) {
|
|
5582
6805
|
const auth = await ensureAuth();
|
|
5583
6806
|
const project = requireProject();
|
|
@@ -5596,26 +6819,28 @@ async function shareCommand(opts) {
|
|
|
5596
6819
|
}
|
|
5597
6820
|
|
|
5598
6821
|
// src/commands/pull.ts
|
|
5599
|
-
var
|
|
5600
|
-
var
|
|
6822
|
+
var import_node_fs14 = require("fs");
|
|
6823
|
+
var import_node_path17 = require("path");
|
|
6824
|
+
init_branding();
|
|
6825
|
+
init_branding();
|
|
5601
6826
|
function isProbablyBase64Binary(path) {
|
|
5602
6827
|
return /\.(png|jpe?g|gif|webp|ico|woff2?|ttf|otf|eot|pdf|zip|mp[34]|webm|avif)$/i.test(path);
|
|
5603
6828
|
}
|
|
5604
6829
|
function writeEntry(root, rel, value) {
|
|
5605
|
-
const target = (0,
|
|
5606
|
-
(0,
|
|
6830
|
+
const target = (0, import_node_path17.join)(root, rel);
|
|
6831
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path17.dirname)(target), { recursive: true });
|
|
5607
6832
|
const content = typeof value === "object" && value !== null && "content" in value ? String(value.content) : String(value ?? "");
|
|
5608
6833
|
if (isProbablyBase64Binary(rel)) {
|
|
5609
|
-
(0,
|
|
6834
|
+
(0, import_node_fs14.writeFileSync)(target, Buffer.from(content, "base64"));
|
|
5610
6835
|
} else {
|
|
5611
|
-
(0,
|
|
6836
|
+
(0, import_node_fs14.writeFileSync)(target, content, "utf8");
|
|
5612
6837
|
}
|
|
5613
6838
|
}
|
|
5614
6839
|
async function pullCommand(opts) {
|
|
5615
6840
|
console.log(banner());
|
|
5616
6841
|
const auth = await ensureAuth();
|
|
5617
6842
|
const project = requireProject();
|
|
5618
|
-
const nonHidden = (0,
|
|
6843
|
+
const nonHidden = (0, import_node_fs14.readdirSync)(process.cwd()).filter((n) => n !== ".agentful" && !n.startsWith("."));
|
|
5619
6844
|
if (nonHidden.length > 0 && !opts.force) {
|
|
5620
6845
|
throw new ApiError(
|
|
5621
6846
|
0,
|
|
@@ -5639,9 +6864,9 @@ async function pullCommand(opts) {
|
|
|
5639
6864
|
ui.warn(`Skipped ${rel} (HTTP ${resp.status})`);
|
|
5640
6865
|
continue;
|
|
5641
6866
|
}
|
|
5642
|
-
const target = (0,
|
|
5643
|
-
(0,
|
|
5644
|
-
(0,
|
|
6867
|
+
const target = (0, import_node_path17.join)(process.cwd(), rel);
|
|
6868
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path17.dirname)(target), { recursive: true });
|
|
6869
|
+
(0, import_node_fs14.writeFileSync)(target, Buffer.from(await resp.arrayBuffer()));
|
|
5645
6870
|
written++;
|
|
5646
6871
|
}
|
|
5647
6872
|
} else if (data.files) {
|
|
@@ -5659,6 +6884,7 @@ async function pullCommand(opts) {
|
|
|
5659
6884
|
var THIRD_PARTY_LICENSES_default = 'Third-party software distributed with the Agentful CLI\n======================================================\n\n## Agentful engine (opencode)\n\nThe Agentful CLI downloads and runs the opencode engine\n(https://github.com/anomalyco/opencode) as its local coding engine.\nIt is distributed under the MIT License, reproduced below.\n\nMIT License\n\nCopyright (c) 2025 opencode\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n## Bundled npm dependencies\n\ncommander (MIT), fflate (MIT), ignore (MIT) \u2014 see their repositories\nfor the full license texts.\n';
|
|
5660
6885
|
|
|
5661
6886
|
// src/commands/licenses.ts
|
|
6887
|
+
init_branding();
|
|
5662
6888
|
async function licensesCommand() {
|
|
5663
6889
|
console.log(paint.bold(`${brand.name} v${VERSION}`));
|
|
5664
6890
|
console.log("");
|
|
@@ -5666,6 +6892,7 @@ async function licensesCommand() {
|
|
|
5666
6892
|
}
|
|
5667
6893
|
|
|
5668
6894
|
// src/commands/models.ts
|
|
6895
|
+
init_branding();
|
|
5669
6896
|
async function modelsCommand(opts) {
|
|
5670
6897
|
console.log(banner());
|
|
5671
6898
|
const auth = await ensureAuth();
|
|
@@ -5725,6 +6952,7 @@ async function modelsCommand(opts) {
|
|
|
5725
6952
|
|
|
5726
6953
|
// src/commands/upgrade.ts
|
|
5727
6954
|
var import_node_child_process6 = require("child_process");
|
|
6955
|
+
init_branding();
|
|
5728
6956
|
async function upgradeCommand() {
|
|
5729
6957
|
console.log(banner());
|
|
5730
6958
|
let latest = "";
|
|
@@ -5781,8 +7009,9 @@ program2.name("agentful").description("Agentful in your terminal \u2014 local de
|
|
|
5781
7009
|
program2.command("login").description("Sign in to your Agentful account (device flow)").option("--no-browser", "do not open the browser automatically").action(run((opts) => loginCommand({ browser: opts.browser !== false })));
|
|
5782
7010
|
program2.command("logout").description("Remove the stored credentials").action(run(logoutCommand));
|
|
5783
7011
|
program2.command("whoami").description("Show the signed-in account").action(run(whoamiCommand));
|
|
5784
|
-
program2.command("init").description("Create (or link) the Agentful cloud project for this directory").option("--link <projectId>", "link an existing project instead of creating one").option("--title <title>", "project title (skips the prompt)").action(run((opts) => initCommand(opts)));
|
|
5785
|
-
program2.command("push").description("Upload this project and get a live preview URL").option("--prebuilt", "upload a locally built output instead of building in the cloud").option("--dir <path>", "built-output directory for --prebuilt (default: auto-detect)").action(run((opts) => pushCommand(opts)));
|
|
7012
|
+
program2.command("init").description("Create (or link) the Agentful cloud project for this directory").argument("[directory]", "with --template: new folder to scaffold into (created for you)").option("--link <projectId>", "link an existing project instead of creating one").option("--title <title>", "project title (skips the prompt)").option("--template <framework>", "scaffold from a platform starter (nextjs, react, vue, vanilla)").action(run((directory, opts) => initCommand(opts, directory)));
|
|
7013
|
+
program2.command("push").description("Upload this project and get a live preview URL").option("--prebuilt", "upload a locally built output instead of building in the cloud").option("--dir <path>", "built-output directory for --prebuilt (default: auto-detect)").option("--force", "push even when the pre-upload compatibility check finds blocking issues").action(run((opts) => pushCommand(opts)));
|
|
7014
|
+
program2.command("build-status").description("Show the cloud's record of the last build (the real error on failures)").action(run(buildStatusCommand));
|
|
5786
7015
|
program2.command("open").description("Open the live preview in the browser").action(run(openCommand));
|
|
5787
7016
|
program2.command("tui").description("Start the Agentful coding TUI with your Agentful account").option("-s, --session <id>", "resume a previous session").action(run((opts) => tuiCommand(opts)));
|
|
5788
7017
|
program2.command("publish <subdomain>").description("Publish the built project at https://<subdomain>.agentful.dev").action(run(publishCommand));
|