@starci/hfs 4.9.2 → 4.9.4
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/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/emit/db-types.mjs +2 -3
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +9 -15
- package/runtime/knowledge/hfs/rules.yaml +23 -0
- package/runtime/modules/kernel/failure-codes.yaml +10 -0
- package/runtime/scripts/hfs/canon-pins.mjs +27 -0
- package/runtime/scripts/hfs/check.mjs +13 -15
- package/runtime/scripts/hfs/rules/pipeline.mjs +3 -3
- package/scaffold/add-app.mjs +2 -1
- package/scaffold/add-cli-lite.mjs +2 -3
- package/scaffold/add-queue-lite.mjs +2 -1
- package/scaffold/add.mjs +2 -1
- package/scaffold/app.mjs +5 -5
- package/src/main.mjs +15 -11
- package/upgrade/index.mjs +2 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.9.4 - 2026-10-28
|
|
4
|
+
|
|
5
|
+
- Added: the bundled rule catalog carries R323 (`FE_PAGE_HEADER_HAND_ROLLED`, enforced by `@starci/eslint-grammar-core` `no-hand-rolled-page-header`) and its failure-code guidance.
|
|
6
|
+
- Changed: the bundled canon pins follow release F (Grammar family 0.15.3, hfs 4.9.4, canon-fe 9.4.3, canon-be 3.6.3, ai-router 0.3.0).
|
|
7
|
+
|
|
8
|
+
## 4.9.3 - 2026-10-28
|
|
9
|
+
|
|
10
|
+
- Changed: the bundled canon pins no longer pin the command line. An app's `starci-harness` is judged and scaffolded at the version of the CLI that runs this package: `main(argv, { harness: { version } })` takes it, and `runtime/scripts/hfs/canon-pins.mjs` (`canonPins`, the one reader of `knowledge/hfs/canon-pins.yaml`) adds the `starci-harness` row from it for HFS_CANON_PIN_DRIFT, HFS_CI_MISSING_CANON and the scaffold's root devDependencies. The CLI depends on this package exactly, so a bundled pin of the CLI tied every CLI release to a new hfs and back. Run without a CLI, nothing judges the app's `starci-harness` and `scaffoldApp` refuses HFS_SCAFFOLD_PIN_MISSING before `git init`. Spec: `src/canon-pins.spec.mjs` (also: every `source:` pin at its folder's version, the five runtime copies byte-identical, `contexts/stack/pins.yaml` in step).
|
|
11
|
+
- Changed: the bundled canon pins follow release E (Grammar family 0.15.2, hfs 4.9.3, canon-fe 9.4.2, canon-be 3.6.2, ai-router 0.3.0).
|
|
12
|
+
|
|
3
13
|
## 4.9.2 - 2026-10-28
|
|
4
14
|
|
|
5
15
|
- Fixed: `repositoryName` (runtime/scripts/hfs/repo-identity.mjs) names a repository by its origin remote, whatever folder holds the clone; the main checkout's folder names it only without an origin. HFS_README_TITLE_INVALID and the stack project name no longer change with the clone folder. Spec: `src/repo-identity.spec.mjs`.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @starci/hfs
|
|
2
2
|
|
|
3
|
-
The HFS command line of a StarCi app: the one repository of a product, `<app>/` with its root (the `app.*` slots: the one `package.json`, lockfile and `node_modules`, the `hfs.json` declaration of kind `app`, CI, hooks, the `app.starciwork` work area) and its two sides `be/` and `fe/` (slot `app.sides`). It is installed from the npm registry
|
|
3
|
+
The HFS command line of a StarCi app: the one repository of a product, `<app>/` with its root (the `app.*` slots: the one `package.json`, lockfile and `node_modules`, the `hfs.json` declaration of kind `app`, CI, hooks, the `app.starciwork` work area) and its two sides `be/` and `fe/` (slot `app.sides`). It is installed from the npm registry as the exact dependency of the CLI that runs it in-process, `starci-harness` (see [`packages/README.md`](../README.md)), which passes `main` its own version as `harness`,
|
|
4
4
|
and it is self-contained: `runtime/` carries the slot manifest, the canon pins, the Vietnamese why
|
|
5
5
|
catalog slice, the loader and the architecture machine with every file it imports, so it runs where there is no runtime
|
|
6
6
|
checkout. The machine loads `typescript` from the app it checks (never its own copy), so run `npm ci` first.
|
|
@@ -98,7 +98,7 @@ Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file
|
|
|
98
98
|
| `HFS_STARCIWORK_LITE_FORBIDDEN` | error | content outside the lite `app.starciwork` boundary: "lite .starciwork holds only workspace.yaml and brand/ (plus .gitignore)" (R212) |
|
|
99
99
|
| `HFS_SLOT_REQUIRED_MISSING` | error | a file or directory a required slot, app or instance must contain |
|
|
100
100
|
| `HFS_MIN_INSTANCES` | error | fewer instances of a slot than `minInstances` |
|
|
101
|
-
| `HFS_CANON_PIN_DRIFT` | error | a dependency not at the exact version of `knowledge/hfs/canon-pins.yaml` |
|
|
101
|
+
| `HFS_CANON_PIN_DRIFT` | error | a dependency not at the exact version of `knowledge/hfs/canon-pins.yaml`, or `starci-harness` not at the version of the CLI that runs the check (`runtime/scripts/hfs/canon-pins.mjs`) |
|
|
102
102
|
| `HFS_SIZE_SOFT_BACKLOG` | info | a source file over `ruleParams.fileLines.soft`; report only, never fails |
|
|
103
103
|
| `HFS_MANAGED_FILE_DRIFT` | error | a managed file that exists but differs from its render (the `managedBy` slots: hooks, workflows, sonar, the back-end `be.tool-config` files, `app.format-config`, the `scripts` block of `app.package-manifest`, compared as parsed JSON) |
|
|
104
104
|
| `HFS_RULE_OFF_WITHOUT_REPLACEMENT` | error | `eslint.config.mjs`, or a front end's `stylelint.config.mjs`, differs from its managed factory render, so a rule could be off, warned or redefined in it |
|
package/emit/db-types.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { parse as parseToml } from 'smol-toml';
|
|
19
|
-
import {
|
|
19
|
+
import { bundledPins } from '../runtime/scripts/hfs/canon-pins.mjs';
|
|
20
20
|
|
|
21
21
|
/** The repository-relative path of the generated types (the path of slot `app.supabase.types`). */
|
|
22
22
|
export const dbTypesPath = 'supabase/types/database.types.ts';
|
|
@@ -56,7 +56,6 @@ class DbTypesError extends Error {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const spawn = (file, args, options = {}) => spawnSync(file, args, { encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024, ...options });
|
|
59
|
-
const PINS_FILE = new URL('../runtime/knowledge/hfs/canon-pins.yaml', import.meta.url);
|
|
60
59
|
|
|
61
60
|
/**
|
|
62
61
|
* The one spawn: an argument array, a hidden window, a bound buffer, never a shell. Specs inject a fake of the same shape. The
|
|
@@ -65,7 +64,7 @@ const PINS_FILE = new URL('../runtime/knowledge/hfs/canon-pins.yaml', import.met
|
|
|
65
64
|
const withNpxFallback = (run, platform) => (file, args, options = {}) => {
|
|
66
65
|
const first = run(file, args, options);
|
|
67
66
|
if (file !== SUPABASE_CLI || first.error?.code !== 'ENOENT') return first;
|
|
68
|
-
const version =
|
|
67
|
+
const version = bundledPins()[SUPABASE_CLI]?.version;
|
|
69
68
|
if (!version) return first;
|
|
70
69
|
return platform === 'win32'
|
|
71
70
|
? run('cmd.exe', ['/d', '/s', '/c', 'npx', '--yes', `${SUPABASE_CLI}@${version}`, ...args], options)
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
schema: starci/canon-pins@1
|
|
2
2
|
purpose: >-
|
|
3
3
|
The exact version of every shared dependency in the StarCi family, including package-owned runtime dependencies. One version per dependency for the whole
|
|
4
|
-
family.
|
|
4
|
+
family. The command line itself is not pinned here: an app's starci-harness is the version of the CLI that runs @starci/hfs to
|
|
5
|
+
judge or scaffold it (scripts/hfs/canon-pins.mjs), since that CLI depends on this package exactly. Raising a version is a change to this file first (with the package bump when it is a @starci package),
|
|
5
6
|
then the repositories' own upgrade lane (each sets the exact version and reinstalls; nothing else installs a @starci package). Read on 2026-09-29 from the packages under
|
|
6
7
|
.claude/packages and from the installed node_modules of the six product repositories; where they disagreed the
|
|
7
8
|
newest version installed in more than one repository won, and the runtime's own version won for @starci packages.
|
|
@@ -30,35 +31,35 @@ pins:
|
|
|
30
31
|
source: packages/event-stream/package.json
|
|
31
32
|
why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
|
|
32
33
|
'@starci/grammar':
|
|
33
|
-
version: 0.15.
|
|
34
|
+
version: 0.15.3
|
|
34
35
|
group: starci
|
|
35
36
|
install: registry
|
|
36
37
|
side: fe
|
|
37
38
|
source: packages/grammar/package.json
|
|
38
39
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
39
40
|
'@starci/eslint-canon-be':
|
|
40
|
-
version: 3.6.
|
|
41
|
+
version: 3.6.3
|
|
41
42
|
group: starci
|
|
42
43
|
install: registry
|
|
43
44
|
side: be
|
|
44
45
|
source: packages/eslint-canon-be/package.json
|
|
45
46
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
46
47
|
'@starci/eslint-canon-fe':
|
|
47
|
-
version: 9.4.
|
|
48
|
+
version: 9.4.3
|
|
48
49
|
group: starci
|
|
49
50
|
install: registry
|
|
50
51
|
side: fe
|
|
51
52
|
source: packages/eslint-canon-fe/package.json
|
|
52
53
|
why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
|
|
53
54
|
'@starci/eslint-grammar-core':
|
|
54
|
-
version: 0.15.
|
|
55
|
+
version: 0.15.3
|
|
55
56
|
group: starci
|
|
56
57
|
install: registry
|
|
57
58
|
side: fe
|
|
58
59
|
source: packages/eslint-grammar-core/package.json
|
|
59
60
|
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
60
61
|
'@starci/eslint-grammar-offset-pop':
|
|
61
|
-
version: 0.15.
|
|
62
|
+
version: 0.15.3
|
|
62
63
|
group: starci
|
|
63
64
|
install: registry
|
|
64
65
|
side: fe
|
|
@@ -98,21 +99,14 @@ pins:
|
|
|
98
99
|
source: packages/test-world/package.json
|
|
99
100
|
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
100
101
|
'@starci/ai-router':
|
|
101
|
-
version: 0.
|
|
102
|
+
version: 0.3.0
|
|
102
103
|
group: starci
|
|
103
104
|
install: registry
|
|
104
105
|
side: be
|
|
105
106
|
source: packages/ai-router/package.json
|
|
106
107
|
why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
107
|
-
'starci-harness':
|
|
108
|
-
version: 1.1.0-alpha.2
|
|
109
|
-
group: starci
|
|
110
|
-
install: registry
|
|
111
|
-
side: both
|
|
112
|
-
source: cli/package.json
|
|
113
|
-
why: 'the one StarCi command line (bins starci and starci-harness): a product repository installs it for starci app, which it runs in-process through its exact @starci/hfs dependency, and its codegen loads starci-harness/input-bounds.'
|
|
114
108
|
'@starci/hfs':
|
|
115
|
-
version: 4.9.
|
|
109
|
+
version: 4.9.4
|
|
116
110
|
group: starci
|
|
117
111
|
install: registry
|
|
118
112
|
side: both
|
|
@@ -5687,3 +5687,26 @@ rules:
|
|
|
5687
5687
|
source: owner
|
|
5688
5688
|
quote: "A chip label starts uppercase."
|
|
5689
5689
|
ruling: "The label of a status chip starts with an uppercase letter (owner ruling chip-label-uppercase); StatusDot capitalises a text label it renders, and the lint refuses a lowercase literal label at its source."
|
|
5690
|
+
- id: R323
|
|
5691
|
+
code: "FE_PAGE_HEADER_HAND_ROLLED"
|
|
5692
|
+
law: "A page is headed by Grammar's PageHeader; a Breadcrumbs trail followed by a SectionHeader in one parent is a page head composed by hand, with no band, no top inset and no separator."
|
|
5693
|
+
kinds: [lint]
|
|
5694
|
+
gates: [pre-commit, settle, land, ci]
|
|
5695
|
+
failureCodes: ["FE_PAGE_HEADER_HAND_ROLLED"]
|
|
5696
|
+
enforcers:
|
|
5697
|
+
- {kind: eslint-grammar-core, id: no-hand-rolled-page-header}
|
|
5698
|
+
editions: [lite, full]
|
|
5699
|
+
docs: [knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP]
|
|
5700
|
+
templateNa: "A template states no page head; the pages are the app's."
|
|
5701
|
+
proves:
|
|
5702
|
+
- packages/eslint-grammar-core/src/page-header.spec.ts
|
|
5703
|
+
exemplifiedBy:
|
|
5704
|
+
- {path: packages/eslint-grammar-core/src/page-header.spec.ts, editions: [lite, full], codes: [FE_PAGE_HEADER_HAND_ROLLED]}
|
|
5705
|
+
witness:
|
|
5706
|
+
- {path: packages/eslint-grammar-core/src/page-header.spec.ts, editions: [lite, full], codes: [FE_PAGE_HEADER_HAND_ROLLED]}
|
|
5707
|
+
cases:
|
|
5708
|
+
- id: case-1
|
|
5709
|
+
date: "2026-10-28"
|
|
5710
|
+
source: owner
|
|
5711
|
+
quote: "Publish Grammar 0.15.3 with PageHeader now, then move all 18 pages to it before deploying. (translated from Vietnamese)"
|
|
5712
|
+
ruling: "A page is headed by Grammar's PageHeader (owner ruling page-header-band): a surface band whose content shares the page content's edges and which ends in its own full-width separator, page tabs standing on it; the lint refuses a page head composed by hand from Breadcrumbs and SectionHeader."
|
|
@@ -2714,6 +2714,16 @@ FE_PACKAGE_SHAPE:
|
|
|
2714
2714
|
owner: op-retry
|
|
2715
2715
|
kind: check-finding
|
|
2716
2716
|
|
|
2717
|
+
FE_PAGE_HEADER_HAND_ROLLED:
|
|
2718
|
+
title: "A page head is composed by hand instead of PageHeader"
|
|
2719
|
+
title_vi: "Phần đầu trang được ghép tay thay vì dùng PageHeader"
|
|
2720
|
+
meaning_vi: "Tại <file>, một Breadcrumbs theo sau là một SectionHeader trong cùng một phần tử cha: phần đầu trang ghép tay không có dải nền, không có khoảng đệm trên và không có đường phân cách, nên mỗi trang tự quyết khoảng cách riêng."
|
|
2721
|
+
causes_vi:
|
|
2722
|
+
- "Đặt Breadcrumbs rồi SectionHeader level={1} ở đầu trang hoặc layout."
|
|
2723
|
+
nextStep_vi: "Dùng PageHeader của Grammar với breadcrumbs, title và các khe cần thiết (description, meta, status, mark, actions, tabs) (quy định page-header-band)."
|
|
2724
|
+
owner: op-retry
|
|
2725
|
+
kind: check-finding
|
|
2726
|
+
|
|
2717
2727
|
FE_PAGE_METADATA_MISSING:
|
|
2718
2728
|
title: "Every page exports metadata"
|
|
2719
2729
|
title_vi: "Trang không khai báo metadata"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// canon-pins.mjs - the pins an app is judged and scaffolded against. knowledge/hfs/canon-pins.yaml pins every shared dependency
|
|
2
|
+
// but the command line itself: the `starci-harness` row is the version of the CLI that runs this @starci/hfs (it passes its own
|
|
3
|
+
// version to main as `harness`), never a bundled row, since the CLI depends on this package exactly and a bundled pin of the CLI
|
|
4
|
+
// would make each release of one need a release of the other. Run without a CLI (the package's own specs and diagnostics), there
|
|
5
|
+
// is no such row: nothing judges the app's starci-harness, and the scaffold, which needs it, refuses.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
9
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
10
|
+
|
|
11
|
+
/** The runtime-relative file of the bundled pins. */
|
|
12
|
+
export const CANON_PINS_FILE = 'knowledge/hfs/canon-pins.yaml';
|
|
13
|
+
/** The npm name of the command line that runs @starci/hfs, pinned at its own version. */
|
|
14
|
+
export const HARNESS_PACKAGE = 'starci-harness';
|
|
15
|
+
|
|
16
|
+
/** The pin map of knowledge/hfs/canon-pins.yaml under `root` (the bundled runtime by default). */
|
|
17
|
+
export const bundledPins = (root = skillRoot) => parseYaml(fs.readFileSync(path.join(root, CANON_PINS_FILE), 'utf8'))?.pins ?? {};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The pin map an app answers to: the bundled pins plus the `starci-harness` row at `harness.version`, the version of the CLI that
|
|
21
|
+
* runs this package; without `harness` the bundled pins alone.
|
|
22
|
+
*/
|
|
23
|
+
export function canonPins({ root = skillRoot, harness } = {}) {
|
|
24
|
+
const pins = bundledPins(root);
|
|
25
|
+
if (harness?.version === undefined) return pins;
|
|
26
|
+
return { ...pins, [HARNESS_PACKAGE]: { version: harness.version, group: 'starci', install: 'registry', side: 'both' } };
|
|
27
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// hfs-check.mjs - the HFS app check, behind `starci app check | explain` (packages/hfs) and reusable by any
|
|
2
2
|
// runtime check. It reads three things and nothing else: the app's hfs.json, the slot manifest
|
|
3
|
-
// (knowledge/hfs/slots.yaml through scripts/hfs/slots.mjs) and the pins (knowledge/hfs/canon-pins.yaml
|
|
3
|
+
// (knowledge/hfs/slots.yaml through scripts/hfs/slots.mjs) and the pins (canon-pins.mjs: the bundled knowledge/hfs/canon-pins.yaml
|
|
4
|
+
// and the version of the starci-harness that runs the check). It never
|
|
4
5
|
// writes to the repository it inspects.
|
|
5
6
|
//
|
|
6
7
|
// checkRepo() answers, for the tracked paths of one app (git ls-files), per scope (the app root and each side folder):
|
|
@@ -64,7 +65,7 @@ import { appWiring, BOOT_PROOF_CODE } from './app-wiring.mjs';
|
|
|
64
65
|
import { coverageFrom as coverageFromReport } from './coverage-from.mjs';
|
|
65
66
|
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
66
67
|
import { ARCHITECTURE_RULE_IDS, checkArchitecture } from './architecture/index.mjs';
|
|
67
|
-
import {
|
|
68
|
+
import { canonPins } from './canon-pins.mjs';
|
|
68
69
|
import { readCatalogOnce } from './catalog-once.mjs';
|
|
69
70
|
import { APP_SCOPE, HFS_DECLARATION_FILE, appRelativeMessages, HfsSlotsError, SIDES, createSlotResolver, loadSlotManifest, readRepoDeclaration, resolveRepoDeclaration } from './slots.mjs';
|
|
70
71
|
import { judgedFindings, summarize, withWhy } from './check-results.mjs';
|
|
@@ -101,7 +102,6 @@ export { REFUSAL_CODES } from './refusal-codes.mjs';
|
|
|
101
102
|
// The database rules (R213-R216) read SQL through a WASM parser, so they are async: `starci app check` calls them beside the other emitters and passes the findings in as `extraFindings`.
|
|
102
103
|
export { checkDatabase } from './rules/database.mjs';
|
|
103
104
|
|
|
104
|
-
const CANON_PINS_FILE = 'knowledge/hfs/canon-pins.yaml';
|
|
105
105
|
export const FAILURE_CODES_FILE = 'modules/kernel/failure-codes.yaml';
|
|
106
106
|
/** The codes this module emits that are not the slot loader's own: the why bundle of packages/hfs ships exactly these plus the loader's. */
|
|
107
107
|
export const CHECK_CODES = Object.freeze([
|
|
@@ -151,9 +151,6 @@ export function trackedFiles(repoRoot) {
|
|
|
151
151
|
|
|
152
152
|
const fill = (text, bindings) => String(text).replace(VAR, (whole, name) => bindings[name] ?? whole);
|
|
153
153
|
|
|
154
|
-
/** The pin map of knowledge/hfs/canon-pins.yaml under `root`. */
|
|
155
|
-
const readPins = (root) => parseYaml(fs.readFileSync(path.join(root, CANON_PINS_FILE), 'utf8'))?.pins ?? {};
|
|
156
|
-
|
|
157
154
|
/** Instances (slot, root, bindings) present in the tracked tree, for every slot that names required files or a minimum. */
|
|
158
155
|
function instancesOf(resolver, files) {
|
|
159
156
|
const found = new Map();
|
|
@@ -252,7 +249,7 @@ function sizeSoftFindings({ repoRoot, resolver, files, inScope }) {
|
|
|
252
249
|
* relative to it, exactly as the standalone repository root was). `files` are the scope's tracked paths; `all` (root only) every
|
|
253
250
|
* tracked path of the app, for the rules that read across it (dependency skew, proof commands).
|
|
254
251
|
*/
|
|
255
|
-
function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, scoped, manifest, appSide = false, editionDeclaration = true, phase }) {
|
|
252
|
+
function scopeFindings({ repoRoot, root, harness, repo, resolver, files, all = files, scoped, manifest, appSide = false, editionDeclaration = true, phase }) {
|
|
256
253
|
const inScope = (file) => !scoped || scoped.has(file);
|
|
257
254
|
// A required directory of the root (be/, fe/) is present through the files below it, which are the sides' own.
|
|
258
255
|
const trackedSet = new Set(all);
|
|
@@ -265,14 +262,15 @@ function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, sco
|
|
|
265
262
|
// The runtime repository (kind runtime) is judged here by its tree law only; its own rules run in scripts/hfs/runtime-check.mjs.
|
|
266
263
|
if (repo.kind === RUNTIME_KIND) return findings;
|
|
267
264
|
|
|
268
|
-
const pins =
|
|
265
|
+
const pins = canonPins({ root, harness });
|
|
269
266
|
findings.push(...contentRuleFindings({ repoRoot, repo, resolver, files, all, scoped, pins, inScope, isRoot, appSide, editionDeclaration, manifest, phase }));
|
|
270
267
|
if (!isRoot) findings.push(...sizeSoftFindings({ repoRoot, resolver, files, inScope }));
|
|
271
268
|
return findings;
|
|
272
269
|
}
|
|
273
270
|
|
|
274
271
|
/**
|
|
275
|
-
* The check of one app. `repoRoot` is the app root (a side folder is checked as that side alone). `
|
|
272
|
+
* The check of one app. `repoRoot` is the app root (a side folder is checked as that side alone). `harness` ({ version }) is the CLI
|
|
273
|
+
* that runs the check, whose version is the app's starci-harness pin (canon-pins.mjs). `declaration` overrides hfs.json
|
|
276
274
|
* (a dry run over an app that has none); `files` overrides git ls-files (specs), app-relative. `only` (a list of app-relative paths)
|
|
277
275
|
* limits the per-path checks (slot, pin, size) to those paths; the checks of the tree as a whole (required files, minimum instances,
|
|
278
276
|
* empty directories, untracked entries) are not per-path. `tree: false` skips the file-system checks (empty directories, ghosts,
|
|
@@ -284,7 +282,7 @@ function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, sco
|
|
|
284
282
|
* `phase: 'source'` is the runtime's no-install example-render pass: tracked obligations still run, while ignored generated
|
|
285
283
|
* artifact presence/freshness belongs to the default `workspace` phase after codegen. Ordinary app check/lint always use it.
|
|
286
284
|
*/
|
|
287
|
-
export function checkRepo({ repoRoot, root = skillRoot, declaration, files, only, extraFindings = [], tree = files === undefined, manifest = loadSlotManifest({ root }), surface = 'all', phase = 'workspace' }) {
|
|
285
|
+
export function checkRepo({ repoRoot, root = skillRoot, harness, declaration, files, only, extraFindings = [], tree = files === undefined, manifest = loadSlotManifest({ root }), surface = 'all', phase = 'workspace' }) {
|
|
288
286
|
if (!['source', 'workspace'].includes(phase)) throw new Error(`unknown HFS inspection phase ${phase}`);
|
|
289
287
|
const why = readWhy(root);
|
|
290
288
|
let repo;
|
|
@@ -303,18 +301,18 @@ export function checkRepo({ repoRoot, root = skillRoot, declaration, files, only
|
|
|
303
301
|
const keep = (list, scopeRoot) => (surface === 'check' ? list.filter((finding) => !(finding.origin === 'repo' && onLintSurface(scopeRoot, finding))) : list);
|
|
304
302
|
if (repo.profile === APP_SCOPE) {
|
|
305
303
|
const own = tracked.filter((file) => resolver.sideOf(file) === null);
|
|
306
|
-
findings.push(...keep(scopeFindings({ repoRoot, root, repo, resolver, files: own, all: tracked, scoped, manifest, phase }), repoRoot));
|
|
304
|
+
findings.push(...keep(scopeFindings({ repoRoot, root, harness, repo, resolver, files: own, all: tracked, scoped, manifest, phase }), repoRoot));
|
|
307
305
|
for (const side of SIDES) {
|
|
308
306
|
const prefix = `${side}/`;
|
|
309
307
|
const sideRoot = path.join(repoRoot, side);
|
|
310
308
|
const sideFiles = tracked.filter((file) => file.startsWith(prefix)).map((file) => file.slice(prefix.length));
|
|
311
309
|
const sideScoped = scoped ? new Set([...scoped].filter((file) => file.startsWith(prefix)).map((file) => file.slice(prefix.length))) : null;
|
|
312
|
-
const sideFindings = scopeFindings({ repoRoot: sideRoot, root, repo: repo.sides[side], resolver: resolver.sides[side], files: sideFiles, scoped: sideScoped, manifest, appSide: true, editionDeclaration: false, phase });
|
|
310
|
+
const sideFindings = scopeFindings({ repoRoot: sideRoot, root, harness, repo: repo.sides[side], resolver: resolver.sides[side], files: sideFiles, scoped: sideScoped, manifest, appSide: true, editionDeclaration: false, phase });
|
|
313
311
|
const message = appRelativeMessages(side, sideRoot);
|
|
314
312
|
findings.push(...keep(sideFindings, sideRoot).map((finding) => ({ ...finding, side, path: onSide(side, finding.path), message: message(finding.message) })));
|
|
315
313
|
}
|
|
316
314
|
} else {
|
|
317
|
-
findings.push(...keep(scopeFindings({ repoRoot, root, repo, resolver, files: tracked, scoped, manifest, phase }), repoRoot));
|
|
315
|
+
findings.push(...keep(scopeFindings({ repoRoot, root, harness, repo, resolver, files: tracked, scoped, manifest, phase }), repoRoot));
|
|
318
316
|
}
|
|
319
317
|
findings.push(...extraFindings);
|
|
320
318
|
const untrackedSecrets = untrackedSecretFindings({ repoRoot, files: tracked, repo });
|
|
@@ -367,9 +365,9 @@ function machineFindings(report) {
|
|
|
367
365
|
* templates or run the formatter (packages/hfs/sync), judged with checkRepo's own. `machine` is injectable for specs. A missing
|
|
368
366
|
* merge-base under `fast` is an Error, never a silent full pass. `repoRoot` may also be a side folder: that side alone is checked.
|
|
369
367
|
*/
|
|
370
|
-
export function checkRepository({ repoRoot, root = skillRoot, fast = false, base, extraFindings = [], manifest = loadSlotManifest({ root }), machine = checkArchitecture, wiring = false, coverageFrom }) {
|
|
368
|
+
export function checkRepository({ repoRoot, root = skillRoot, harness, fast = false, base, extraFindings = [], manifest = loadSlotManifest({ root }), machine = checkArchitecture, wiring = false, coverageFrom }) {
|
|
371
369
|
const changed = fast ? changedSince(repoRoot, base) : null;
|
|
372
|
-
const slotResult = checkRepo({ repoRoot, root, manifest, extraFindings, surface: 'check', ...(changed ? { only: changed.files, tree: false } : {}) });
|
|
370
|
+
const slotResult = checkRepo({ repoRoot, root, harness, manifest, extraFindings, surface: 'check', ...(changed ? { only: changed.files, tree: false } : {}) });
|
|
373
371
|
if (slotResult.profile === null) return { ...slotResult, machine: { status: 'skipped', reason: 'hfs.json is not valid' } };
|
|
374
372
|
const repo = readRepoDeclaration(manifest, repoRoot);
|
|
375
373
|
const scopes = repo.profile === APP_SCOPE
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// pipeline.mjs - HFS_CI_MISSING_CANON (R13): CI runs the pinned `starci app lint`.
|
|
2
2
|
// .github/workflows/ci.yml a `run:` step that runs the one lint entry: `starci app lint`,
|
|
3
3
|
// `npx starci-harness[@<version>] app lint`, or `npm run <script>` whose command runs `starci app lint`
|
|
4
|
-
// (`npm run lint`, arguments after `--` allowed); a version named in the step is the starci-harness pin
|
|
5
|
-
// canon-pins.
|
|
4
|
+
// (`npm run lint`, arguments after `--` allowed); a version named in the step is the starci-harness pin,
|
|
5
|
+
// the version of the CLI that runs the check (canon-pins.mjs; an installed one is pinned by the pin check, R15)
|
|
6
6
|
// (the push hook is no lint or typecheck step: it checks the release gate, scripts/hfs/runtime-rules/hook-shape.mjs keeps its shape)
|
|
7
7
|
// A missing file is the slot manifest's finding (HFS_SLOT_REQUIRED_MISSING); a file that is present and lacks the step is this rule's.
|
|
8
8
|
// The redirect of `core.hooksPath` away from husky is the architecture machine's (HFS_HOOKS_PATH_REDIRECTED).
|
|
@@ -77,7 +77,7 @@ function expanded(command, scripts) {
|
|
|
77
77
|
/** Only the bundled example runner with its fixed current-app arguments delegates to the following argv. */
|
|
78
78
|
const exampleCommandOf = (command) => command.startsWith(EXAMPLE_RUN_PREFIX) ? command.slice(EXAMPLE_RUN_PREFIX.length) : command;
|
|
79
79
|
|
|
80
|
-
/** The findings of R13 over the repository at `repoRoot`; `pins` is the
|
|
80
|
+
/** The findings of R13 over the repository at `repoRoot`; `pins` is the canon pin map (canon-pins.mjs). */
|
|
81
81
|
export function pipelineFindings({ repoRoot, files, pins }) {
|
|
82
82
|
const findings = [];
|
|
83
83
|
const pinned = pins?.[hfsPackage]?.version;
|
package/scaffold/add-app.mjs
CHANGED
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
} from "../runtime/scripts/hfs/slots.mjs";
|
|
8
8
|
import { imageFiles, render, TEMPLATES_DIR } from "../sync/index.mjs";
|
|
9
9
|
import { ScaffoldError, pascalOf } from "./service.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { bundledPins } from "../runtime/scripts/hfs/canon-pins.mjs";
|
|
11
|
+
import { PARTIAL_VARIABLES, callbackSourceFiles, coreFeApps, hasAuthCallback, jsonText, packageJsonText } from "./app.mjs";
|
|
11
12
|
|
|
12
13
|
/** The shared fe app core (modules/api, auth, toast, redux, the providers, the hooks): the base tree, then the edition overlay. */
|
|
13
14
|
const CORE_DIRS = Object.freeze({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveRepoDeclaration } from "../runtime/scripts/hfs/slots.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { bundledPins } from "../runtime/scripts/hfs/canon-pins.mjs";
|
|
5
5
|
import { appScripts, appSource, imageFiles, TEMPLATES_DIR } from "../sync/index.mjs";
|
|
6
6
|
import { ScaffoldError } from "./service.mjs";
|
|
7
7
|
import { jsonText, packageJsonText } from "./app.mjs";
|
|
@@ -12,7 +12,6 @@ const read = (relative) =>
|
|
|
12
12
|
fs
|
|
13
13
|
.readFileSync(path.join(TEMPLATES_DIR, ...relative.split("/")), "utf8")
|
|
14
14
|
.replace(/\r\n/g, "\n");
|
|
15
|
-
const PINS_FILE = path.join(import.meta.dirname, "..", "runtime", "knowledge", "hfs", "canon-pins.yaml");
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
const managedScripts = (repo, source) => {
|
|
@@ -274,7 +273,7 @@ export function ensureLiteCli({ root, manifest, repo }) {
|
|
|
274
273
|
if (!fs.existsSync(packageFile))
|
|
275
274
|
throw new ScaffoldError("HFS_ADD_NOT_AN_APP", "add cli needs the app root package.json");
|
|
276
275
|
const packageManifest = JSON.parse(fs.readFileSync(packageFile, "utf8"));
|
|
277
|
-
const pin =
|
|
276
|
+
const pin = bundledPins()["nest-commander"]?.version;
|
|
278
277
|
if (typeof pin !== "string" || pin === "")
|
|
279
278
|
throw new ScaffoldError("HFS_ADD_CANON_PIN_MISSING", "add cli needs the nest-commander canon pin");
|
|
280
279
|
packageManifest.dependencies = { ...packageManifest.dependencies, "nest-commander": pin };
|
|
@@ -13,7 +13,8 @@ import { materializeDeclaredPlaceholders } from '../runtime/scripts/hfs/sealed-m
|
|
|
13
13
|
import { REDIS_IMAGE, TEMPLATES_DIR, redisDevScripts, render } from '../sync/index.mjs';
|
|
14
14
|
import { byCodeUnit } from '../report/order.mjs';
|
|
15
15
|
import { withSqlIdentifierError } from './add-database-lite.mjs';
|
|
16
|
-
import {
|
|
16
|
+
import { jsonText, packageJsonText } from './app.mjs';
|
|
17
|
+
import { bundledPins } from '../runtime/scripts/hfs/canon-pins.mjs';
|
|
17
18
|
import { registerLiteExports } from './lite-exports.mjs';
|
|
18
19
|
import { ScaffoldError } from './service.mjs';
|
|
19
20
|
import { YamlBlockEdit } from './yaml-block-edit.mjs';
|
package/scaffold/add.mjs
CHANGED
|
@@ -25,7 +25,8 @@ import { addLiteWebhookInbox } from './add-inbox-lite.mjs';
|
|
|
25
25
|
import { planLiteQueue } from './add-queue-lite.mjs';
|
|
26
26
|
import { addBuiltinLiteCli, fillKnownForms, formsOf, planKind, readAddContext } from './add-plan.mjs';
|
|
27
27
|
export { liteApiTemplates } from './add-plan.mjs';
|
|
28
|
-
import {
|
|
28
|
+
import { jsonText, packageJsonText, peerDependencies } from './app.mjs';
|
|
29
|
+
import { bundledPins } from '../runtime/scripts/hfs/canon-pins.mjs';
|
|
29
30
|
import { byCodeUnit } from '../report/order.mjs';
|
|
30
31
|
import { registerLiteDomainService, registerLiteExports } from './lite-exports.mjs';
|
|
31
32
|
|
package/scaffold/app.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import { spawnSync } from 'node:child_process';
|
|
18
18
|
import { loadSlotManifest, resolveRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
19
19
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
20
|
+
import { HARNESS_PACKAGE, canonPins } from '../runtime/scripts/hfs/canon-pins.mjs';
|
|
20
21
|
import { dbTypesPath, generateDbTypes } from '../emit/db-types.mjs';
|
|
21
22
|
import { TEMPLATES_DIR, appSource, imageFiles, render, renderTargets, writeTargets } from '../sync/index.mjs';
|
|
22
23
|
import { ScaffoldError } from './service.mjs';
|
|
@@ -40,7 +41,6 @@ const APP_DIR = '__app__';
|
|
|
40
41
|
/** The path variable of the project name: `packages/__project__-ui` is written as `packages/<project>-ui`. */
|
|
41
42
|
const PROJECT_DIR = '__project__';
|
|
42
43
|
const TIMESTAMP = '__timestamp__';
|
|
43
|
-
const PINS_FILE = path.join(import.meta.dirname, '..', 'runtime', 'knowledge', 'hfs', 'canon-pins.yaml');
|
|
44
44
|
/**
|
|
45
45
|
* The front-end stack catalog (knowledge/hfs/fe-stack.yaml): the bundled runtime copy, else the runtime checkout's own file (the
|
|
46
46
|
* package inside the StarCi repository before the runtime copy carries it).
|
|
@@ -53,8 +53,6 @@ const SONAR_GATE_FILE = path.join(import.meta.dirname, '..', 'runtime', 'knowled
|
|
|
53
53
|
/** The runtime-peer catalog (knowledge/hfs/peer-integrations.yaml, R111) the package carries. */
|
|
54
54
|
const PEERS_FILE = path.join(import.meta.dirname, '..', 'runtime', 'knowledge', 'hfs', 'peer-integrations.yaml');
|
|
55
55
|
const pascal = name => name.split('-').map(part => part[0].toUpperCase() + part.slice(1)).join('');
|
|
56
|
-
/** The canon pins (knowledge/hfs/canon-pins.yaml) the package carries. */
|
|
57
|
-
export const bundledPins = () => parseYaml(fs.readFileSync(PINS_FILE, 'utf8')).pins;
|
|
58
56
|
|
|
59
57
|
/** The parsed front-end stack catalog; a package without it cannot know what every front end installs. */
|
|
60
58
|
function bundledFeStack() {
|
|
@@ -637,7 +635,8 @@ function emitScaffoldDatabaseTypes(root, name, writes, emitTypes) {
|
|
|
637
635
|
* `starci app scaffold <name>`: writes the new app under `into`, resolves its lockfile with npm (`lock`, npmLock), and returns
|
|
638
636
|
* `{ root, files }` (app-relative paths, sorted). A new path is first made an empty repository with unborn main (`initGit`), so the
|
|
639
637
|
* app always lives in its own Git repository. `presets` are the jest preset's Sonar exclusions; the CLI passes the copy @starci/hfs
|
|
640
|
-
* ships (bundledPresets).
|
|
638
|
+
* ships (bundledPresets). `harness` ({ version }) is the CLI that runs the scaffold: the root devDependencies name starci-harness at
|
|
639
|
+
* its version (canon-pins.mjs). Failure removes only scaffold-owned entries, retaining the .git.
|
|
641
640
|
*/
|
|
642
641
|
/** Seal declared starter members to a fresh app recipient through the scaffold rollback owner. */
|
|
643
642
|
function initializeScaffoldSecrets(root, writes) {
|
|
@@ -659,13 +658,14 @@ function bornRepository(root, initGit) {
|
|
|
659
658
|
return fs.lstatSync(root);
|
|
660
659
|
}
|
|
661
660
|
|
|
662
|
-
export function scaffoldApp({ name, into, presets, edition = 'full', authCallback = false, registryVisibility = 'public', manifest = loadSlotManifest(), pins =
|
|
661
|
+
export function scaffoldApp({ name, into, presets, harness, edition = 'full', authCallback = false, registryVisibility = 'public', manifest = loadSlotManifest(), pins = canonPins({ harness }), lock = npmLock, emitTypes = generateDbTypes, initGit = gitInit, now = () => new Date() }) {
|
|
663
662
|
if (!NAME.test(String(name))) throw new ScaffoldError('HFS_SCAFFOLD_NAME_INVALID', `the app name ${name} must be kebab-case (a project name: ${NAME})`);
|
|
664
663
|
const root = path.resolve(into, name);
|
|
665
664
|
const existing = fs.lstatSync(root, { throwIfNoEntry: false });
|
|
666
665
|
if (existing && !emptyUnbornGit(root, existing)) throw new ScaffoldError('HFS_SCAFFOLD_EXISTS', `${root} already exists; only a plain directory containing solely .git with unborn main and an empty index can be scaffolded`);
|
|
667
666
|
if (!['public', 'private'].includes(registryVisibility)) throw new ScaffoldError('HFS_SCAFFOLD_REGISTRY_VISIBILITY', `--registry-visibility accepts public or private (got ${registryVisibility})`);
|
|
668
667
|
if (typeof authCallback !== 'boolean') throw new ScaffoldError('HFS_SCAFFOLD_NAME_INVALID', 'authCallback must be a boolean');
|
|
668
|
+
if (!pins[HARNESS_PACKAGE]) throw new ScaffoldError('HFS_SCAFFOLD_PIN_MISSING', `${HARNESS_PACKAGE} has no pin: an app pins the CLI that scaffolds it, so run the scaffold through it (starci app scaffold)`);
|
|
669
669
|
const original = existing ?? bornRepository(root, initGit);
|
|
670
670
|
const declaration = starterDeclaration(name, manifest, edition);
|
|
671
671
|
if (authCallback) enableAuthCallback(declaration);
|
package/src/main.mjs
CHANGED
|
@@ -242,7 +242,7 @@ function printExplain(e, out) {
|
|
|
242
242
|
* The app pass: slots, managed files, formatter, contract snapshots and the architecture machine's check surface, root and sides.
|
|
243
243
|
* `only` limits the formatter to those (app-relative) files.
|
|
244
244
|
*/
|
|
245
|
-
async function runCheck({ repoRoot, fast = false, base, only, presets, prettier, dbTypes = false }) {
|
|
245
|
+
async function runCheck({ repoRoot, harness, fast = false, base, only, presets, prettier, dbTypes = false }) {
|
|
246
246
|
const tracked = trackedFiles(repoRoot);
|
|
247
247
|
// Managed files of the root and both sides, the .gitignore block and sonar against their render (R04, R05, R11, ...), and prettier
|
|
248
248
|
// through the app's own install (R19, never under --fast).
|
|
@@ -260,7 +260,7 @@ async function runCheck({ repoRoot, fast = false, base, only, presets, prettier,
|
|
|
260
260
|
contracts = { status: 'checked', apps: emitted.apps };
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
|
-
const result = checkRepository({ repoRoot, fast, base, extraFindings });
|
|
263
|
+
const result = checkRepository({ repoRoot, harness, fast, base, extraFindings });
|
|
264
264
|
if (contracts) result.contracts = contracts;
|
|
265
265
|
return result;
|
|
266
266
|
}
|
|
@@ -274,7 +274,7 @@ export function appRootAbove(dir) {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
/** `starci app lint`: ESLint per side, the app checks and stylelint over the fe side as one report; see lint/run.mjs. */
|
|
277
|
-
async function lintMain(argv, { cwd, stdout, presets, prettier, runLinter, checkApp = runCheck }) {
|
|
277
|
+
async function lintMain(argv, { cwd, harness, stdout, presets, prettier, runLinter, checkApp = runCheck }) {
|
|
278
278
|
const opts = parseLintArgs(argv);
|
|
279
279
|
// --workspace names a folder from the working directory (`starci app lint --workspace .` is an fe workspace script); the app
|
|
280
280
|
// root is then the nearest folder above it that holds hfs.json, and the workspace is passed on app-relative.
|
|
@@ -283,7 +283,7 @@ async function lintMain(argv, { cwd, stdout, presets, prettier, runLinter, check
|
|
|
283
283
|
if (workspaceDir) opts.workspace = path.relative(repoRoot, workspaceDir).split(path.sep).join('/');
|
|
284
284
|
const { report, sonar, exit } = await lintRepository({
|
|
285
285
|
repoRoot, opts, trackedFiles, runLinter,
|
|
286
|
-
hfsCheck: (root) => checkApp({ repoRoot: root, only: opts.changed ?? undefined, presets, prettier }),
|
|
286
|
+
hfsCheck: (root) => checkApp({ repoRoot: root, harness, only: opts.changed ?? undefined, presets, prettier }),
|
|
287
287
|
});
|
|
288
288
|
if (opts.sonar !== undefined) writeReport(path.resolve(cwd, opts.sonar), sonar);
|
|
289
289
|
if (opts.format === 'json') stdout(`${JSON.stringify(report, null, 2)}
|
|
@@ -346,10 +346,10 @@ function printCheckProofs(result, out) {
|
|
|
346
346
|
if (result.coverage) out(formatCoverageReport(result.coverage));
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
async function checkMain(opts, { cwd: repoRoot, edition, stdout: out, presets, prettier, checkApp = runCheck, runLinter }) {
|
|
349
|
+
async function checkMain(opts, { cwd: repoRoot, harness, edition, stdout: out, presets, prettier, checkApp = runCheck, runLinter }) {
|
|
350
350
|
if (opts.positional.length) throw new Error('starci app check takes no path');
|
|
351
351
|
if (opts.base !== undefined && opts.fast !== true) throw new Error('--base names the ref --fast compares with; it needs --fast');
|
|
352
|
-
const checked = await checkEdition({ repoRoot, edition, normal: () => checkApp({ repoRoot, fast: opts.fast === true, base: opts.base, presets, prettier, dbTypes: opts['db-types'] === true }), presets, prettier });
|
|
352
|
+
const checked = await checkEdition({ repoRoot, edition, normal: () => checkApp({ repoRoot, harness, fast: opts.fast === true, base: opts.base, presets, prettier, dbTypes: opts['db-types'] === true }), presets, prettier });
|
|
353
353
|
const proof = withCheckProofs(checked, { repoRoot, edition, wiring: opts.wiring === true, coverageFrom: opts['coverage-from'] });
|
|
354
354
|
const changed = opts.fast === true ? changedSince(repoRoot, proof.fast?.base ?? opts.base).files : null;
|
|
355
355
|
const existing = changed ? changed.filter(file => fs.existsSync(path.join(repoRoot, file))) : [];
|
|
@@ -397,10 +397,10 @@ function addMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
|
397
397
|
return 0;
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
async function scaffoldMain(opts, { cwd, edition = 'full', presets, lock, stdout: out }) {
|
|
400
|
+
async function scaffoldMain(opts, { cwd, harness, edition = 'full', presets, lock, stdout: out }) {
|
|
401
401
|
const [name, ...extra] = opts.positional;
|
|
402
402
|
if (!name || extra.length) throw new Error('starci app scaffold takes `<name> [--into <dir>] [--edition full|lite] [--registry-visibility public|private]`');
|
|
403
|
-
const { root: created, files } = scaffoldApp({ name, into: path.resolve(cwd, opts.into ?? '.'), edition, presets: presets ?? scaffoldPresets(), ...(opts['registry-visibility'] === undefined ? {} : { registryVisibility: opts['registry-visibility'] }), ...(lock ? { lock } : {}) });
|
|
403
|
+
const { root: created, files } = scaffoldApp({ name, into: path.resolve(cwd, opts.into ?? '.'), harness, edition, presets: presets ?? scaffoldPresets(), ...(opts['registry-visibility'] === undefined ? {} : { registryVisibility: opts['registry-visibility'] }), ...(lock ? { lock } : {}) });
|
|
404
404
|
out(`starci app scaffold: created ${created} (${files.length} files); next: starci npm ci (the lockfile, then the git hooks through npm run prepare; after a raw npm ci --ignore-scripts run npm run prepare), then starci app lint\n`);
|
|
405
405
|
return 0;
|
|
406
406
|
}
|
|
@@ -449,9 +449,13 @@ function errorMessage(error) {
|
|
|
449
449
|
return `starci app: ${error.message}\n${USAGE}`;
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
-
/**
|
|
452
|
+
/**
|
|
453
|
+
* `harness` ({ version }) is the CLI that runs this package in-process: its version is the starci-harness an app is judged and
|
|
454
|
+
* scaffolded at (runtime/scripts/hfs/canon-pins.mjs). `run` injects the Supabase runner, `runLinter` the linter transport, `checkApp`
|
|
455
|
+
* the HFS check and `stackIo` Swarm tools; production defaults execute every check.
|
|
456
|
+
*/
|
|
453
457
|
export async function main(argv, {
|
|
454
|
-
cwd: baseCwd = process.cwd(), stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain,
|
|
458
|
+
cwd: baseCwd = process.cwd(), harness, stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain,
|
|
455
459
|
env = process.env, stdin, sops, tools, platform, stackIo, runLinter, checkApp,
|
|
456
460
|
} = {}) {
|
|
457
461
|
try {
|
|
@@ -460,7 +464,7 @@ export async function main(argv, {
|
|
|
460
464
|
const out = global.quiet ? () => {} : stdout;
|
|
461
465
|
if (global.help) { out(USAGE); return 0; }
|
|
462
466
|
if (!VERBS.includes(verb)) { stderr(USAGE); return 2; }
|
|
463
|
-
return await dispatchMain(verb, rest, { cwd: global.cwd, edition: global.edition, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, sops, tools, platform, stackIo, runLinter, checkApp });
|
|
467
|
+
return await dispatchMain(verb, rest, { cwd: global.cwd, harness, edition: global.edition, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, sops, tools, platform, stackIo, runLinter, checkApp });
|
|
464
468
|
} catch (error) {
|
|
465
469
|
stderr(errorMessage(error));
|
|
466
470
|
return 2;
|
package/upgrade/index.mjs
CHANGED
|
@@ -7,20 +7,12 @@
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { remember, ensureParent, writeFile, restore } from "./file-state.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { bundledPins } from "../runtime/scripts/hfs/canon-pins.mjs";
|
|
11
11
|
import { loadSlotManifest } from "../runtime/scripts/hfs/slots.mjs";
|
|
12
12
|
import { LOCK_STEP, npmLock } from "../scaffold/app.mjs";
|
|
13
13
|
import { WORLD_MARKER, testWorldFiles } from "../scaffold/test-world.mjs";
|
|
14
14
|
import { loadHfs } from "../sync/index.mjs";
|
|
15
15
|
|
|
16
|
-
const PACKAGE_ROOT = path.join(import.meta.dirname, "..");
|
|
17
|
-
const PINS_FILE = path.join(
|
|
18
|
-
PACKAGE_ROOT,
|
|
19
|
-
"runtime",
|
|
20
|
-
"knowledge",
|
|
21
|
-
"hfs",
|
|
22
|
-
"canon-pins.yaml",
|
|
23
|
-
);
|
|
24
16
|
const FULL_DEV_DEPENDENCIES = Object.freeze([
|
|
25
17
|
"@nestjs/testing",
|
|
26
18
|
"@starci/jest-preset",
|
|
@@ -111,7 +103,7 @@ function fullCompletion(root, declaration) {
|
|
|
111
103
|
(name) => current.devDependencies?.[name] === undefined && current.dependencies?.[name] === undefined,
|
|
112
104
|
);
|
|
113
105
|
if (missing.length) {
|
|
114
|
-
const pins =
|
|
106
|
+
const pins = bundledPins();
|
|
115
107
|
const added = pinnedPackage(root, pins).devDependencies;
|
|
116
108
|
const devDependencies = Object.fromEntries(
|
|
117
109
|
Object.entries({ ...current.devDependencies, ...Object.fromEntries(missing.map((name) => [name, added[name]])) })
|