create-contractor-site 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/bin/create-contractor-site.mjs +1 -1
- package/package.json +3 -3
- package/scripts/smoke-test.mjs +558 -12
- package/src/copy-template.mjs +11 -1
package/README.md
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Scaffold a client contractor website from the [website-multipages](https://github.com/glacayo/website-multipages) Astro template.
|
|
4
4
|
|
|
5
|
-
**Package version:** `2.
|
|
5
|
+
**Package version:** `2.3.0` · **Default template ref:** `v2.3.0` (`CREATE_CONTRACTOR_TEMPLATE_REF`)
|
|
6
6
|
|
|
7
7
|
The CLI validates the target, copies the template, replaces placeholder values in `src/data/*.json`, installs with **pnpm**, validates data, builds, and initializes git **only after** success.
|
|
8
8
|
|
|
9
|
-
## What's new in 2.
|
|
9
|
+
## What's new in 2.3.0
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
-
|
|
11
|
+
- **Page-shell reuse** — shared page header shell extracted across about/contact/remaining pages, reducing duplicated layout markup.
|
|
12
|
+
- **Isolated smart-image capsule** — optional image helpers live in a separate pnpm workspace at `tools/smart-image/`, outside the root dependency graph; root lockfile/workspace stay separate.
|
|
13
|
+
- **Scaffold/state protections** — `CUSTOMER-IMAGES/` and `.img-ia/` are gitignored and scaffold-denied; capsule `node_modules/` stays ignored; no credential/state bytes reach `dist/`.
|
|
14
|
+
- **Agent-owned image fulfillment** — `images:check` / `images:setup` / `images:run` wrappers; the active agent autonomously copies compliant candidates from `<image-root>/_out/` into `src/assets/images/` and updates JSON values + alt text (no human promotion step).
|
|
15
|
+
- **pnpm/Corepack compatibility fix** — pnpm pinned for Corepack compatibility so `pnpm install` is deterministic across environments.
|
|
16
|
+
- **Template clone default** — published fallback uses git ref **`v2.3.0`** unless you set `CREATE_CONTRACTOR_TEMPLATE_ROOT` or `CREATE_CONTRACTOR_TEMPLATE_REF`.
|
|
15
17
|
|
|
16
18
|
Full release notes: repository root [`CHANGELOG.md`](../../CHANGELOG.md).
|
|
17
19
|
|
|
@@ -61,7 +63,7 @@ CREATE_CONTRACTOR_TEMPLATE_ROOT=/path/to/website-multipages \
|
|
|
61
63
|
1. Checks that **pnpm** and **git** are available
|
|
62
64
|
2. Resolves the template source (see env vars below)
|
|
63
65
|
3. Validates the target directory and refuses targets equal to or inside the template root
|
|
64
|
-
4. Copies the template into the target directory (denylist excludes `node_modules`, `dist`, `.astro`, `.git`, `.codegraph`, `docs_trash`, `openspec`, `.atl`, `logs`, `*.log`, `.env*`, `package-lock.json`, and `packages/`)
|
|
66
|
+
4. Copies the template into the target directory (denylist excludes `node_modules`, `dist`, `.astro`, `.git`, `.codegraph`, `docs_trash`, `openspec`, `.atl`, `logs`, `*.log`, `.env*`, `package-lock.json`, and `packages/`)
|
|
65
67
|
5. Replaces **values only** in target `src/data/*.json` (schema/shape preserved)
|
|
66
68
|
6. Runs `pnpm install`
|
|
67
69
|
7. Runs `pnpm run validate:data`
|
|
@@ -88,7 +90,7 @@ See the template root `AGENTS.md`, `SKILL.md`, and `README.md` for the full agen
|
|
|
88
90
|
| `CREATE_CONTRACTOR_SITE_ANSWERS_JSON` | JSON object with client answers for scripted/non-interactive scaffolds |
|
|
89
91
|
| `CREATE_CONTRACTOR_TEMPLATE_ROOT` | Path to a local template checkout (preferred for monorepo/dev) |
|
|
90
92
|
| `CREATE_CONTRACTOR_TEMPLATE_REPO` | Git URL for the published fallback clone (default: this template repo) |
|
|
91
|
-
| `CREATE_CONTRACTOR_TEMPLATE_REF` | Git branch/tag/ref to clone (default: `v2.
|
|
93
|
+
| `CREATE_CONTRACTOR_TEMPLATE_REF` | Git branch/tag/ref to clone (default: `v2.3.0`) |
|
|
92
94
|
|
|
93
95
|
Template source precedence: `CREATE_CONTRACTOR_TEMPLATE_ROOT` → local monorepo discovery → temporary clone of repo @ ref.
|
|
94
96
|
|
|
@@ -46,7 +46,7 @@ Environment:
|
|
|
46
46
|
Default: https://github.com/glacayo/website-multipages.git
|
|
47
47
|
|
|
48
48
|
CREATE_CONTRACTOR_TEMPLATE_REF
|
|
49
|
-
Git branch/tag/ref to clone for the published fallback. Default: v2.
|
|
49
|
+
Git branch/tag/ref to clone for the published fallback. Default: v2.3.0
|
|
50
50
|
|
|
51
51
|
Answer defaults (buildAnswers; blank/whitespace → these; --yes omits trust/payment/hours/social/directories/siteType):
|
|
52
52
|
freeEstimate Free On-Site Estimate
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-contractor-site",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Scaffold a client contractor website from the placeholder multipage template",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/glacayo/website-multipages/tree/main/packages/create-contractor-site",
|
|
28
28
|
"engines": {
|
|
29
|
-
"node": ">=22.
|
|
29
|
+
"node": ">=22.13.0",
|
|
30
30
|
"pnpm": ">=11.1.2"
|
|
31
31
|
},
|
|
32
32
|
"devEngines": {
|
|
33
33
|
"packageManager": {
|
|
34
34
|
"name": "pnpm",
|
|
35
|
-
"version": "
|
|
35
|
+
"version": "11.18.0",
|
|
36
36
|
"onFail": "error"
|
|
37
37
|
}
|
|
38
38
|
},
|
package/scripts/smoke-test.mjs
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
* 1. Missing target argument
|
|
7
7
|
* 2. Existing non-empty target
|
|
8
8
|
* 3. Target equal to / inside template root
|
|
9
|
-
* 4. Published DEFAULT_TEMPLATE_REF / CREATE_CONTRACTOR_TEMPLATE_REF contract (v2.
|
|
9
|
+
* 4. Published DEFAULT_TEMPLATE_REF / CREATE_CONTRACTOR_TEMPLATE_REF contract (v2.3.0)
|
|
10
10
|
* 5. Duplicate service input normalization + slug uniqueness + business/services alignment
|
|
11
11
|
* 6. Service-area name parse/dedupe (Chesapeake duplicate-slug case) + areas rebuild
|
|
12
12
|
* without leaking stale template county/ZIP metadata into new areas
|
|
13
13
|
* 7. CREATE_CONTRACTOR_SITE_ANSWERS_JSON real CLI spawn (copy+replace, skip setup)
|
|
14
|
-
* 8.
|
|
14
|
+
* 8. Root workspace isolation vs tools/smart-image capsule (scope, sharp/zod, dist/)
|
|
15
|
+
* 9. Smart-image wrapper/lifecycle (argv safety, Windows bin, setup/check, no hooks)
|
|
16
|
+
* 10. Scaffold deny/gitignore/required retention (isDeniedName, check-ignore, copyTemplate)
|
|
17
|
+
* 11. Temp-target --yes scaffold (install/validate/build) unless SKIP_CLI_E2E=1
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import { spawnSync } from 'node:child_process';
|
|
@@ -50,9 +53,17 @@ import {
|
|
|
50
53
|
import { isSameOrInside, validateTarget, TargetValidationError } from '../src/validate-target.mjs';
|
|
51
54
|
import {
|
|
52
55
|
DEFAULT_TEMPLATE_REF,
|
|
56
|
+
REQUIRED_AFTER_COPY,
|
|
57
|
+
assertRequiredCopied,
|
|
58
|
+
copyTemplate,
|
|
53
59
|
findLocalTemplateRoot,
|
|
60
|
+
isDeniedName,
|
|
54
61
|
} from '../src/copy-template.mjs';
|
|
55
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
isVersionAtLeast,
|
|
64
|
+
resolveCommandPath,
|
|
65
|
+
runCommand,
|
|
66
|
+
} from '../src/run-command.mjs';
|
|
56
67
|
|
|
57
68
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
58
69
|
const CLI = path.resolve(__dirname, '../bin/create-contractor-site.mjs');
|
|
@@ -145,25 +156,25 @@ async function main() {
|
|
|
145
156
|
});
|
|
146
157
|
|
|
147
158
|
await test(
|
|
148
|
-
'DEFAULT_TEMPLATE_REF / CREATE_CONTRACTOR_TEMPLATE_REF published contract is v2.
|
|
159
|
+
'DEFAULT_TEMPLATE_REF / CREATE_CONTRACTOR_TEMPLATE_REF published contract is v2.3.0',
|
|
149
160
|
() => {
|
|
150
161
|
const copySrcPath = path.join(PKG_ROOT, 'src', 'copy-template.mjs');
|
|
151
162
|
const copySrc = fs.readFileSync(copySrcPath, 'utf8');
|
|
152
163
|
// Pin the source fallback so a silent ref bump cannot ship without updating this test.
|
|
153
164
|
assert(
|
|
154
|
-
/CREATE_CONTRACTOR_TEMPLATE_REF\s*\|\|\s*['"]v2\.
|
|
155
|
-
'copy-template.mjs DEFAULT_TEMPLATE_REF fallback must be v2.
|
|
165
|
+
/CREATE_CONTRACTOR_TEMPLATE_REF\s*\|\|\s*['"]v2\.3\.0['"]/.test(copySrc),
|
|
166
|
+
'copy-template.mjs DEFAULT_TEMPLATE_REF fallback must be v2.3.0',
|
|
156
167
|
);
|
|
157
168
|
const expected =
|
|
158
|
-
process.env.CREATE_CONTRACTOR_TEMPLATE_REF || 'v2.
|
|
169
|
+
process.env.CREATE_CONTRACTOR_TEMPLATE_REF || 'v2.3.0';
|
|
159
170
|
assert(
|
|
160
171
|
DEFAULT_TEMPLATE_REF === expected,
|
|
161
|
-
`DEFAULT_TEMPLATE_REF must equal CREATE_CONTRACTOR_TEMPLATE_REF or v2.
|
|
172
|
+
`DEFAULT_TEMPLATE_REF must equal CREATE_CONTRACTOR_TEMPLATE_REF or v2.3.0 (got ${DEFAULT_TEMPLATE_REF})`,
|
|
162
173
|
);
|
|
163
174
|
if (!process.env.CREATE_CONTRACTOR_TEMPLATE_REF) {
|
|
164
175
|
assert(
|
|
165
|
-
DEFAULT_TEMPLATE_REF === 'v2.
|
|
166
|
-
`published default template ref must be v2.
|
|
176
|
+
DEFAULT_TEMPLATE_REF === 'v2.3.0',
|
|
177
|
+
`published default template ref must be v2.3.0 (got ${DEFAULT_TEMPLATE_REF})`,
|
|
167
178
|
);
|
|
168
179
|
}
|
|
169
180
|
|
|
@@ -175,8 +186,8 @@ async function main() {
|
|
|
175
186
|
'help must document CREATE_CONTRACTOR_TEMPLATE_REF',
|
|
176
187
|
);
|
|
177
188
|
assert(
|
|
178
|
-
/v2\.
|
|
179
|
-
'help must document default template ref v2.
|
|
189
|
+
/v2\.3\.0/.test(helpOut),
|
|
190
|
+
'help must document default template ref v2.3.0',
|
|
180
191
|
);
|
|
181
192
|
},
|
|
182
193
|
);
|
|
@@ -1105,10 +1116,521 @@ async function main() {
|
|
|
1105
1116
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1106
1117
|
});
|
|
1107
1118
|
|
|
1119
|
+
// --- image-tooling capsule isolation (Unit 1) ---
|
|
1120
|
+
await test('root workspace stays two projects and excludes tools/*', () => {
|
|
1121
|
+
assert(REPO_ROOT, 'expected local template root');
|
|
1122
|
+
const wsPath = path.join(REPO_ROOT, 'pnpm-workspace.yaml');
|
|
1123
|
+
const wsText = fs.readFileSync(wsPath, 'utf8');
|
|
1124
|
+
assert(/\n\s*-\s*\.\s*$/m.test(wsText) || /packages:\s*\n\s*-\s*\./.test(wsText),
|
|
1125
|
+
'root workspace must include "."');
|
|
1126
|
+
assert(/-\s*packages\/\*/.test(wsText), 'root workspace must include packages/*');
|
|
1127
|
+
assert(!/tools\//.test(wsText), 'root workspace must not absorb tools/*');
|
|
1128
|
+
assert(
|
|
1129
|
+
!/better-sqlite3/.test(wsText),
|
|
1130
|
+
'root pnpm-workspace.yaml must not declare allowBuilds.better-sqlite3',
|
|
1131
|
+
);
|
|
1132
|
+
|
|
1133
|
+
const list = runCommand('pnpm', ['list', '-r', '--depth', '-1'], {
|
|
1134
|
+
cwd: REPO_ROOT,
|
|
1135
|
+
stdio: 'pipe',
|
|
1136
|
+
});
|
|
1137
|
+
// pnpm prints one name@version line per workspace package
|
|
1138
|
+
const names = (list.stdout || '').match(/^[^\s].*@\d/gm) || [];
|
|
1139
|
+
assert(
|
|
1140
|
+
names.length === 2,
|
|
1141
|
+
`expected exactly 2 root workspace projects, got ${names.length}: ${names.join(', ')}`,
|
|
1142
|
+
);
|
|
1143
|
+
assert(
|
|
1144
|
+
!/smart-image/i.test(list.stdout || ''),
|
|
1145
|
+
'root workspace listing must not include smart-image capsule',
|
|
1146
|
+
);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
await test('root sharp/zod ranges stay unchanged with no overrides', () => {
|
|
1150
|
+
assert(REPO_ROOT, 'expected local template root');
|
|
1151
|
+
const pkg = readJson(path.join(REPO_ROOT, 'package.json'));
|
|
1152
|
+
const sharp = pkg.devDependencies?.sharp ?? pkg.dependencies?.sharp;
|
|
1153
|
+
const zod = pkg.devDependencies?.zod ?? pkg.dependencies?.zod;
|
|
1154
|
+
assert(sharp === '^0.34.5', `root sharp range must stay ^0.34.5, got ${sharp}`);
|
|
1155
|
+
assert(zod === '^4.1.5', `root zod range must stay ^4.1.5, got ${zod}`);
|
|
1156
|
+
assert(pkg.overrides == null, 'root package.json must not declare overrides');
|
|
1157
|
+
assert(
|
|
1158
|
+
pkg.pnpm?.overrides == null,
|
|
1159
|
+
'root package.json must not declare pnpm.overrides',
|
|
1160
|
+
);
|
|
1161
|
+
assert(
|
|
1162
|
+
!pkg.dependencies?.['smart-image-cli'] && !pkg.devDependencies?.['smart-image-cli'],
|
|
1163
|
+
'root package.json must not depend on smart-image-cli',
|
|
1164
|
+
);
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
await test('dist holds zero capsule or smart-image-cli references', () => {
|
|
1168
|
+
assert(REPO_ROOT, 'expected local template root');
|
|
1169
|
+
const distDir = path.join(REPO_ROOT, 'dist');
|
|
1170
|
+
if (!fs.existsSync(distDir)) {
|
|
1171
|
+
console.log(' (dist/ absent — skip content scan; build harness covers post-build)');
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
/** @type {string[]} */
|
|
1175
|
+
const hits = [];
|
|
1176
|
+
const needles = ['smart-image-cli', 'better-sqlite3', 'tools/smart-image', 'tools\\smart-image'];
|
|
1177
|
+
/**
|
|
1178
|
+
* @param {string} dir
|
|
1179
|
+
*/
|
|
1180
|
+
function walk(dir) {
|
|
1181
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1182
|
+
const full = path.join(dir, ent.name);
|
|
1183
|
+
if (ent.isDirectory()) {
|
|
1184
|
+
walk(full);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
// Skip large binary assets; scan text-ish build outputs only.
|
|
1188
|
+
if (!/\.(html|js|mjs|cjs|css|json|txt|xml|svg|map)$/i.test(ent.name)) continue;
|
|
1189
|
+
let text;
|
|
1190
|
+
try {
|
|
1191
|
+
text = fs.readFileSync(full, 'utf8');
|
|
1192
|
+
} catch {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
for (const n of needles) {
|
|
1196
|
+
if (text.includes(n)) hits.push(`${path.relative(distDir, full)}:${n}`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
walk(distDir);
|
|
1201
|
+
assert(hits.length === 0, `dist/ must not reference capsule/CLI: ${hits.slice(0, 10).join(', ')}`);
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
// --- image-tooling wrapper + lifecycle (Unit 2) ---
|
|
1205
|
+
const capDir = REPO_ROOT ? path.join(REPO_ROOT, 'tools', 'smart-image') : '';
|
|
1206
|
+
const runJs = capDir ? path.join(capDir, 'run.mjs') : '';
|
|
1207
|
+
const checkJs = capDir ? path.join(capDir, 'check.mjs') : '';
|
|
1208
|
+
|
|
1209
|
+
/** @param {string} v */
|
|
1210
|
+
const qCmd = (v) =>
|
|
1211
|
+
v === '' ? '""' : /[\s"&<>|^%!]/.test(v) ? `"${String(v).replace(/"/g, '""')}"` : v;
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* @param {string} cmd
|
|
1215
|
+
* @param {string[]} args
|
|
1216
|
+
* @param {{ env?: NodeJS.ProcessEnv, cwd?: string }} [o]
|
|
1217
|
+
*/
|
|
1218
|
+
function spawnArgv(cmd, args, o = {}) {
|
|
1219
|
+
let c = cmd;
|
|
1220
|
+
let a = [...args];
|
|
1221
|
+
let verbatim = false;
|
|
1222
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(cmd)) {
|
|
1223
|
+
c = process.env.ComSpec || 'cmd.exe';
|
|
1224
|
+
a = ['/d', '/s', '/c', `"${[cmd, ...args].map(qCmd).join(' ')}"`];
|
|
1225
|
+
verbatim = true;
|
|
1226
|
+
}
|
|
1227
|
+
return spawnSync(c, a, {
|
|
1228
|
+
encoding: 'utf8',
|
|
1229
|
+
cwd: o.cwd || REPO_ROOT || process.cwd(),
|
|
1230
|
+
env: { ...process.env, ...(o.env || {}) },
|
|
1231
|
+
shell: false,
|
|
1232
|
+
windowsHide: true,
|
|
1233
|
+
windowsVerbatimArguments: verbatim,
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/** @param {string[]} args @param {{ env?: NodeJS.ProcessEnv, cwd?: string }} [o] */
|
|
1238
|
+
function runPnpm(args, o = {}) {
|
|
1239
|
+
const bin = resolveCommandPath('pnpm');
|
|
1240
|
+
assert(Boolean(bin), 'pnpm required');
|
|
1241
|
+
return spawnArgv(/** @type {string} */ (bin), args, o);
|
|
1242
|
+
}
|
|
1243
|
+
/** @param {string[]} args @param {{ env?: NodeJS.ProcessEnv }} [o] */
|
|
1244
|
+
const imgRun = (args, o = {}) =>
|
|
1245
|
+
spawnSync(process.execPath, [runJs, ...args], {
|
|
1246
|
+
encoding: 'utf8',
|
|
1247
|
+
cwd: REPO_ROOT,
|
|
1248
|
+
env: { ...process.env, ...(o.env || {}) },
|
|
1249
|
+
shell: false,
|
|
1250
|
+
windowsHide: true,
|
|
1251
|
+
});
|
|
1252
|
+
/** @param {{ env?: NodeJS.ProcessEnv }} [o] */
|
|
1253
|
+
const imgCheck = (o = {}) =>
|
|
1254
|
+
spawnSync(process.execPath, [checkJs], {
|
|
1255
|
+
encoding: 'utf8',
|
|
1256
|
+
cwd: REPO_ROOT,
|
|
1257
|
+
env: { ...process.env, ...(o.env || {}) },
|
|
1258
|
+
shell: false,
|
|
1259
|
+
windowsHide: true,
|
|
1260
|
+
});
|
|
1261
|
+
/** @param {import('node:child_process').SpawnSyncReturns<string>} r */
|
|
1262
|
+
const out = (r) => `${r.stdout || ''}\n${r.stderr || ''}`;
|
|
1263
|
+
|
|
1264
|
+
await test('images:run passes shell metacharacters as literal argv (exit 3, no side effects)', () => {
|
|
1265
|
+
assert(REPO_ROOT && fs.existsSync(runJs), 'run.mjs required');
|
|
1266
|
+
const marker = path.join(os.tmpdir(), `si-side-${process.pid}.txt`);
|
|
1267
|
+
if (fs.existsSync(marker)) fs.rmSync(marker, { force: true });
|
|
1268
|
+
for (const payload of [
|
|
1269
|
+
'--json; rm -rf /',
|
|
1270
|
+
`$(echo pwned > "${marker}")`,
|
|
1271
|
+
`&& echo pwned > "${marker}"`,
|
|
1272
|
+
]) {
|
|
1273
|
+
for (const r of [runPnpm(['run', 'images:run', '--', payload]), imgRun([payload])]) {
|
|
1274
|
+
assert(r.status === 3, `exit 3 expected, got ${r.status}: ${out(r).slice(0, 300)}`);
|
|
1275
|
+
assert(/invalid_input|unknown|error/i.test(out(r)), `invalid_input for ${payload}`);
|
|
1276
|
+
}
|
|
1277
|
+
assert(!fs.existsSync(marker), `no side-effect file for ${payload}`);
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
await test('Windows smart-img bin is silent while wrapper emits parseable doctor JSON', () => {
|
|
1282
|
+
assert(REPO_ROOT && fs.existsSync(runJs), 'run.mjs required');
|
|
1283
|
+
const binCmd = path.join(capDir, 'node_modules', '.bin', 'smart-img.CMD');
|
|
1284
|
+
assert(
|
|
1285
|
+
fs.existsSync(binCmd) || fs.existsSync(path.join(capDir, 'node_modules', '.bin', 'smart-img')),
|
|
1286
|
+
'smart-img bin required',
|
|
1287
|
+
);
|
|
1288
|
+
if (process.platform === 'win32' && fs.existsSync(binCmd)) {
|
|
1289
|
+
const bin = spawnArgv(binCmd, ['doctor', '--json']);
|
|
1290
|
+
assert(
|
|
1291
|
+
`${bin.stdout || ''}${bin.stderr || ''}`.trim().length === 0,
|
|
1292
|
+
`bin must be silent, got ${out(bin).slice(0, 200)}`,
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
for (const wrap of [runPnpm(['run', 'images:run', '--', 'doctor', '--json']), imgRun(['doctor', '--json'])]) {
|
|
1296
|
+
assert(wrap.status === 0, `wrapper exit 0, got ${wrap.status}: ${wrap.stderr}`);
|
|
1297
|
+
const parsed = JSON.parse((wrap.stdout || '').trim());
|
|
1298
|
+
assert(parsed.command === 'doctor' || parsed.ok === true, 'doctor JSON');
|
|
1299
|
+
assert(Array.isArray(parsed.details?.checks) || parsed.status === 'success', 'doctor checks');
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
await test('images:* never hooked from install/build/validate/CI; scripts exist', () => {
|
|
1304
|
+
assert(REPO_ROOT, 'repo root');
|
|
1305
|
+
const pkg = readJson(path.join(REPO_ROOT, 'package.json'));
|
|
1306
|
+
const s = pkg.scripts || {};
|
|
1307
|
+
assert(/frozen-lockfile/.test(s['images:setup'] || '') && /tools\/smart-image/.test(s['images:setup'] || ''));
|
|
1308
|
+
assert(/tools\/smart-image\/run\.mjs/.test(s['images:run'] || ''));
|
|
1309
|
+
assert(/tools\/smart-image\/check\.mjs/.test(s['images:check'] || ''));
|
|
1310
|
+
for (const k of ['preinstall', 'postinstall', 'prepare', 'prepublishOnly', 'build', 'validate:data', 'ci:install']) {
|
|
1311
|
+
assert(!/images:/.test(s[k] || ''), `${k} must not reference images:`);
|
|
1312
|
+
}
|
|
1313
|
+
const ci = path.join(REPO_ROOT, '.github');
|
|
1314
|
+
if (fs.existsSync(ci)) {
|
|
1315
|
+
/** @param {string} d */
|
|
1316
|
+
const walk = (d) => {
|
|
1317
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
1318
|
+
const f = path.join(d, e.name);
|
|
1319
|
+
if (e.isDirectory()) walk(f);
|
|
1320
|
+
else if (/\.ya?ml$/i.test(e.name)) {
|
|
1321
|
+
assert(!/images:(check|setup|run)/.test(fs.readFileSync(f, 'utf8')), `CI ${f}`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
walk(ci);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
await test('missing capsule / corrupt lock / offline setup remediate; provider is warning', () => {
|
|
1330
|
+
assert(REPO_ROOT && fs.existsSync(checkJs) && fs.existsSync(capDir), 'capsule required');
|
|
1331
|
+
for (const p of [
|
|
1332
|
+
path.join(REPO_ROOT, 'tools', 'smart-image-MISSING', 'check.mjs'),
|
|
1333
|
+
path.join(REPO_ROOT, 'tools', 'smart-image-MISSING', 'run.mjs'),
|
|
1334
|
+
]) {
|
|
1335
|
+
const r = spawnSync(process.execPath, [p], { encoding: 'utf8', shell: false, windowsHide: true });
|
|
1336
|
+
assert(r.error || r.status !== 0, `missing path must fail: ${p}`);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const tmpCap = fs.mkdtempSync(path.join(os.tmpdir(), 'si-cap-'));
|
|
1340
|
+
const emptyStore = fs.mkdtempSync(path.join(os.tmpdir(), 'si-store-'));
|
|
1341
|
+
try {
|
|
1342
|
+
for (const f of ['package.json', 'pnpm-workspace.yaml', 'pnpm-lock.yaml', 'check.mjs', 'run.mjs']) {
|
|
1343
|
+
fs.copyFileSync(path.join(capDir, f), path.join(tmpCap, f));
|
|
1344
|
+
}
|
|
1345
|
+
const noNm = spawnSync(process.execPath, [path.join(tmpCap, 'check.mjs')], {
|
|
1346
|
+
encoding: 'utf8',
|
|
1347
|
+
shell: false,
|
|
1348
|
+
windowsHide: true,
|
|
1349
|
+
});
|
|
1350
|
+
assert(noNm.status === 1 && /images:setup|not ready|not installed/i.test(out(noNm)));
|
|
1351
|
+
const noRun = spawnSync(process.execPath, [path.join(tmpCap, 'run.mjs'), 'doctor', '--json'], {
|
|
1352
|
+
encoding: 'utf8',
|
|
1353
|
+
shell: false,
|
|
1354
|
+
windowsHide: true,
|
|
1355
|
+
});
|
|
1356
|
+
assert(noRun.status === 1 && /images:setup/i.test(out(noRun)));
|
|
1357
|
+
const offline = runPnpm(
|
|
1358
|
+
['install', '--frozen-lockfile', '--dir', tmpCap, '--offline', '--store-dir', emptyStore],
|
|
1359
|
+
);
|
|
1360
|
+
assert(offline.status !== 0 && /ERR_|error|failed|offline|not found|ENOENT/i.test(out(offline)));
|
|
1361
|
+
} finally {
|
|
1362
|
+
fs.rmSync(tmpCap, { recursive: true, force: true });
|
|
1363
|
+
fs.rmSync(emptyStore, { recursive: true, force: true });
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const lockPath = path.join(capDir, 'pnpm-lock.yaml');
|
|
1367
|
+
const lockOrig = fs.readFileSync(lockPath);
|
|
1368
|
+
try {
|
|
1369
|
+
fs.writeFileSync(lockPath, 'this: is: not: valid: lockfile\n');
|
|
1370
|
+
const setup = runPnpm(['run', 'images:setup']);
|
|
1371
|
+
assert(setup.status !== 0 && /ERR_|error|failed|lock/i.test(out(setup)));
|
|
1372
|
+
const bad = imgCheck();
|
|
1373
|
+
assert(bad.status === 1 && /corrupt|lockfile|not ready|Remediation|images:setup/i.test(out(bad)));
|
|
1374
|
+
} finally {
|
|
1375
|
+
fs.writeFileSync(lockPath, lockOrig);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const healthy = imgCheck();
|
|
1379
|
+
assert(healthy.status === 0 && /ready/i.test(healthy.stdout || ''));
|
|
1380
|
+
|
|
1381
|
+
const emptyCfg = fs.mkdtempSync(path.join(os.tmpdir(), 'si-cfg-'));
|
|
1382
|
+
try {
|
|
1383
|
+
const env = {
|
|
1384
|
+
...process.env,
|
|
1385
|
+
APPDATA: emptyCfg,
|
|
1386
|
+
XDG_CONFIG_HOME: emptyCfg,
|
|
1387
|
+
HOME: emptyCfg,
|
|
1388
|
+
USERPROFILE: emptyCfg,
|
|
1389
|
+
};
|
|
1390
|
+
const warn = imgCheck({ env });
|
|
1391
|
+
assert(warn.status === 0 && /ready/i.test(warn.stdout || ''));
|
|
1392
|
+
assert(/warning:.*provider/i.test(warn.stderr || ''), warn.stderr);
|
|
1393
|
+
const pick = imgRun(
|
|
1394
|
+
[
|
|
1395
|
+
'pick',
|
|
1396
|
+
REPO_ROOT,
|
|
1397
|
+
'--source',
|
|
1398
|
+
'pixabay',
|
|
1399
|
+
'--category',
|
|
1400
|
+
'exterior',
|
|
1401
|
+
'--orientation',
|
|
1402
|
+
'landscape',
|
|
1403
|
+
'--width',
|
|
1404
|
+
'800',
|
|
1405
|
+
'--height',
|
|
1406
|
+
'600',
|
|
1407
|
+
'--json',
|
|
1408
|
+
],
|
|
1409
|
+
{ env },
|
|
1410
|
+
);
|
|
1411
|
+
assert(pick.status !== 0, `provider pick must fail, got ${pick.status}`);
|
|
1412
|
+
assert(
|
|
1413
|
+
pick.status === 4 || /provider|api.?key|pixabay|failed|error/i.test(out(pick)),
|
|
1414
|
+
out(pick).slice(0, 300),
|
|
1415
|
+
);
|
|
1416
|
+
} finally {
|
|
1417
|
+
fs.rmSync(emptyCfg, { recursive: true, force: true });
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
// --- image-tooling scaffold / git isolation (Unit 3) ---
|
|
1422
|
+
await test('isDeniedName covers every deny entry, rules, and near-miss negatives', () => {
|
|
1423
|
+
const denyDirs = [
|
|
1424
|
+
'node_modules',
|
|
1425
|
+
'dist',
|
|
1426
|
+
'.astro',
|
|
1427
|
+
'.git',
|
|
1428
|
+
'.codegraph',
|
|
1429
|
+
'docs_trash',
|
|
1430
|
+
'openspec',
|
|
1431
|
+
'logs',
|
|
1432
|
+
'packages',
|
|
1433
|
+
'.atl',
|
|
1434
|
+
'CUSTOMER-IMAGES',
|
|
1435
|
+
'.img-ia',
|
|
1436
|
+
];
|
|
1437
|
+
const denyFiles = ['package-lock.json'];
|
|
1438
|
+
for (const name of denyDirs) {
|
|
1439
|
+
assert(isDeniedName(name) === true, `deny dir ${name}`);
|
|
1440
|
+
}
|
|
1441
|
+
for (const name of denyFiles) {
|
|
1442
|
+
assert(isDeniedName(name) === true, `deny file ${name}`);
|
|
1443
|
+
}
|
|
1444
|
+
assert(isDeniedName('debug.log') === true, '*.log rule');
|
|
1445
|
+
assert(isDeniedName('app.log') === true, '*.log rule (app.log)');
|
|
1446
|
+
assert(isDeniedName('.env') === true, '.env* rule');
|
|
1447
|
+
assert(isDeniedName('.env.local') === true, '.env* rule (.env.local)');
|
|
1448
|
+
assert(isDeniedName('.env.production') === true, '.env* rule (.env.production)');
|
|
1449
|
+
// Near-miss / unrelated basenames must stay allowed
|
|
1450
|
+
assert(isDeniedName('CUSTOMER-IMAGES.md') === false, 'CUSTOMER-IMAGES.md near-miss');
|
|
1451
|
+
assert(isDeniedName('my-CUSTOMER-IMAGES') === false, 'my-CUSTOMER-IMAGES near-miss');
|
|
1452
|
+
assert(isDeniedName('tools') === false, 'tools must never be denied');
|
|
1453
|
+
assert(isDeniedName('smart-image') === false, 'smart-image basename allowed');
|
|
1454
|
+
assert(isDeniedName('_out') === false, 'bare _out must never be denied');
|
|
1455
|
+
assert(isDeniedName('README.sh') === false, 'README.sh alone is not denied');
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
await test('git check-ignore is root-relative for state and capsule node_modules only', () => {
|
|
1459
|
+
assert(REPO_ROOT, 'expected local template root');
|
|
1460
|
+
/**
|
|
1461
|
+
* @param {string} rel
|
|
1462
|
+
* @returns {boolean} true when git treats path as ignored
|
|
1463
|
+
*/
|
|
1464
|
+
function isIgnored(rel) {
|
|
1465
|
+
const r = spawnSync('git', ['check-ignore', '-q', '--', rel], {
|
|
1466
|
+
cwd: REPO_ROOT,
|
|
1467
|
+
encoding: 'utf8',
|
|
1468
|
+
shell: false,
|
|
1469
|
+
windowsHide: true,
|
|
1470
|
+
});
|
|
1471
|
+
if (r.error) throw r.error;
|
|
1472
|
+
return r.status === 0;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Directory patterns use trailing slash; also prove bare dir names when they exist.
|
|
1476
|
+
assert(isIgnored('CUSTOMER-IMAGES/') === true, 'root CUSTOMER-IMAGES/ ignored');
|
|
1477
|
+
assert(isIgnored('.img-ia/') === true, 'root .img-ia/ ignored');
|
|
1478
|
+
assert(isIgnored('tools/smart-image/node_modules') === true, 'capsule node_modules ignored');
|
|
1479
|
+
assert(isIgnored('tools/smart-image/node_modules/') === true, 'capsule node_modules/ ignored');
|
|
1480
|
+
|
|
1481
|
+
const probeCi = path.join(REPO_ROOT, 'CUSTOMER-IMAGES');
|
|
1482
|
+
const probeImg = path.join(REPO_ROOT, '.img-ia');
|
|
1483
|
+
/** @type {string[]} */
|
|
1484
|
+
const probes = [];
|
|
1485
|
+
try {
|
|
1486
|
+
if (!fs.existsSync(probeCi)) {
|
|
1487
|
+
fs.mkdirSync(probeCi, { recursive: true });
|
|
1488
|
+
probes.push(probeCi);
|
|
1489
|
+
}
|
|
1490
|
+
if (!fs.existsSync(probeImg)) {
|
|
1491
|
+
fs.mkdirSync(probeImg, { recursive: true });
|
|
1492
|
+
probes.push(probeImg);
|
|
1493
|
+
}
|
|
1494
|
+
assert(isIgnored('CUSTOMER-IMAGES') === true, 'root CUSTOMER-IMAGES dir ignored');
|
|
1495
|
+
assert(isIgnored('.img-ia') === true, 'root .img-ia dir ignored');
|
|
1496
|
+
assert(isIgnored('CUSTOMER-IMAGES/secret.jpg') === true, 'root CUSTOMER-IMAGES child ignored');
|
|
1497
|
+
assert(isIgnored('.img-ia/_out/x.png') === true, 'root .img-ia child ignored');
|
|
1498
|
+
} finally {
|
|
1499
|
+
for (const p of probes) {
|
|
1500
|
+
try {
|
|
1501
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
1502
|
+
} catch {
|
|
1503
|
+
// ignore
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Nested lookalikes remain trackable (root-anchored / patterns only).
|
|
1509
|
+
// Avoid bare trailing-slash probes here: CRLF blank gitignore lines can
|
|
1510
|
+
// confuse check-ignore's directory-only matching on some Windows git builds.
|
|
1511
|
+
assert(isIgnored('src/CUSTOMER-IMAGES') === false, 'nested CUSTOMER-IMAGES trackable');
|
|
1512
|
+
assert(isIgnored('src/CUSTOMER-IMAGES/photo.jpg') === false, 'nested CUSTOMER-IMAGES file trackable');
|
|
1513
|
+
assert(isIgnored('docs/.img-ia') === false, 'nested .img-ia trackable');
|
|
1514
|
+
assert(isIgnored('docs/.img-ia/readme') === false, 'nested .img-ia child trackable');
|
|
1515
|
+
assert(isIgnored('packages/create-contractor-site/.img-ia') === false, 'nested package .img-ia trackable');
|
|
1516
|
+
|
|
1517
|
+
// Capsule source / manifest / lock / wrapper remain trackable
|
|
1518
|
+
for (const rel of [
|
|
1519
|
+
'tools/smart-image/package.json',
|
|
1520
|
+
'tools/smart-image/pnpm-workspace.yaml',
|
|
1521
|
+
'tools/smart-image/pnpm-lock.yaml',
|
|
1522
|
+
'tools/smart-image/run.mjs',
|
|
1523
|
+
'tools/smart-image/check.mjs',
|
|
1524
|
+
]) {
|
|
1525
|
+
assert(isIgnored(rel) === false, `${rel} must remain trackable`);
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
await test('copyTemplate excludes denied parents, keeps unrelated _out, required misses loud', () => {
|
|
1530
|
+
const src = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-deny-src-'));
|
|
1531
|
+
const dest = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-deny-dst-'));
|
|
1532
|
+
try {
|
|
1533
|
+
// Minimal template-shaped tree
|
|
1534
|
+
fs.writeFileSync(path.join(src, 'keep.txt'), 'safe');
|
|
1535
|
+
fs.mkdirSync(path.join(src, 'CUSTOMER-IMAGES'), { recursive: true });
|
|
1536
|
+
fs.writeFileSync(path.join(src, 'CUSTOMER-IMAGES', 'secret.jpg'), 'NOCOPY');
|
|
1537
|
+
fs.mkdirSync(path.join(src, '.img-ia', '_out'), { recursive: true });
|
|
1538
|
+
fs.writeFileSync(path.join(src, '.img-ia', 'README.sh'), '#!/bin/sh\necho leak\n');
|
|
1539
|
+
fs.writeFileSync(path.join(src, '.img-ia', '_out', 'candidate.png'), 'NOCOPY');
|
|
1540
|
+
fs.mkdirSync(path.join(src, '_out'), { recursive: true });
|
|
1541
|
+
fs.writeFileSync(path.join(src, '_out', 'client-keep.txt'), 'KEEP');
|
|
1542
|
+
fs.mkdirSync(path.join(src, 'tools', 'smart-image'), { recursive: true });
|
|
1543
|
+
fs.writeFileSync(path.join(src, 'tools', 'smart-image', 'run.mjs'), '// wrapper');
|
|
1544
|
+
|
|
1545
|
+
const { skipped } = copyTemplate(src, dest);
|
|
1546
|
+
const skippedNorm = skipped.map((s) => s.replaceAll('\\', '/'));
|
|
1547
|
+
|
|
1548
|
+
assert(fs.existsSync(path.join(dest, 'keep.txt')), 'unrelated file copies');
|
|
1549
|
+
assert(fs.existsSync(path.join(dest, '_out', 'client-keep.txt')), 'unrelated _out copies');
|
|
1550
|
+
assert(fs.existsSync(path.join(dest, 'tools', 'smart-image', 'run.mjs')), 'tools/smart-image retained');
|
|
1551
|
+
assert(!fs.existsSync(path.join(dest, 'CUSTOMER-IMAGES')), 'CUSTOMER-IMAGES excluded');
|
|
1552
|
+
assert(!fs.existsSync(path.join(dest, '.img-ia')), '.img-ia excluded (parent deny)');
|
|
1553
|
+
assert(
|
|
1554
|
+
!fs.existsSync(path.join(dest, '.img-ia', 'README.sh')),
|
|
1555
|
+
'.img-ia/README.sh absent — no recurse into denied parent',
|
|
1556
|
+
);
|
|
1557
|
+
assert(
|
|
1558
|
+
!fs.existsSync(path.join(dest, '.img-ia', '_out', 'candidate.png')),
|
|
1559
|
+
'.img-ia/_out bytes never copied',
|
|
1560
|
+
);
|
|
1561
|
+
assert(skippedNorm.includes('CUSTOMER-IMAGES'), 'skipped lists CUSTOMER-IMAGES');
|
|
1562
|
+
assert(skippedNorm.includes('.img-ia'), 'skipped lists .img-ia');
|
|
1563
|
+
assert(!skippedNorm.some((s) => s === '_out' || s.startsWith('_out/')), '_out not skipped');
|
|
1564
|
+
|
|
1565
|
+
// assertRequiredCopied: every required capsule/wrapper/skill path missing is reported
|
|
1566
|
+
const stubRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-req-'));
|
|
1567
|
+
try {
|
|
1568
|
+
for (const rel of REQUIRED_AFTER_COPY) {
|
|
1569
|
+
const full = path.join(stubRoot, ...rel.split('/'));
|
|
1570
|
+
const looksFile = path.extname(rel) !== '' || rel.endsWith('package.json');
|
|
1571
|
+
if (looksFile) {
|
|
1572
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
1573
|
+
fs.writeFileSync(full, 'x');
|
|
1574
|
+
} else {
|
|
1575
|
+
fs.mkdirSync(full, { recursive: true });
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
assert(assertRequiredCopied(stubRoot).length === 0, 'full stub must pass required check');
|
|
1579
|
+
|
|
1580
|
+
const requiredMustExist = [
|
|
1581
|
+
'tools/smart-image/package.json',
|
|
1582
|
+
'tools/smart-image/pnpm-workspace.yaml',
|
|
1583
|
+
'tools/smart-image/pnpm-lock.yaml',
|
|
1584
|
+
'tools/smart-image/run.mjs',
|
|
1585
|
+
'tools/smart-image/check.mjs',
|
|
1586
|
+
'.agents/skills/smart-image-cli/SKILL.md',
|
|
1587
|
+
];
|
|
1588
|
+
for (const rel of requiredMustExist) {
|
|
1589
|
+
assert(
|
|
1590
|
+
REQUIRED_AFTER_COPY.includes(rel),
|
|
1591
|
+
`REQUIRED_AFTER_COPY must list ${rel}`,
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
for (const rel of requiredMustExist) {
|
|
1596
|
+
const full = path.join(stubRoot, ...rel.split('/'));
|
|
1597
|
+
fs.rmSync(full, { force: true });
|
|
1598
|
+
const missing = assertRequiredCopied(stubRoot);
|
|
1599
|
+
assert(missing.includes(rel), `missing must report ${rel}, got ${missing.join(',')}`);
|
|
1600
|
+
// restore for next iteration
|
|
1601
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
1602
|
+
fs.writeFileSync(full, 'x');
|
|
1603
|
+
}
|
|
1604
|
+
} finally {
|
|
1605
|
+
fs.rmSync(stubRoot, { recursive: true, force: true });
|
|
1606
|
+
}
|
|
1607
|
+
} finally {
|
|
1608
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
1609
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1108
1613
|
const skipE2E = process.env.SKIP_CLI_E2E === '1';
|
|
1109
1614
|
if (skipE2E) {
|
|
1110
1615
|
console.log('\n ↷ SKIP_CLI_E2E=1 — skipping temp-target full scaffold');
|
|
1111
1616
|
} else {
|
|
1617
|
+
/** @type {string[]} */
|
|
1618
|
+
const e2eSeeds = [];
|
|
1619
|
+
if (REPO_ROOT) {
|
|
1620
|
+
const seedCi = path.join(REPO_ROOT, 'CUSTOMER-IMAGES');
|
|
1621
|
+
const seedImgIaRoot = path.join(REPO_ROOT, '.img-ia');
|
|
1622
|
+
if (!fs.existsSync(seedCi)) {
|
|
1623
|
+
fs.mkdirSync(seedCi, { recursive: true });
|
|
1624
|
+
fs.writeFileSync(path.join(seedCi, 'e2e-secret.jpg'), 'NOCOPY');
|
|
1625
|
+
e2eSeeds.push(seedCi);
|
|
1626
|
+
}
|
|
1627
|
+
if (!fs.existsSync(seedImgIaRoot)) {
|
|
1628
|
+
fs.mkdirSync(path.join(seedImgIaRoot, '_out'), { recursive: true });
|
|
1629
|
+
fs.writeFileSync(path.join(seedImgIaRoot, '_out', 'e2e-candidate.png'), 'NOCOPY');
|
|
1630
|
+
e2eSeeds.push(seedImgIaRoot);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
try {
|
|
1112
1634
|
await test('temp-target --yes scaffold install/validate/build/git', () => {
|
|
1113
1635
|
assert(REPO_ROOT, 'expected local template root for E2E');
|
|
1114
1636
|
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-e2e-'));
|
|
@@ -1138,6 +1660,21 @@ async function main() {
|
|
|
1138
1660
|
!fs.existsSync(path.join(targetDir, 'packages')),
|
|
1139
1661
|
'packages/ must not be copied',
|
|
1140
1662
|
);
|
|
1663
|
+
assert(!fs.existsSync(path.join(targetDir, 'CUSTOMER-IMAGES')), 'CUSTOMER-IMAGES excluded');
|
|
1664
|
+
assert(!fs.existsSync(path.join(targetDir, '.img-ia')), '.img-ia excluded');
|
|
1665
|
+
assert(fs.existsSync(path.join(targetDir, 'tools', 'smart-image', 'package.json')), 'capsule manifest');
|
|
1666
|
+
assert(fs.existsSync(path.join(targetDir, 'tools', 'smart-image', 'pnpm-workspace.yaml')), 'capsule workspace');
|
|
1667
|
+
assert(fs.existsSync(path.join(targetDir, 'tools', 'smart-image', 'pnpm-lock.yaml')), 'capsule lock');
|
|
1668
|
+
assert(fs.existsSync(path.join(targetDir, 'tools', 'smart-image', 'run.mjs')), 'wrapper run.mjs');
|
|
1669
|
+
assert(fs.existsSync(path.join(targetDir, 'tools', 'smart-image', 'check.mjs')), 'wrapper check.mjs');
|
|
1670
|
+
assert(
|
|
1671
|
+
fs.existsSync(path.join(targetDir, '.agents', 'skills', 'smart-image-cli', 'SKILL.md')),
|
|
1672
|
+
'skill retained',
|
|
1673
|
+
);
|
|
1674
|
+
const tgtPkg = JSON.parse(fs.readFileSync(path.join(targetDir, 'package.json'), 'utf8'));
|
|
1675
|
+
for (const k of ['images:check', 'images:setup', 'images:run']) {
|
|
1676
|
+
assert(typeof tgtPkg.scripts?.[k] === 'string', `scaffold scripts must include ${k}`);
|
|
1677
|
+
}
|
|
1141
1678
|
|
|
1142
1679
|
const business = JSON.parse(
|
|
1143
1680
|
fs.readFileSync(path.join(targetDir, 'src/data/business.json'), 'utf8'),
|
|
@@ -1198,6 +1735,15 @@ async function main() {
|
|
|
1198
1735
|
console.warn(` (could not fully remove ${targetDir})`);
|
|
1199
1736
|
}
|
|
1200
1737
|
});
|
|
1738
|
+
} finally {
|
|
1739
|
+
for (const p of e2eSeeds) {
|
|
1740
|
+
try {
|
|
1741
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
1742
|
+
} catch {
|
|
1743
|
+
// ignore seed cleanup failures
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1201
1747
|
}
|
|
1202
1748
|
|
|
1203
1749
|
console.log(`\n${passed} passed, ${failed} failed`);
|
package/src/copy-template.mjs
CHANGED
|
@@ -17,6 +17,9 @@ const DENY_DIR_NAMES = new Set([
|
|
|
17
17
|
// CLI package + internal agent tooling — not part of a client site
|
|
18
18
|
'packages',
|
|
19
19
|
'.atl',
|
|
20
|
+
// Image-tooling runtime state only (exact basename; never bare _out or tools)
|
|
21
|
+
'CUSTOMER-IMAGES',
|
|
22
|
+
'.img-ia',
|
|
20
23
|
]);
|
|
21
24
|
|
|
22
25
|
const DENY_FILE_NAMES = new Set(['package-lock.json']);
|
|
@@ -27,7 +30,7 @@ export const DEFAULT_TEMPLATE_REPO =
|
|
|
27
30
|
'https://github.com/glacayo/website-multipages.git';
|
|
28
31
|
|
|
29
32
|
export const DEFAULT_TEMPLATE_REF =
|
|
30
|
-
process.env.CREATE_CONTRACTOR_TEMPLATE_REF || 'v2.
|
|
33
|
+
process.env.CREATE_CONTRACTOR_TEMPLATE_REF || 'v2.3.0';
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
36
|
* @param {string} name basename
|
|
@@ -267,6 +270,13 @@ export const REQUIRED_AFTER_COPY = [
|
|
|
267
270
|
'scripts/enforce-package-manager.cjs',
|
|
268
271
|
'src/assets/images',
|
|
269
272
|
'src/data/business.json',
|
|
273
|
+
// Smart-image capsule + agent skill (must scaffold with every client site)
|
|
274
|
+
'tools/smart-image/package.json',
|
|
275
|
+
'tools/smart-image/pnpm-workspace.yaml',
|
|
276
|
+
'tools/smart-image/pnpm-lock.yaml',
|
|
277
|
+
'tools/smart-image/run.mjs',
|
|
278
|
+
'tools/smart-image/check.mjs',
|
|
279
|
+
'.agents/skills/smart-image-cli/SKILL.md',
|
|
270
280
|
];
|
|
271
281
|
|
|
272
282
|
/**
|