create-ailk 0.28.1 → 0.29.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 CHANGED
@@ -552,6 +552,128 @@ deliverables, pricing, registers, listings — so a flow that needs one is
552
552
  authored in the hub's Admin; once it carries one, the hub refuses a seed of
553
553
  that flow (`409 slug_owned`) rather than delete the block.
554
554
 
555
+ ### Seeding the workspace
556
+
557
+ On a site with an Admin (the `workspace` or `multi-tenant` module), the
558
+ starter's pages are also rows in the site's workspace, so the Admin's Docs
559
+ lists Home, About, Contact and every other roster page on first open.
560
+ `seed-workspace` writes them there: each `content/en/<Type>/<slug>.mdx` page
561
+ becomes one draft `text/page` Artifact.
562
+
563
+ ```bash
564
+ AILK_AGENT_KEY=<agent key> create-ailk seed-workspace ./my-site --api-url <api origin> --workspace <wksp_id>
565
+ ```
566
+
567
+ - The API origin is `--api-url`, else `AILK_API_URL`; it must be `https:` (or
568
+ `http:` on a loopback host). The workspace id is `--workspace`, else
569
+ `AILK_WORKSPACE_ID`. The first line printed names the workspace, the origin
570
+ and the origin's source, before any request.
571
+ - The agent key is read from `AILK_AGENT_KEY` only — never a flag or a file —
572
+ and never printed. An owner issues it in the Admin with the `content-edit`
573
+ grant.
574
+ - Each page prints `created`, `updated`, `unchanged`, `skipped` or `failed`,
575
+ then a count line. A re-run creates nothing and reports every page
576
+ `unchanged`. An agent writes ten times a minute, so a first seed of about
577
+ thirty pages waits out the rate limit a few times.
578
+ - The site's dependencies must be installed (`pnpm install`): the pages are
579
+ read through the site's own `@working-theory/content-adapters`.
580
+ - The API must run with `content-write` and a tenancy module (`workspace` or
581
+ `multi-tenant`) in `AILK_ENABLED_MODULES`. Without them the content routes
582
+ are not served and every page fails `404`.
583
+ - Exit 0 when every page was seeded, 1 when any failed, 2 on a refusal (no
584
+ `.ailk-site-manifest.json`, a spoke, a missing key, workspace id or origin,
585
+ the adapter not installed), which sends nothing.
586
+
587
+ `pnpm create ailk` runs the same seed as its last step, after `--install`,
588
+ when the site has an Admin and `AILK_API_URL`, `AILK_WORKSPACE_ID` and
589
+ `AILK_AGENT_KEY` are all set. When the site has an Admin but one is missing,
590
+ or `--install` was not passed, it prints that seeding was skipped and the
591
+ command above. A site with no Admin, or a spoke, prints nothing about it. A
592
+ seed that fails or is refused at scaffold time prints why and never fails the
593
+ scaffold; `seed-workspace` is where a seed's result is an exit code.
594
+
595
+ On a site carrying `signal-collectors`, the two `ThankYouPage` seeds
596
+ `contact` and `newsletter` share an address (locale and slug) with the
597
+ `ContactPage` and `NewsletterSignup` pages, so those two are reported
598
+ `failed` and the seed exits 1 until one side's slug changes.
599
+
600
+ To check a seed:
601
+
602
+ ```bash
603
+ cd my-site
604
+ # 1. Re-run the seed: nothing may be created, updated or fail.
605
+ AILK_AGENT_KEY=<agent key> create-ailk seed-workspace . --api-url <api origin> --workspace <wksp_id>
606
+ # expect the count line: created 0 · updated 0 · unchanged <N> · skipped <S> · failed 0
607
+ # 2. Read the workspace's pages back: at least <N> rows.
608
+ curl -sS -H "authorization: Bearer $AILK_AGENT_KEY" "<api origin>/v1/workspaces/<wksp_id>/content?locale=en" | jq '.data | length'
609
+ ```
610
+
611
+ ### Migrating an existing site's content
612
+
613
+ `migrate-content` imports the pages of a site that was not seeded at
614
+ scaffold time — a site with no `.ailk-site-manifest.json`, or a spoke — into
615
+ a workspace: each `content/en/<Type>/<slug>.mdx` page, and each legacy
616
+ `content/<Type>/<slug>.mdx` page, becomes one draft `text/page` Artifact.
617
+
618
+ ```bash
619
+ AILK_AGENT_KEY=<agent key> create-ailk migrate-content ./my-site --api-url <api origin> --workspace <wksp_id>
620
+ AILK_AGENT_KEY=<agent key> create-ailk migrate-content ./my-site --apply --api-url <api origin> --workspace <wksp_id>
621
+ ```
622
+
623
+ - It is a dry run by default and writes nothing: it reads the workspace,
624
+ prints what `--apply` would do to each page — `create`, `update`,
625
+ `unchanged`, `skipped` or `failed` — then a count line. A dry run never sends
626
+ a write: its transport answers a create or an update itself.
627
+ - `--apply` writes. Each page prints `created`, `updated`, `unchanged`,
628
+ `skipped` or `failed`, then a count line; a re-run creates nothing and
629
+ reports every page `unchanged`.
630
+ - The API origin is `--api-url`, else `AILK_API_URL`; it must be `https:` (or
631
+ `http:` on a loopback host). The workspace id is `--workspace`, else
632
+ `AILK_WORKSPACE_ID`. The agent key is read from `AILK_AGENT_KEY` only —
633
+ never a flag or a file — and never printed; it needs the `content-edit`
634
+ grant. The first line printed names the workspace, the origin and the
635
+ origin's source, before any request.
636
+ - On a spoke, the workspace is its hub's: pass the hub's API origin and the
637
+ spoke's workspace on the hub.
638
+ - Pages land as drafts and nothing is published, so the live site keeps
639
+ serving its MDX files and does not change.
640
+ - The plan cannot see a write the API would refuse at `--apply` (a conflict
641
+ with an edit made in between, or a server error): such a page is planned
642
+ `create` or `update`. A page whose content fails its page type is planned
643
+ and then saved with the errors the editor shows, not `failed`.
644
+ - The site's dependencies must be installed (`pnpm install`): the pages are
645
+ read through the site's own `@working-theory/content-adapters`.
646
+ - Exit 0 when no page failed, 1 when any did (in a dry run, when any would), 2
647
+ on a refusal (a missing key, workspace id or origin, no `content/`, the
648
+ adapter not installed), which sends nothing.
649
+
650
+ To migrate a live site and prove a visitor sees no change (samhenry.org's
651
+ sequence; the placeholders are yours to fill):
652
+
653
+ ```bash
654
+ cd <the samhenry repo> # dependencies installed (pnpm install)
655
+ export AILK_AGENT_KEY=<agent key> # content-edit, issued in the hub's Admin for the Sam Henry workspace
656
+ API=<hub api origin>; WS=<wksp_id>
657
+ # 0. Snapshot the live site before anything is written.
658
+ curl -sS https://samhenry.org/sitemap.xml | grep -o '<loc>[^<]*</loc>' | sed -e 's/<loc>//' -e 's/<\/loc>//' | sort -u > urls.txt
659
+ mkdir -p before after
660
+ n=0; while read -r u; do n=$((n+1)); curl -sS -o "before/$n.html" -w '%{http_code} %{url_effective}\n' "$u"; done < urls.txt > before/status.txt
661
+ # 1. Dry run: the plan; nothing is written. Expect failed 0 and exit 0.
662
+ npx create-ailk@<version> migrate-content . --api-url "$API" --workspace "$WS"
663
+ # 2. Apply. Expect: created <N> · updated 0 · unchanged 0 · skipped <S> · failed 0
664
+ npx create-ailk@<version> migrate-content . --apply --api-url "$API" --workspace "$WS"
665
+ # 3. Re-run: nothing changes. Expect: created 0 · updated 0 · unchanged <N> · skipped <S> · failed 0
666
+ npx create-ailk@<version> migrate-content . --apply --api-url "$API" --workspace "$WS"
667
+ # 4. Docs lists them: at least <N> rows (exactly <N> on a workspace that had no pages).
668
+ printf 'authorization: Bearer %s\n' "$AILK_AGENT_KEY" | curl -sS -H @- "$API/v1/workspaces/$WS/content?locale=en" | jq '.data | length'
669
+ # 5. The live diff: re-fetch the same URLs and compare. Expect no output from diff.
670
+ n=0; while read -r u; do n=$((n+1)); curl -sS -o "after/$n.html" -w '%{http_code} %{url_effective}\n' "$u"; done < urls.txt > after/status.txt
671
+ diff -r before after && echo "live site unchanged"
672
+ ```
673
+
674
+ A non-empty diff is a finding: stop and report it before migrating another
675
+ site.
676
+
555
677
  ### A spoke's declared environment
