create-ailk 0.19.1 → 0.20.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 +64 -7
- package/dist/cli.js +35 -0
- package/dist/copy.d.ts +41 -0
- package/dist/copy.js +87 -21
- package/dist/lib/fetch-template-snapshot.d.ts +21 -0
- package/dist/lib/fetch-template-snapshot.js +205 -0
- package/dist/lib/merge-three-way.d.ts +31 -0
- package/dist/lib/merge-three-way.js +65 -0
- package/dist/module-architecture.js +55 -2
- package/dist/programmatic.d.ts +7 -1
- package/dist/programmatic.js +32 -2
- package/dist/spoke-checkout.d.ts +7 -0
- package/dist/spoke-checkout.js +8 -1
- package/dist/surfaces.js +67 -2
- package/dist/template-record.d.ts +59 -0
- package/dist/template-record.js +183 -0
- package/dist/update.d.ts +48 -0
- package/dist/update.js +504 -0
- package/package.json +3 -3
- package/templates/.claude/cloudparity-repo-hooks.json +9 -0
- package/templates/.claude/hooks/architecture-check.sh +8 -0
- package/templates/.claude/rules/loop-contract.md +129 -0
- package/templates/.claude/settings.json +3 -2
- package/templates/.env.example +13 -0
- package/templates/CLAUDE.md +1 -1
- package/templates/CONVENTIONS.md +3 -2
- package/templates/_gitignore +162 -0
- package/templates/apps/api/CLAUDE.md +14 -10
- package/templates/apps/api/package.json +2 -1
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +22 -2
- package/templates/apps/api/src/__tests__/setup.ts +7 -0
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
- package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
- package/templates/apps/api/src/lib/proposal-paid-store.ts +49 -0
- package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
- package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +5 -2
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +21 -14
- package/templates/apps/api/src/middleware/auth.ts +8 -4
- package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +290 -82
- package/templates/apps/api/src/routes/webhooks/README.md +33 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +369 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +184 -6
- package/templates/apps/api/src/server.ts +14 -2
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
- package/templates/apps/api/src/services/audit-log.ts +72 -10
- package/templates/apps/api/src/services/default-tenant.ts +12 -3
- package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
- package/templates/apps/api/src/services/public-site-resolver.ts +21 -2
- package/templates/apps/api/vercel.json +1 -1
- package/templates/apps/mcp/CLAUDE.md +19 -7
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +95 -8
- package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
- package/templates/apps/mcp/src/tools/proposals.ts +27 -0
- package/templates/apps/mcp/src/workspace-server.ts +16 -0
- package/templates/apps/web/_gitignore +6 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +281 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +28 -83
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +27 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +2 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +102 -18
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +3 -2
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +11 -2
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +5 -24
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +64 -18
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +5 -3
- package/templates/apps/web/app/[locale]/consent-banner.tsx +32 -0
- package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +13 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
- package/templates/apps/web/app/[locale]/layout.tsx +23 -2
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
- package/templates/apps/web/app/robots.txt/route.ts +13 -10
- package/templates/apps/web/jest.config.cjs +3 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +96 -7
- package/templates/apps/web/lib/account-config.ts +31 -16
- package/templates/apps/web/lib/consent-visitor.ts +103 -0
- package/templates/apps/web/lib/relay.ts +20 -0
- package/templates/apps/web/messages/en.json +67 -4
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/database/CHANGELOG.md +39 -0
- package/templates/database/__tests__/outbound-message-action.db.test.ts +80 -0
- package/templates/database/__tests__/person-notes.db.test.ts +249 -0
- package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
- package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
- package/templates/database/_gitignore +8 -0
- package/templates/database/inbox/schema.prisma +38 -12
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +77 -0
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
- package/templates/database/ops/schema.prisma +16 -0
- package/templates/database/package.json +1 -1
- package/templates/project.yaml +6 -3
- package/versions.json +35 -35
- package/templates/.claude/rules/preserve-existing-style.md +0 -51
package/README.md
CHANGED
|
@@ -262,6 +262,62 @@ the route-surface registry in the same file.
|
|
|
262
262
|
`ROUTE_SINCE` entry — `__tests__/sync-routes.test.ts` fails otherwise (and it
|
|
263
263
|
fails on a stale entry for a route the template no longer ships).
|
|
264
264
|
|
|
265
|
+
### Update the copied app code
|
|
266
|
+
|
|
267
|
+
A fresh scaffold records every `apps/web`, `apps/api` and `apps/mcp` file the
|
|
268
|
+
base template copy wrote — and no later layer (a module bundle's patch, a
|
|
269
|
+
generator) rewrote — in `.ailk-template-files.json`, beside
|
|
270
|
+
`.ailk-site-manifest.json`: each file's content hash and the create-ailk
|
|
271
|
+
version it came from. `update` reads that record to bring a template fix
|
|
272
|
+
into a site that already exists, instead of a per-site porting issue:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
create-ailk update ./my-site
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
For each recorded file, `update` compares the site's current bytes to the
|
|
279
|
+
recorded hash:
|
|
280
|
+
|
|
281
|
+
- **Untouched** (the bytes still match the record) and the running
|
|
282
|
+
version's copy differs: the file is overwritten, and the record's hash
|
|
283
|
+
and version move forward. Reported as **Updated**.
|
|
284
|
+
- **Untouched** and the running version's copy is gone: the file is
|
|
285
|
+
deleted and dropped from the record. Reported as **Removed**.
|
|
286
|
+
- **Edited** (the bytes differ from the record) and the running version's
|
|
287
|
+
copy also changed: the two changes are three-way merged with `git
|
|
288
|
+
merge-file` against the recorded version's own copy as the base. A clean
|
|
289
|
+
merge keeps your edit AND the fix, and is reported as **Merged**. A
|
|
290
|
+
conflict leaves the file exactly as it was — never written with conflict
|
|
291
|
+
markers — and is reported under **Needs review** with both versions
|
|
292
|
+
named; nothing else in the run is held back by it.
|
|
293
|
+
- A recordable file the running version adds that the site lacks is
|
|
294
|
+
**Added**, unless it sits under a route or directory the site deliberately
|
|
295
|
+
does not carry (a gated-off surface, a spoke exclusion, or a directory you
|
|
296
|
+
deleted entirely) — those are left alone.
|
|
297
|
+
|
|
298
|
+
A file outside the gated-off surfaces and spoke exclusions the site's scope
|
|
299
|
+
no longer covers is **Skipped**, as is one that exists on disk but was never
|
|
300
|
+
recorded (a module bundle's own file, say) when its bytes differ from the
|
|
301
|
+
running copy. Nothing the base scaffold did not write is ever touched. No
|
|
302
|
+
template change at all means `Already up to date` and zero files written.
|
|
303
|
+
|
|
304
|
+
**A site scaffolded before this record existed** gets one built on its first
|
|
305
|
+
`update` run, from the `patternVersion` `.ailk-site-manifest.json` already
|
|
306
|
+
records — no separate command, no per-site setup. If that version predates
|
|
307
|
+
the site's actual files (an adopted site, or one whose `patternVersion` was
|
|
308
|
+
bumped by hand per "Keeping a spoke current" below), a file `update` cannot
|
|
309
|
+
tell was edited may show up under Needs review even though you never touched
|
|
310
|
+
it; the merge itself still never loses content.
|
|
311
|
+
|
|
312
|
+
**Requirements.** `git` must be on `PATH` — the merge runs `git merge-file`,
|
|
313
|
+
never a shell. Fetching an old template version (when it is not the running
|
|
314
|
+
package's own) reads the public npm registry at
|
|
315
|
+
`https://registry.npmjs.org`, overridable with `AILK_NPM_REGISTRY_URL` for a
|
|
316
|
+
mirror. `update` never touches content, env files, `.claude/`, lockfiles or
|
|
317
|
+
any `package.json` — those stay `add-module`'s and the consumer version
|
|
318
|
+
bump's job. Commit before running `update`, and review the diff after: a
|
|
319
|
+
clean merge is still worth reading.
|
|
320
|
+
|
|
265
321
|
### Adopting an existing site
|
|
266
322
|
|
|
267
323
|
A site that create-ailk did not scaffold, or that was scaffolded before
|
|
@@ -311,13 +367,14 @@ included). A second run is refused with exit 2 and the manifest untouched.
|
|
|
311
367
|
|
|
312
368
|
### Keeping a spoke current
|
|
313
369
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
370
|
+
`create-ailk update` (above) brings the copied `apps/web`, `apps/api` and
|
|
371
|
+
`apps/mcp` code forward on its own. What it leaves alone still needs a
|
|
372
|
+
per-version review: the generated seams, the declared-env check
|
|
373
|
+
(`tools/declared-env.ts`, `tools/check-declared-env.ts`) and the one-line
|
|
374
|
+
route re-exports are regenerated, not recorded, so `update` never touches
|
|
375
|
+
them; everything else is content, or code that arrives through a
|
|
376
|
+
`@working-theory/*` package bump. Moving a spoke to a new create-ailk
|
|
377
|
+
version still means:
|
|
321
378
|
|
|
322
379
|
1. **Read the changesets.** A create-ailk changeset that changes what
|
|
323
380
|
create-ailk writes into a scaffold-written spoke file names each such file
|
package/dist/cli.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* to a linked Vercel project and redeploy, exit (#6683 D9)
|
|
20
20
|
* on an `apply-design` subcommand: rewrite an existing site's look,
|
|
21
21
|
* print the changed-file report, exit (#6819)
|
|
22
|
+
* on an `update` subcommand: bring template fixes into a site's
|
|
23
|
+
* already-copied apps/web, apps/api and apps/mcp code, print the
|
|
24
|
+
* per-file report, exit (2 on a refusal it names) (#8522)
|
|
22
25
|
* 1. parse argv → CliOptions
|
|
23
26
|
* 2. on --help: print helpText, exit 0
|
|
24
27
|
* 3. on --version: print version, exit 0
|
|
@@ -58,6 +61,7 @@ import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCata
|
|
|
58
61
|
import { SEED_FLOWS_HELP, SeedFlowsRefusedError, parseSeedFlowsArgs, seedFlows, } from "./seed-flows.js";
|
|
59
62
|
import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
|
|
60
63
|
import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
|
|
64
|
+
import { UPDATE_HELP, UpdateRefusedError, formatUpdateReport, parseUpdateArgs, updateSite, } from "./update.js";
|
|
61
65
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
62
66
|
const PKG_JSON = resolve(PKG_ROOT, "package.json");
|
|
63
67
|
function readPackageVersion() {
|
|
@@ -272,6 +276,35 @@ async function main() {
|
|
|
272
276
|
process.stdout.write(formatApplyDesignReport(result));
|
|
273
277
|
process.exit(0);
|
|
274
278
|
}
|
|
279
|
+
// `update [<target-dir>]` (#8522): bring template fixes into a site's
|
|
280
|
+
// already-copied apps/web, apps/api and apps/mcp code. A refusal it names
|
|
281
|
+
// (no record and no patternVersion, an unparseable record, a downgrade, an
|
|
282
|
+
// unreadable architecture.yaml or manifest topology, a backfill whose
|
|
283
|
+
// fetch fails) exits 2 before anything is written. A clean run exits 0
|
|
284
|
+
// even when it lists files under "Needs review" — the review list is the
|
|
285
|
+
// hand-off, not a failure.
|
|
286
|
+
if (argv[0] === "update") {
|
|
287
|
+
const args = parseUpdateArgs(argv.slice(1));
|
|
288
|
+
if (args.help) {
|
|
289
|
+
process.stdout.write(UPDATE_HELP);
|
|
290
|
+
process.exit(0);
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const result = await updateSite({
|
|
294
|
+
targetDir: args.target ?? process.cwd(),
|
|
295
|
+
templatesDir: resolve(PKG_ROOT, "templates"),
|
|
296
|
+
version: readPackageVersion(),
|
|
297
|
+
});
|
|
298
|
+
process.stdout.write(formatUpdateReport(result));
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
if (!(err instanceof UpdateRefusedError))
|
|
302
|
+
throw err;
|
|
303
|
+
process.stderr.write(`[create-ailk] ${err.message}\n`);
|
|
304
|
+
process.exit(err.exitCode);
|
|
305
|
+
}
|
|
306
|
+
process.exit(0);
|
|
307
|
+
}
|
|
275
308
|
// No silent default (#6754 D2/D3): a scaffold run naming neither --shape
|
|
276
309
|
// nor --template asks for a shape on a terminal and is refused without one.
|
|
277
310
|
// A --design or --modules outside the release scope is refused before that
|
|
@@ -317,6 +350,8 @@ async function main() {
|
|
|
317
350
|
"\n" +
|
|
318
351
|
APPLY_DESIGN_HELP +
|
|
319
352
|
"\n" +
|
|
353
|
+
UPDATE_HELP +
|
|
354
|
+
"\n" +
|
|
320
355
|
LICENSE_SET_HELP);
|
|
321
356
|
process.exit(0);
|
|
322
357
|
}
|
package/dist/copy.d.ts
CHANGED
|
@@ -19,11 +19,52 @@ export interface CopyTemplateOptions {
|
|
|
19
19
|
* `.../services` never excludes a sibling `.../services-extra`.
|
|
20
20
|
*/
|
|
21
21
|
excludePaths?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Called once per file actually written, with its `/`-separated
|
|
24
|
+
* destination path (relative to `targetDir`) and the exact bytes written
|
|
25
|
+
* (#8522 D2) — `create-ailk update`'s per-file record is built from these
|
|
26
|
+
* calls. No existing caller passes this, so their behaviour is unchanged.
|
|
27
|
+
*/
|
|
28
|
+
onWrite?: (relPath: string, bytes: Uint8Array) => void;
|
|
22
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether a walked template-relative path is inside (or equal to) an excluded
|
|
32
|
+
* directory. Compares `/`-normalized paths so the check holds on Windows, and
|
|
33
|
+
* only at a directory boundary — a prefix that stops mid-segment is no match.
|
|
34
|
+
* Exported (#8522 D3) so `create-ailk update` applies the same surface-gating
|
|
35
|
+
* boundary to a candidate new file.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isExcluded(relativePath: string, excluded: readonly string[]): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The pack-safe name `build-templates.mjs` ships each snapshot `.gitignore`
|
|
40
|
+
* under (#7366 D1) — npm and pnpm strip a file literally named `.gitignore`
|
|
41
|
+
* from a packed tarball, but keep one named `_gitignore`. One definition,
|
|
42
|
+
* imported by `build-templates.mjs` (from the compiled `dist/copy.js`) and
|
|
43
|
+
* by `copyTemplate` below, so the two can never spell it differently.
|
|
44
|
+
*/
|
|
45
|
+
export declare const PACKED_GITIGNORE_NAME = "_gitignore";
|
|
23
46
|
/**
|
|
24
47
|
* Whether a scaffolded path's content is text we run markers over. Exported
|
|
25
48
|
* because the template/base layer (`installBundleTree`) substitutes with the
|
|
26
49
|
* same classifier — one definition, not two (#6460).
|
|
27
50
|
*/
|
|
28
51
|
export declare function isTextFile(relativePath: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The destination path a template-relative path (`/`-separated) copies to —
|
|
54
|
+
* `undefined` for a packaging-only `.gitkeep` or `.npmignore` (never a
|
|
55
|
+
* scaffold file), and a final `PACKED_GITIGNORE_NAME` restored to
|
|
56
|
+
* `.gitignore` in the same directory otherwise (#7366 D3) — so a scaffold
|
|
57
|
+
* never carries the packaging-only name. Exported (#8522 D3) as the one
|
|
58
|
+
* renderer of "the template's copy", shared by `copyTemplate` and by
|
|
59
|
+
* `create-ailk update`'s render of a running or an old template version.
|
|
60
|
+
*/
|
|
61
|
+
export declare function templateDestination(rel: string): string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* The bytes a template-relative file copies as, at its destination path
|
|
64
|
+
* (#8522 D3): workspace versions plus markers for `package.json`, markers
|
|
65
|
+
* for `isTextFile`, raw bytes otherwise. Exported so `copyTemplate`'s walk
|
|
66
|
+
* and `create-ailk update`'s render of a running or an old template version
|
|
67
|
+
* share the one renderer — nothing about the copy itself changes.
|
|
68
|
+
*/
|
|
69
|
+
export declare function renderTemplateFile(bytes: Uint8Array, destRel: string, versions: WorkspaceVersionMap, markers: MarkerMap): Uint8Array;
|
|
29
70
|
export declare function copyTemplate(opts: CopyTemplateOptions): void;
|
package/dist/copy.js
CHANGED
|
@@ -17,8 +17,23 @@
|
|
|
17
17
|
* alike. Symlinks are skipped outright (never followed, never copied) —
|
|
18
18
|
* the snapshotted templates/ tree is built without symlinks; any symlink
|
|
19
19
|
* present at scaffold time is treated as a packaging accident and ignored.
|
|
20
|
+
*
|
|
21
|
+
* Packed ignore files (#7366):
|
|
22
|
+
* - npm and pnpm strip every file literally named `.gitignore` from a
|
|
23
|
+
* packed tarball, so `build-templates.mjs` ships each snapshot
|
|
24
|
+
* `.gitignore` under `PACKED_GITIGNORE_NAME` (`_gitignore`) instead. A
|
|
25
|
+
* file named `PACKED_GITIGNORE_NAME` is restored to `.gitignore` in the
|
|
26
|
+
* same directory on the way into a scaffold, classified and
|
|
27
|
+
* marker-substituted exactly as a `.gitignore` is today (its destination
|
|
28
|
+
* relative path, not its source name, drives `isTextFile`). A directory
|
|
29
|
+
* carrying both `.gitignore` and `PACKED_GITIGNORE_NAME` is refused
|
|
30
|
+
* outright, before either is written, rather than resolved by
|
|
31
|
+
* `readdir()` order. `.npmignore` — the packaging-only sibling
|
|
32
|
+
* `build-templates.mjs` writes beside each `PACKED_GITIGNORE_NAME` to
|
|
33
|
+
* keep pack-time exclusions working — is skipped like `.gitkeep`: it
|
|
34
|
+
* never belongs in a scaffold.
|
|
20
35
|
*/
|
|
21
|
-
import {
|
|
36
|
+
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
22
37
|
import { extname, join, relative, resolve, sep } from "node:path";
|
|
23
38
|
import pkg from "../package.json" with { type: "json" };
|
|
24
39
|
import { substituteMarkers, substituteWorkspaceVersions, } from "./substitute.js";
|
|
@@ -57,8 +72,10 @@ export function buildScaffoldMarkers(projectName) {
|
|
|
57
72
|
* Whether a walked template-relative path is inside (or equal to) an excluded
|
|
58
73
|
* directory. Compares `/`-normalized paths so the check holds on Windows, and
|
|
59
74
|
* only at a directory boundary — a prefix that stops mid-segment is no match.
|
|
75
|
+
* Exported (#8522 D3) so `create-ailk update` applies the same surface-gating
|
|
76
|
+
* boundary to a candidate new file.
|
|
60
77
|
*/
|
|
61
|
-
function isExcluded(relativePath, excluded) {
|
|
78
|
+
export function isExcluded(relativePath, excluded) {
|
|
62
79
|
if (excluded.length === 0)
|
|
63
80
|
return false;
|
|
64
81
|
const rel = relativePath.split(sep).join("/");
|
|
@@ -71,6 +88,14 @@ function isExcluded(relativePath, excluded) {
|
|
|
71
88
|
}
|
|
72
89
|
return false;
|
|
73
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The pack-safe name `build-templates.mjs` ships each snapshot `.gitignore`
|
|
93
|
+
* under (#7366 D1) — npm and pnpm strip a file literally named `.gitignore`
|
|
94
|
+
* from a packed tarball, but keep one named `_gitignore`. One definition,
|
|
95
|
+
* imported by `build-templates.mjs` (from the compiled `dist/copy.js`) and
|
|
96
|
+
* by `copyTemplate` below, so the two can never spell it differently.
|
|
97
|
+
*/
|
|
98
|
+
export const PACKED_GITIGNORE_NAME = "_gitignore";
|
|
74
99
|
/** File extensions whose content we treat as text (and run markers over). */
|
|
75
100
|
const TEXT_EXTS = new Set([
|
|
76
101
|
".md",
|
|
@@ -113,6 +138,46 @@ function isPackageJson(relativePath) {
|
|
|
113
138
|
const base = relativePath.split(/[\\/]/).pop() ?? "";
|
|
114
139
|
return base === "package.json";
|
|
115
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The destination path a template-relative path (`/`-separated) copies to —
|
|
143
|
+
* `undefined` for a packaging-only `.gitkeep` or `.npmignore` (never a
|
|
144
|
+
* scaffold file), and a final `PACKED_GITIGNORE_NAME` restored to
|
|
145
|
+
* `.gitignore` in the same directory otherwise (#7366 D3) — so a scaffold
|
|
146
|
+
* never carries the packaging-only name. Exported (#8522 D3) as the one
|
|
147
|
+
* renderer of "the template's copy", shared by `copyTemplate` and by
|
|
148
|
+
* `create-ailk update`'s render of a running or an old template version.
|
|
149
|
+
*/
|
|
150
|
+
export function templateDestination(rel) {
|
|
151
|
+
const parts = rel.split("/");
|
|
152
|
+
const last = parts.length - 1;
|
|
153
|
+
if (last < 0)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (parts[last] === ".gitkeep" || parts[last] === ".npmignore") {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
if (parts[last] === PACKED_GITIGNORE_NAME) {
|
|
159
|
+
parts[last] = ".gitignore";
|
|
160
|
+
}
|
|
161
|
+
return parts.join("/");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The bytes a template-relative file copies as, at its destination path
|
|
165
|
+
* (#8522 D3): workspace versions plus markers for `package.json`, markers
|
|
166
|
+
* for `isTextFile`, raw bytes otherwise. Exported so `copyTemplate`'s walk
|
|
167
|
+
* and `create-ailk update`'s render of a running or an old template version
|
|
168
|
+
* share the one renderer — nothing about the copy itself changes.
|
|
169
|
+
*/
|
|
170
|
+
export function renderTemplateFile(bytes, destRel, versions, markers) {
|
|
171
|
+
if (isPackageJson(destRel)) {
|
|
172
|
+
const text = Buffer.from(bytes).toString("utf8");
|
|
173
|
+
return Buffer.from(substituteMarkers(substituteWorkspaceVersions(text, versions), markers), "utf8");
|
|
174
|
+
}
|
|
175
|
+
if (isTextFile(destRel)) {
|
|
176
|
+
const text = Buffer.from(bytes).toString("utf8");
|
|
177
|
+
return Buffer.from(substituteMarkers(text, markers), "utf8");
|
|
178
|
+
}
|
|
179
|
+
return Buffer.from(bytes);
|
|
180
|
+
}
|
|
116
181
|
export function copyTemplate(opts) {
|
|
117
182
|
const templatesAbs = resolve(opts.templatesDir);
|
|
118
183
|
const targetAbs = resolve(opts.targetDir);
|
|
@@ -124,19 +189,30 @@ export function copyTemplate(opts) {
|
|
|
124
189
|
if (dir === undefined)
|
|
125
190
|
break;
|
|
126
191
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
192
|
+
// A directory carrying both the live name and the packed name would
|
|
193
|
+
// otherwise resolve by `readdir()` order — refuse outright, before
|
|
194
|
+
// writing either (#7366 D3).
|
|
195
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
196
|
+
if (names.has(".gitignore") && names.has(PACKED_GITIGNORE_NAME)) {
|
|
197
|
+
throw new Error(`templates directory carries both .gitignore and ${PACKED_GITIGNORE_NAME}: ${dir}`);
|
|
198
|
+
}
|
|
127
199
|
for (const entry of entries) {
|
|
128
|
-
// .gitkeep is a packaging artifact for the empty-templates state in git;
|
|
129
|
-
// never propagate it to scaffolder output.
|
|
130
|
-
if (entry.name === ".gitkeep")
|
|
131
|
-
continue;
|
|
132
200
|
const srcPath = join(dir, entry.name);
|
|
133
|
-
const rel = relative(templatesAbs, srcPath);
|
|
201
|
+
const rel = relative(templatesAbs, srcPath).split(sep).join("/");
|
|
134
202
|
// Surface gating (#4154): a route directory belonging to a surface the
|
|
135
203
|
// declared architecture does not carry is skipped outright — the route is
|
|
136
204
|
// ABSENT from the scaffold, not present-and-serving-a-placeholder.
|
|
137
205
|
if (isExcluded(rel, excluded))
|
|
138
206
|
continue;
|
|
139
|
-
|
|
207
|
+
// The destination path: `undefined` drops a packaging-only .gitkeep /
|
|
208
|
+
// .npmignore (#7366 D3); otherwise a packed .gitignore's name is
|
|
209
|
+
// restored. Everything below — the path-safety check, text
|
|
210
|
+
// classification, and marker substitution — runs on THIS path, never
|
|
211
|
+
// the source name.
|
|
212
|
+
const destRel = templateDestination(rel);
|
|
213
|
+
if (destRel === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
const dstPath = join(targetAbs, destRel);
|
|
140
216
|
// Path-safety guard: dstPath MUST be within targetAbs. Use `path.sep`
|
|
141
217
|
// so the check holds on Windows (`\\`) as well as POSIX (`/`).
|
|
142
218
|
const resolvedDst = resolve(dstPath);
|
|
@@ -152,19 +228,9 @@ export function copyTemplate(opts) {
|
|
|
152
228
|
stack.push(srcPath);
|
|
153
229
|
continue;
|
|
154
230
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
writeFileSync(dstPath, transformed);
|
|
159
|
-
}
|
|
160
|
-
else if (isTextFile(rel)) {
|
|
161
|
-
const text = readFileSync(srcPath, "utf8");
|
|
162
|
-
writeFileSync(dstPath, substituteMarkers(text, opts.markers));
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
// Binary or unknown — copy verbatim.
|
|
166
|
-
copyFileSync(srcPath, dstPath);
|
|
167
|
-
}
|
|
231
|
+
const rendered = renderTemplateFile(readFileSync(srcPath), destRel, opts.versions, opts.markers);
|
|
232
|
+
writeFileSync(dstPath, rendered);
|
|
233
|
+
opts.onWrite?.(destRel, rendered);
|
|
168
234
|
}
|
|
169
235
|
}
|
|
170
236
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const DEFAULT_AILK_NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2
|
+
export declare class FetchTemplateSnapshotError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export interface FetchTemplateSnapshotOptions {
|
|
6
|
+
/** The create-ailk version to re-materialise, e.g. "0.19.1". */
|
|
7
|
+
version: string;
|
|
8
|
+
/** Registry root; defaults to `AILK_NPM_REGISTRY_URL`, else the public npm registry. */
|
|
9
|
+
registryUrl?: string;
|
|
10
|
+
/** Injected fetch for tests. Never a live network call in CI. */
|
|
11
|
+
fetchImpl?: typeof fetch;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Fetch and re-materialise create-ailk `version`'s `templates/apps/{web,api,mcp}/`
|
|
15
|
+
* tree from its published npm tarball.
|
|
16
|
+
*
|
|
17
|
+
* @throws FetchTemplateSnapshotError on an invalid version, an unreachable
|
|
18
|
+
* or malformed registry response, an integrity mismatch, or a symlink /
|
|
19
|
+
* hard-link entry under one of the three app prefixes.
|
|
20
|
+
*/
|
|
21
|
+
export declare function fetchTemplateSnapshot(options: FetchTemplateSnapshotOptions): Promise<Map<string, Uint8Array>>;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-materialise an old create-ailk's `templates/apps/{web,api,mcp}/` tree
|
|
3
|
+
* from its published npm tarball (#8522 D6) — the merge base
|
|
4
|
+
* `create-ailk update` needs for a recorded version no longer sitting in
|
|
5
|
+
* this package's own `templates/`.
|
|
6
|
+
*
|
|
7
|
+
* Reads the public, unauthenticated npm registry — no bearer token, unlike
|
|
8
|
+
* `fetch-template.ts` / `fetch-module.ts` — through the same safety guards
|
|
9
|
+
* those two share (`fetch-guard.ts`): an `https:` (or loopback, for the test
|
|
10
|
+
* mock) registry URL, a request timeout, and a declared-`Content-Length`
|
|
11
|
+
* cap before the body is ever buffered. The tarball's integrity is verified
|
|
12
|
+
* against the packument's `dist.integrity` before it is gunzipped, and the
|
|
13
|
+
* gunzip itself is capped at the same `MAX_TOTAL_SIZE_BYTES`
|
|
14
|
+
* `extract-module-bundle.ts` enforces for a module bundle, so the whole
|
|
15
|
+
* fetch is bounded in memory regardless of what the tarball's own headers
|
|
16
|
+
* claim. The gunzipped tar is parsed in memory with `tar`'s own `Parser` —
|
|
17
|
+
* never extracted to disk — and only regular files under the three app
|
|
18
|
+
* prefixes are kept.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { gunzipSync } from "node:zlib";
|
|
22
|
+
import * as tar from "tar";
|
|
23
|
+
import { templateDestination } from "../copy.js";
|
|
24
|
+
import { MAX_TOTAL_SIZE_BYTES } from "./extract-module-bundle.js";
|
|
25
|
+
import { assertContentLengthWithinLimit, assertHttpsRegistryUrl, fetchTimeoutSignal, } from "./fetch-guard.js";
|
|
26
|
+
export const DEFAULT_AILK_NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
27
|
+
/** The same version shape `.ailk-template-files.json` rows are checked against (D1). */
|
|
28
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
29
|
+
/** The one tar path prefix a snapshot is read from, and the three app roots inside it. */
|
|
30
|
+
const TAR_ROOT = "package/templates/";
|
|
31
|
+
const APP_PREFIXES = ["apps/web/", "apps/api/", "apps/mcp/"];
|
|
32
|
+
export class FetchTemplateSnapshotError extends Error {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "FetchTemplateSnapshotError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function detail(err) {
|
|
39
|
+
return err instanceof Error ? err.message : String(err);
|
|
40
|
+
}
|
|
41
|
+
/** The one version's packument: `GET <registry>/create-ailk/<version>`. */
|
|
42
|
+
async function fetchPackument(registryUrl, version, fetchImpl) {
|
|
43
|
+
const url = `${registryUrl.replace(/\/+$/, "")}/create-ailk/${encodeURIComponent(version)}`;
|
|
44
|
+
let response;
|
|
45
|
+
try {
|
|
46
|
+
response = await fetchImpl(url, {
|
|
47
|
+
method: "GET",
|
|
48
|
+
headers: { Accept: "application/json" },
|
|
49
|
+
redirect: "error",
|
|
50
|
+
signal: fetchTimeoutSignal(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} packument unreachable: ${detail(err)}`);
|
|
55
|
+
}
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} packument request failed: ${response.status}`);
|
|
58
|
+
}
|
|
59
|
+
let doc;
|
|
60
|
+
try {
|
|
61
|
+
doc = await response.json();
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} packument is not JSON: ${detail(err)}`);
|
|
65
|
+
}
|
|
66
|
+
if (typeof doc !== "object" || doc === null) {
|
|
67
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} packument is not an object`);
|
|
68
|
+
}
|
|
69
|
+
const record = doc;
|
|
70
|
+
if (record["version"] !== version) {
|
|
71
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} packument names a different version: ${JSON.stringify(record["version"])}`);
|
|
72
|
+
}
|
|
73
|
+
const dist = record["dist"];
|
|
74
|
+
if (typeof dist !== "object" || dist === null) {
|
|
75
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} packument has no "dist"`);
|
|
76
|
+
}
|
|
77
|
+
const distRecord = dist;
|
|
78
|
+
const tarball = distRecord["tarball"];
|
|
79
|
+
if (typeof tarball !== "string" || tarball.length === 0) {
|
|
80
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} packument has no "dist.tarball"`);
|
|
81
|
+
}
|
|
82
|
+
const integrity = distRecord["integrity"];
|
|
83
|
+
if (typeof integrity !== "string" || !integrity.startsWith("sha512-")) {
|
|
84
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} packument has no "dist.integrity" starting "sha512-"`);
|
|
85
|
+
}
|
|
86
|
+
return { version, tarball, integrity };
|
|
87
|
+
}
|
|
88
|
+
async function fetchTarball(tarballUrl, integrity, version, fetchImpl) {
|
|
89
|
+
assertHttpsRegistryUrl(tarballUrl, FetchTemplateSnapshotError);
|
|
90
|
+
let response;
|
|
91
|
+
try {
|
|
92
|
+
response = await fetchImpl(tarballUrl, {
|
|
93
|
+
method: "GET",
|
|
94
|
+
headers: { Accept: "application/octet-stream" },
|
|
95
|
+
redirect: "error",
|
|
96
|
+
signal: fetchTimeoutSignal(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} tarball unreachable: ${detail(err)}`);
|
|
101
|
+
}
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} tarball request failed: ${response.status}`);
|
|
104
|
+
}
|
|
105
|
+
assertContentLengthWithinLimit(response, FetchTemplateSnapshotError);
|
|
106
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
107
|
+
const digest = `sha512-${createHash("sha512").update(buf).digest("base64")}`;
|
|
108
|
+
if (digest !== integrity) {
|
|
109
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} tarball failed its integrity check`);
|
|
110
|
+
}
|
|
111
|
+
return buf;
|
|
112
|
+
}
|
|
113
|
+
/** The app-relative path (no `package/templates/` prefix) for a tar entry path, or undefined outside the three app trees. */
|
|
114
|
+
function appRelativePath(tarPath) {
|
|
115
|
+
if (!tarPath.startsWith(TAR_ROOT))
|
|
116
|
+
return undefined;
|
|
117
|
+
const rel = tarPath.slice(TAR_ROOT.length);
|
|
118
|
+
if (!APP_PREFIXES.some((prefix) => rel.startsWith(prefix)))
|
|
119
|
+
return undefined;
|
|
120
|
+
if (rel.length === 0)
|
|
121
|
+
return undefined;
|
|
122
|
+
const segments = rel.split("/");
|
|
123
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return rel;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Parse the gunzipped tar in memory and return every regular file under the
|
|
130
|
+
* three app trees, keyed by its destination path (D3's `templateDestination`
|
|
131
|
+
* — `_gitignore` restored to `.gitignore`, `.gitkeep` / `.npmignore` dropped).
|
|
132
|
+
* A symlink or hard link under one of those prefixes refuses the WHOLE
|
|
133
|
+
* snapshot, never just that entry.
|
|
134
|
+
*/
|
|
135
|
+
function parseSnapshot(gunzipped, version) {
|
|
136
|
+
const files = new Map();
|
|
137
|
+
let refusal;
|
|
138
|
+
const parser = new tar.Parser({
|
|
139
|
+
strict: true,
|
|
140
|
+
filter: (tarPath, rawEntry) => {
|
|
141
|
+
if (refusal !== undefined)
|
|
142
|
+
return false;
|
|
143
|
+
const rel = appRelativePath(tarPath);
|
|
144
|
+
if (rel === undefined)
|
|
145
|
+
return false;
|
|
146
|
+
const entryType = rawEntry.type;
|
|
147
|
+
if (entryType === "SymbolicLink" || entryType === "Link") {
|
|
148
|
+
refusal = `create-ailk ${version} snapshot: "${tarPath}" is a ${entryType} (refused)`;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return entryType === "File";
|
|
152
|
+
},
|
|
153
|
+
onReadEntry: (entry) => {
|
|
154
|
+
const rel = appRelativePath(entry.path);
|
|
155
|
+
const dest = rel !== undefined ? templateDestination(rel) : undefined;
|
|
156
|
+
if (dest === undefined) {
|
|
157
|
+
entry.resume();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const chunks = [];
|
|
161
|
+
entry.on("data", (chunk) => chunks.push(chunk));
|
|
162
|
+
entry.on("end", () => files.set(dest, Buffer.concat(chunks)));
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
166
|
+
parser.on("error", (err) => rejectPromise(err));
|
|
167
|
+
parser.on("end", () => {
|
|
168
|
+
if (refusal !== undefined) {
|
|
169
|
+
rejectPromise(new FetchTemplateSnapshotError(refusal));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
resolvePromise(files);
|
|
173
|
+
});
|
|
174
|
+
parser.end(gunzipped);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Fetch and re-materialise create-ailk `version`'s `templates/apps/{web,api,mcp}/`
|
|
179
|
+
* tree from its published npm tarball.
|
|
180
|
+
*
|
|
181
|
+
* @throws FetchTemplateSnapshotError on an invalid version, an unreachable
|
|
182
|
+
* or malformed registry response, an integrity mismatch, or a symlink /
|
|
183
|
+
* hard-link entry under one of the three app prefixes.
|
|
184
|
+
*/
|
|
185
|
+
export async function fetchTemplateSnapshot(options) {
|
|
186
|
+
const { version } = options;
|
|
187
|
+
if (!VERSION_PATTERN.test(version)) {
|
|
188
|
+
throw new FetchTemplateSnapshotError(`refusing to fetch create-ailk "${version}": not a valid version`);
|
|
189
|
+
}
|
|
190
|
+
const registryUrl = options.registryUrl ??
|
|
191
|
+
process.env["AILK_NPM_REGISTRY_URL"] ??
|
|
192
|
+
DEFAULT_AILK_NPM_REGISTRY_URL;
|
|
193
|
+
assertHttpsRegistryUrl(registryUrl, FetchTemplateSnapshotError);
|
|
194
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
195
|
+
const packument = await fetchPackument(registryUrl, version, fetchImpl);
|
|
196
|
+
const tarball = await fetchTarball(packument.tarball, packument.integrity, version, fetchImpl);
|
|
197
|
+
let gunzipped;
|
|
198
|
+
try {
|
|
199
|
+
gunzipped = gunzipSync(tarball, { maxOutputLength: MAX_TOTAL_SIZE_BYTES });
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
throw new FetchTemplateSnapshotError(`create-ailk ${version} tarball could not be decompressed: ${detail(err)}`);
|
|
203
|
+
}
|
|
204
|
+
return parseSnapshot(gunzipped, version);
|
|
205
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface MergeThreeWayOptions {
|
|
2
|
+
/** The site's current (edited) text. */
|
|
3
|
+
ours: string;
|
|
4
|
+
/** The recorded version's rendered copy. */
|
|
5
|
+
base: string;
|
|
6
|
+
/** The running version's rendered copy. */
|
|
7
|
+
theirs: string;
|
|
8
|
+
oursLabel: string;
|
|
9
|
+
baseLabel: string;
|
|
10
|
+
theirsLabel: string;
|
|
11
|
+
}
|
|
12
|
+
export type MergeThreeWayResult = {
|
|
13
|
+
status: "merged";
|
|
14
|
+
text: string;
|
|
15
|
+
} | {
|
|
16
|
+
status: "conflict";
|
|
17
|
+
} | {
|
|
18
|
+
status: "error";
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Run `git merge-file -p` over the three texts in a `mkdtemp` directory, no
|
|
23
|
+
* shell, argv holding only temp-file paths and the three labels (#8522 D5).
|
|
24
|
+
*
|
|
25
|
+
* Exit 0 is a clean merge (`merged`). Exit 1-127 is a conflicting hunk
|
|
26
|
+
* (`conflict`) — the file is never written with conflict markers by this
|
|
27
|
+
* function; the caller decides what to do with a conflict, and nothing here
|
|
28
|
+
* ever applies one. Any other exit, or a spawn failure (no `git` on `PATH`),
|
|
29
|
+
* is `error`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function mergeThreeWay(opts: MergeThreeWayOptions): MergeThreeWayResult;
|