556
678
 
557
679
  A spoke scaffold's `pnpm preflight` and CI run `pnpm env:declared`: every
@@ -638,7 +760,8 @@ The network invariant (#6759 D6), in full:
638
760
  3. Substitutes `workspace:*` references in every `package.json` with the matching `@working-theory/*` semver from npm — versions are pinned at npm-publish time.
639
761
  4. Substitutes `{{PROJECT_NAME}}`, `{{COMPANY_NAME}}`, `{{COMPANY_DOMAIN}}` markers in `templates/README.md` from the target directory name (and CLI flags / interactive prompts in future versions).
640
762
  5. Derives and writes `architecture.yaml` (the site's code-boundary config) from the selected module set — see below.
641
- 6. Prints next steps: `cd <dir> && pnpm install && pnpm dev`.
763
+ 6. On a site with an Admin, seeds the starter's pages into its workspace when `--install` ran and `AILK_API_URL`, `AILK_WORKSPACE_ID` and `AILK_AGENT_KEY` are set, else prints why not and the `seed-workspace` command — see [Seeding the workspace](#seeding-the-workspace).
764
+ 7. Prints next steps: `cd <dir> && pnpm install && pnpm dev`.
642
765
 
643
766
  ## `architecture.yaml` is derived from the module set
644
767
 
package/dist/cli.js CHANGED
@@ -14,6 +14,15 @@
14
14
  * on a `seed-flows` subcommand: send a spoke's declared flows/*.json to
15
15
  * its hub with the key from AILK_SPOKE_KEY, print one line per flow,
16
16
  * exit 1 if any failed (2 on a refusal it names, nothing sent) (#7876)
17
+ * on a `seed-workspace` subcommand: import a starter's content/ pages
18
+ * into its workspace with the agent key from AILK_AGENT_KEY, print one
19
+ * line per page and the count line, exit 1 if any failed (2 on a
20
+ * refusal it names, nothing sent) (#10204)
21
+ * on a `migrate-content` subcommand: plan an existing site's content/
22
+ * pages against its workspace — or, with --apply, write them — with the
23
+ * agent key from AILK_AGENT_KEY, print one line per page and the count
24
+ * line, exit 1 if any failed (2 on a refusal it names, nothing sent)
25
+ * (#10207)
17
26
  * on a `sync-routes` subcommand: parse its own argv, run syncRoutesSync(),
18
27
  * print the report, exit (1 iff routes are missing in check mode)
19
28
  * on a `license set` subcommand: write the key into the scaffold, push it
@@ -31,7 +40,11 @@
31
40
  * template's own bundle (#5481), then validates + copies the local
32
41
  * template, then merges each fetched bundle in, the site template last
33
42
  * 5. when --install: spawn `pnpm install` (cwd = target, hard-coded argv)
34
- * 6. print next-steps message
43
+ * 6. on a hub with an Admin: seed the starter's pages into the workspace
44
+ * when --install ran and AILK_API_URL, AILK_WORKSPACE_ID and
45
+ * AILK_AGENT_KEY are set, else print why not and the command that seeds
46
+ * it later; never fails the scaffold (#10204)
47
+ * 7. print next-steps message
35
48
  *
36
49
  * Any thrown error (parse failure, validation failure, copy failure,
37
50
  * `LicenseRequiredError`) is caught at the top level: stderr
@@ -55,14 +68,17 @@ import { ADOPT_HELP, AdoptRefusedError, adoptSite, formatAdoptReport, parseAdopt
55
68
  import { APPLY_DESIGN_HELP, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, } from "./apply-design.js";
56
69
  import { CLAUDE_MD_CHECK_PATH } from "./claude-md.js";
57
70
  import { buildScaffoldMarkers } from "./copy.js";
71
+ import { ImportMdxRefusedError } from "./lib/import-mdx.js";
58
72
  import { assertKeyedOrFreeReleased, assertReleased } from "./lib/module-license-gate.js";
59
73
  import { withNpmTokenUserConfig } from "./lib/npm-auth.js";
60
74
  import { FREE_PREREQUISITES_NOTE, LICENSED_PREREQUISITES_NOTE, isLicensedInstall, proCallout, } from "./lib/pro-callout.js";
61
75
  import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
62
- import { PLUGIN_INSTALL_LINES, helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
76
+ import { MIGRATE_CONTENT_HELP, MigrateContentRefusedError, migrateContent, parseMigrateContentArgs, } from "./migrate-content.js";
77
+ import { PLUGIN_INSTALL_LINES, ParseError, helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
63
78
  import { addModule, removeModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
64
79
  import { REMOVE_MODULE_HELP, RemoveModuleRefusedError, formatRemoveModuleReport, parseRemoveModuleArgs, } from "./remove-module.js";
65
80
  import { SEED_FLOWS_HELP, SeedFlowsRefusedError, parseSeedFlowsArgs, seedFlows, } from "./seed-flows.js";
81
+ import { SEED_WORKSPACE_HELP, SeedWorkspaceRefusedError, parseSeedWorkspaceArgs, runScaffoldSeed, seedWorkspace, } from "./seed-workspace.js";
66
82
  import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
67
83
  import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
68
84
  import { UPDATE_HELP, UpdateRefusedError, formatUpdateReport, parseUpdateArgs, updateSite, } from "./update.js";
@@ -271,6 +287,67 @@ async function main() {
271
287
  process.exit(err.exitCode);
272
288
  }
273
289
  }
290
+ // `seed-workspace [dir]` (#10204): import a starter's content/ pages into
291
+ // its workspace with the agent key from AILK_AGENT_KEY. Exit 1 when any
292
+ // page failed; a refusal it names (a bad argv, no roster of record, a
293
+ // spoke, a missing credential, no installed adapter) exits 2 before
294
+ // anything is sent.
295
+ if (argv[0] === "seed-workspace") {
296
+ try {
297
+ const args = parseSeedWorkspaceArgs(argv.slice(1));
298
+ if (args.help) {
299
+ process.stdout.write(SEED_WORKSPACE_HELP);
300
+ process.exit(0);
301
+ }
302
+ const { exitCode } = await seedWorkspace({
303
+ targetDir: args.target ?? process.cwd(),
304
+ apiUrl: args.apiUrl,
305
+ workspaceId: args.workspaceId,
306
+ onLine: (line) => process.stdout.write(`${line}\n`),
307
+ });
308
+ process.exit(exitCode);
309
+ }
310
+ catch (err) {
311
+ if (!(err instanceof ParseError) &&
312
+ !(err instanceof SeedWorkspaceRefusedError) &&
313
+ !(err instanceof ImportMdxRefusedError)) {
314
+ throw err;
315
+ }
316
+ process.stderr.write(`[create-ailk] ${err.message}\n`);
317
+ process.exit(2);
318
+ }
319
+ }
320
+ // `migrate-content [dir]` (#10207): import an existing site's content/
321
+ // pages into its workspace — a dry run that writes nothing unless --apply.
322
+ // Takes a site with no manifest and a spoke. Exit 1 when any page failed; a
323
+ // refusal it names (a bad argv, a missing credential, no installed adapter)
324
+ // exits 2 before anything is sent.
325
+ if (argv[0] === "migrate-content") {
326
+ try {
327
+ const args = parseMigrateContentArgs(argv.slice(1));
328
+ if (args.help) {
329
+ process.stdout.write(MIGRATE_CONTENT_HELP);
330
+ process.exit(0);
331
+ }
332
+ const { exitCode } = await migrateContent({
333
+ targetDir: args.target ?? process.cwd(),
334
+ apply: args.apply,
335
+ apiUrl: args.apiUrl,
336
+ workspaceId: args.workspaceId,
337
+ onLine: (line) => process.stdout.write(`${line}\n`),
338
+ });
339
+ process.exit(exitCode);
340
+ }
341
+ catch (err) {
342
+ if (!(err instanceof ParseError) &&
343
+ !(err instanceof MigrateContentRefusedError) &&
344
+ !(err instanceof ImportMdxRefusedError)) {
345
+ throw err;
346
+ }
347
+ process.stderr.write(`[create-ailk] ${err.message}\n`);
348
+ process.exit(2);
349
+ }
350
+ }
274
351
  // Subcommand dispatch, ahead of the flag parser: `sync-routes` has its own
275
352
  // argv shape (#4986). Check mode exits 1 iff a template route is missing;
276
353
  // locally modified routes are reported but do not fail; --apply exits 0.
@@ -393,6 +470,8 @@ async function main() {
393
470
  "\n" +
394
471
  SEED_FLOWS_HELP +
395
472
  "\n" +
473
+ SEED_WORKSPACE_HELP +
474
+ "\n" +
396
475
  APPLY_DESIGN_HELP +
397
476
  "\n" +
398
477
  UPDATE_HELP +
@@ -486,6 +565,17 @@ async function main() {
486
565
  throw new Error(`pnpm install failed in ${targetAbs} (exit ${result.status}). Run it manually after fixing the issue.`);
487
566
  }
488
567
  }
568
+ // #10204 — on a hub with an Admin, seed the starter's pages into the
569
+ // workspace, or say why not and how to later. A refusal is printed, never
570
+ // thrown: the site was written and is valid either way (D6).
571
+ await runScaffoldSeed({
572
+ targetDir: targetAbs,
573
+ topology: opts.topology,
574
+ modules: effectiveModules,
575
+ installed: opts.install === true,
576
+ env: process.env,
577
+ onLine: (line) => process.stdout.write(`${line}\n`),
578
+ });
489
579
  // Next-steps message. A licensed install (#6480) is not told it needs no
490
580
  // licence key, and is not pitched the Pro tier it already owns. Reads the
491
581
  // EFFECTIVE module set (#6759 D6) — result.modules already carries any
@@ -0,0 +1,59 @@
1
+ /** A refusal the CLI exits 2 on, thrown before any file is read or request sent. */
2
+ export declare class ImportMdxRefusedError extends Error {
3
+ readonly exitCode = 2;
4
+ constructor(message: string);
5
+ }
6
+ /** The adapter package's two members the importer needs, typed structurally. */
7
+ export interface MdxAdapterModule {
8
+ createMdxAdapter(o: {
9
+ contentRoot: string;
10
+ defaultLocale?: string;
11
+ }): {
12
+ getPageSource(type: string, slug: string, locale?: string): Promise<{
13
+ frontmatter: Record<string, unknown>;
14
+ body: string;
15
+ } | null>;
16
+ };
17
+ SCHEMA_ORG_TYPES: readonly string[];
18
+ }
19
+ export interface ImportMdxOptions {
20
+ /** The site's content root (`content/`). */
21
+ contentRoot: string;
22
+ adapters: MdxAdapterModule;
23
+ /** The API's origin: https, or http on a loopback host. */
24
+ apiOrigin: string;
25
+ workspaceId: string;
26
+ /** An agent key; sent only as a bearer token. */
27
+ apiKey: string;
28
+ /** The locale tree imported; defaults to `en`. */
29
+ defaultLocale?: string;
30
+ /** Injectable for tests; defaults to `globalThis.fetch`. */
31
+ fetch?: typeof globalThis.fetch;
32
+ /** Injectable for tests; the wait before a rate-limited retry. */
33
+ sleep?: (ms: number) => Promise<void>;
34
+ /** Each report line as it is decided, and each wait; defaults to nothing. */
35
+ onLine?: (line: string) => void;
36
+ }
37
+ export type ImportMdxOutcome = {
38
+ path: string;
39
+ result: "created" | "updated" | "unchanged";
40
+ type: string;
41
+ slug: string;
42
+ } | {
43
+ path: string;
44
+ result: "skipped" | "failed";
45
+ reason: string;
46
+ };
47
+ export type ImportMdxResultKind = ImportMdxOutcome["result"];
48
+ export interface ImportMdxResult {
49
+ pages: ImportMdxOutcome[];
50
+ counts: Record<ImportMdxResultKind, number>;
51
+ }
52
+ /** The whole report, one line per page and a final count line. */
53
+ export declare function formatImportMdxReport(result: ImportMdxResult): string;
54
+ export declare function importMdx(opts: ImportMdxOptions): Promise<ImportMdxResult>;
55
+ /**
56
+ * The site's installed adapter package, resolved from its `apps/web` — never
57
+ * create-ailk's own `node_modules`.
58
+ */
59
+ export declare function loadSiteMdxAdapter(siteRoot: string): Promise<MdxAdapterModule>;