Skip to content

Commit d89479d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16025-hono-auth-mount-basepath
2 parents 27c034f + 0ea5f9d commit d89479d

21 files changed

Lines changed: 1627 additions & 90 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`flow-template-unknown-field` and `flow-template-lookup-traversal` now reach a `{record.<field>}` template that sits outside a node filter — the `warning` half both rules already declared, and never emitted.
6+
7+
A `{record.<field>}` token in a filter has always been reported as an `error`: an unresolved token there erases the condition and the CRUD node refuses to run. A token anywhere else — a message body, an http request payload, a created row's field values — is the quiet failure the rules were written for: it renders as an empty string on every run, and nothing reports it at either end — no build-time finding, no run-time error — so a hand-off payload naming a renamed field ships an empty value and the run is recorded as a success. That half was silent.
8+
9+
The cause was one key, in the shared flow walk rather than in either rule. A rule that scans a node's config recursively has to read a view of it with the nested regions removed, or it reports every finding inside a `loop` / `try_catch` / `parallel` a second time against the container. That view was built by removing every key that holds a region on *any* node type — and `body` is `loop`'s region slot **and** the canonical request-payload key on an `http` node. So `config.body` was deleted from every node's view before any rule read it, and the whole of an http payload was invisible. The view now removes only the slots the node's own type declares, which is exactly the set the walk descended into: nothing is double-reported, and nothing that was never a region is dropped.
10+
11+
Expect new `warning` findings on flows that publish clean today. Each one names a token that renders empty at run time; `warning` does not change `os validate`'s exit code, so a build that passed still passes.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"create-objectstack": minor
3+
---
4+
5+
Scaffolded projects now install the AI skills bundle for **one** agent runtime
6+
instead of every runtime the skills CLI knows, so the bundle is committed once.
7+
8+
**Route B of the two the card offered was taken**, and the choice was measured
9+
rather than argued. Against `skills@1.5.23` and the 11-skill catalog, the old
10+
`--all` (shorthand for `--skill '*' --agent '*' -y`) wrote the same bundle to
11+
three destinations — `.agents/` (46 real files, 604,102 B), `agent/` (46 real
12+
files, 602,682 B, identical bodies with re-serialised frontmatter) and
13+
`.claude/` (11 symlinks into `.agents/`). The template's `.gitignore` excluded
14+
none of it, so a new project's first `git add -A` staged 22 `SKILL.md` paths
15+
plus 11 symlinks. That reached the initial commit of a real app before anyone
16+
noticed.
17+
18+
The scaffolder now runs
19+
`npx skills add objectstack-ai/objectstack/skills --skill '*' --agent claude-code -y`,
20+
which writes 46 real files to `.claude/skills/` and nothing else: 11 staged
21+
`SKILL.md` paths, no symlinks, and a clone of that commit has readable skill
22+
files on every platform.
23+
24+
Route A (keep `--all`, exclude the duplicates in the template `_gitignore`) was
25+
built and cloned, not reasoned about, and both of its shapes were rejected.
26+
Ignoring `.agents/` and `agent/` while committing `.claude/` gives a fresh
27+
cloner 11 dangling symlinks and zero readable `SKILL.md`. Ignoring only
28+
`agent/` works on POSIX but commits 11 symlinks that a `core.symlinks=false`
29+
clone — git-for-Windows' default — materialises as ordinary files whose whole
30+
content is the link target. `--all --copy`, the other way to make `.claude/`
31+
real, fans out to 56 destination directories totalling 33.8 MB. A denylist is
32+
also the wrong shape regardless of which paths it names: this package does not
33+
choose the destination set, the skills CLI does, and it moves with that
34+
package's releases.
35+
36+
The cost is the multi-runtime default, and it is paid in the open: the closing
37+
summary now always prints an **AI Skills** block naming where the bundle landed
38+
and the one-line command for any other runtime, one agent at a time. The
39+
bundle is identical whichever agent is named.
40+
41+
Existing projects are unaffected. To shrink one that already carries the
42+
triplicate, delete `.agents/` and `agent/` and re-run the single-agent command
43+
above; `skills-lock.json` records source and hash, not paths, so it does not
44+
change.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`visibility-bare-identifier` now reports an identifier written bare beside a `has()` guard in the same visibility predicate.
6+
7+
`has(status) && status == "qualified"` published clean while `status == "qualified"` — the same defect, without the guard — gated at `error`. The guarded spelling is the one the totality discipline pushes authors toward, so an author who correctly adds `has()` and forgets the `record.` prefix on both halves landed in the silent row. That predicate never evaluates for any record, and an unevaluable `visibleWhen` on a form surface fails OPEN: the field renders and carries its `required: true` into the console's submit check.
8+
9+
The cause was not the exclusion a `has()` argument earns — that is correct and stays. `firstUndeclaredReference` reads the first error the CEL checker reports and acts only on `Unknown variable: X`; a bare `has(x)` fails that check with `has() invalid argument` instead, and a first error of a different class masked every undeclared reference behind it in the same predicate, whatever it was called. Each `has(…)` call is now masked out of the source before the checker sees it, using the canonical AST's own spans, so the argument occurrence is excluded and every other occurrence is judged exactly as it would be with no guard written beside it.
10+
11+
Expect new `error` findings on predicates that used to publish clean: a guarded-but-unprefixed `visibleWhen` on a view, page component or form section is now refused at build, validate and lint alike. That is the fail-open shape the rule exists to catch. A `has()` argument that is the only bare occurrence — `has(status)` on its own — stays silent, as it did before.

packages/cli/src/commands/generate-field-type-vocabulary.pin.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,8 +512,10 @@ describe('#14828 — the SQL answers are the platform’s, not this file’s inv
512512
expect(
513513
tsColumn(type),
514514
`os generate migration (typescript) gave a ${type} column something other than a string ` +
515-
'column. A platform id is 26 characters (driver-sql spells one out in its lookup arm), ' +
516-
'so a `uuid` column refuses it outright on Postgres with 22P02.',
515+
'column. A platform id is not a uuid, and its width is not a fixed number (driver-sql\'s ' +
516+
'lookup arm states both: it mints a 16-character nanoid, and stores a supplied id at ' +
517+
'whatever width the caller chose), so a `uuid` column refuses it outright on Postgres ' +
518+
'with 22P02.',
517519
).toBe(`table.string('f_${type}')`);
518520
}
519521
// The width is knex's default for a bare `table.string(name)`, which is the

packages/cli/src/commands/generate-multiple-json-column.pin.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,9 @@ describe('#14829 — `multiple: true` is one answer across all three surfaces',
273273
it('#14828 discharged — the five disputed SCALAR answers are the platform’s', () => {
274274
// A reference column holds the target's `id`: `table.string(name)`, knex's
275275
// varchar(255). `table.uuid` was the one HARD failure of the five — a
276-
// platform id is 26 characters and Postgres refuses one in a `uuid` column.
276+
// platform id is not a uuid, and its width is not a fixed number (driver-sql
277+
// mints a 16-character nanoid, and stores a supplied id at whatever width
278+
// the caller chose), so Postgres refuses one in a `uuid` column.
277279
expect(sqlColumn('single_lookup')).toBe('VARCHAR(255)');
278280
expect(tsColumn('single_lookup')).toBe("table.string('single_lookup')");
279281

packages/cli/src/commands/generate.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -992,10 +992,13 @@ async function runClientGeneration(configPath: string | undefined, flags: { outp
992992
* `lookup` / VARCHAR(36) → VARCHAR(255), with the migration switch's
993993
* `master_detail` `table.uuid` corrected in the same breath. The
994994
* `uuid` half is the only HARD failure of the five: a platform
995-
* id is 26 characters (`createColumn`'s lookup arm says so and
996-
* spells one out — `01JQ8XKZ9M4N7P2R5T6V8W0Y3B`), and Postgres
997-
* refuses one in a `uuid` column with `22P02`. The width half
998-
* is the same rule for the whole REFERENCE_VALUE_TYPES class:
995+
* id is NOT a uuid, and its width is not a fixed number at all
996+
* (`createColumn`'s lookup arm states both): the driver mints a
997+
* 16-character nanoid when the caller supplies none, and stores
998+
* a SUPPLIED id verbatim at whatever width the caller chose.
999+
* Postgres refuses either in a `uuid` column with `22P02`. The
1000+
* width half is the same rule for the whole
1001+
* REFERENCE_VALUE_TYPES class:
9991002
* `user` and `tree` moved with them, because a reference column
10001003
* holds the TARGET's `id` — which the driver itself emits as
10011004
* `table.string('id').primary()`, i.e. `varchar(255)` — and
@@ -1886,10 +1889,12 @@ export function generateMigrationTs(config: Record<string, unknown>): string {
18861889
// answer: `createColumn`'s `case 'lookup': case 'user':` is
18871890
// `table.string(name)`, and `master_detail` reaches the same call
18881891
// through its catch-all. `table.uuid` was the one HARD failure among
1889-
// this card's five rows — a platform id is 26 characters
1890-
// (`01JQ8XKZ9M4N7P2R5T6V8W0Y3B`, spelled out in that same driver arm),
1891-
// and Postgres refuses one in a `uuid` column with `22P02 invalid
1892-
// input syntax for type uuid` on the very first insert.
1892+
// this card's five rows — a platform id is NOT a uuid, and its width
1893+
// is not a fixed number at all: that same driver arm mints a
1894+
// 16-character nanoid when the caller supplies no id, and stores a
1895+
// SUPPLIED one verbatim at whatever width the caller chose. Postgres
1896+
// refuses either in a `uuid` column with `22P02 invalid input syntax
1897+
// for type uuid` on the very first insert.
18931898
case 'lookup': case 'master_detail':
18941899
case 'user': case 'tree':
18951900
case 'image': case 'file': case 'avatar': case 'video': case 'audio':

packages/create-objectstack/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,12 @@ you so rather than failing as an unknown template.
5656
every object name in the template is re-prefixed to match
5757
(`blank_note``my_app_note`).
5858
2. Installs dependencies (pnpm if available, otherwise npm).
59-
3. Installs the ObjectStack AI skills bundle for coding agents
60-
(`npx skills add objectstack-ai/objectstack/skills --all` — scoped to the
61-
curated `skills/` catalog).
59+
3. Installs the ObjectStack AI skills bundle for **one** coding agent —
60+
`npx skills add objectstack-ai/objectstack/skills --skill '*' --agent claude-code -y`,
61+
scoped to the curated `skills/` catalog. One agent, not `--all`, so the
62+
bundle lands in exactly one directory (`.claude/skills/`) and a scaffolded
63+
project commits it once; `src/skills-install.ts` carries the measurement.
64+
The closing summary prints the same command for any other runtime.
6265
4. Writes `AGENTS.md` and `.github/copilot-instructions.md` with the project
6366
conventions — unless the template ships its own.
6467

packages/create-objectstack/src/created-summary.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,18 @@
2828
//
2929
// 1. template copy + identity rewrite + AGENTS.md/copilot-instructions.md
3030
// 2. `<pm> install` -> pnpm-lock.yaml, node_modules/
31-
// 3. `npx skills add … --all` -> .agents/, agent/, .claude/,
32-
// skills-lock.json
31+
// 3. `npx skills add … --agent …` -> .claude/, skills-lock.json
3332
//
3433
// and the list was printed between (1) and (2). Phases 2 and 3 are third-party
3534
// processes whose outputs this package does not choose and cannot enumerate
36-
// ahead of time — the `skills` CLI fans out to every agent runtime it knows,
37-
// and that set changes with ITS releases, not ours. So any hand-maintained
38-
// list is not merely incomplete, it is unmaintainable: it drifts the next time
39-
// a dependency learns a new destination, silently, in the one direction that
35+
// ahead of time. Phase 3's destination set was the sharpest case: the run
36+
// measured above used `--all`, which fanned the catalog out to `.agents/`,
37+
// `agent/` AND `.claude/` — three trees, all committed, which is the defect
38+
// `skills-install.ts` now closes by naming one agent. Naming it does not make
39+
// the destination OURS to enumerate: the path is still the skills CLI's
40+
// choice, and it moves with ITS releases. So any hand-maintained list is not
41+
// merely incomplete, it is unmaintainable: it drifts the next time a
42+
// dependency learns a new destination, silently, in the one direction that
4043
// hides files rather than inventing them.
4144
//
4245
// Reading the directory afterwards is what makes the summary self-correcting.

packages/create-objectstack/src/index.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@
3939
* `--skip-install` there is no resolved version, so the template keeps `latest`
4040
* and its comment keeps telling the reader to pin by hand — true in that path.
4141
*
42-
* Finally we (best-effort) install the ObjectStack skills bundle via
43-
* `npx skills add objectstack-ai/objectstack/skills --all`.
44-
* The `/skills` subpath scopes discovery to the curated, customer-published
45-
* catalog — repo-internal skills (e.g. under `.claude/skills/`) must never
46-
* reach scaffolded projects.
42+
* Finally we (best-effort) install the ObjectStack skills bundle, for ONE
43+
* agent runtime (`skills-install.ts` carries the command and the measurement
44+
* behind that choice: `--all` writes the same bundle to three destinations and
45+
* the project then commits all three). The `/skills` subpath scopes discovery
46+
* to the curated, customer-published catalog — repo-internal skills (e.g.
47+
* under `.claude/skills/` in THIS repo) must never reach scaffolded projects.
4748
*
4849
* Only THEN is the "Created files" summary printed, and it is a walk of the
4950
* finished project directory rather than a list accumulated during the copy
@@ -72,6 +73,13 @@ import {
7273
import { lookupTemplate, templateNames } from './template-registry.js';
7374
import { readResolvedCliVersion, pinRuntimeImage } from './runtime-image.js';
7475
import { summarizeTree, describeEntry } from './created-summary.js';
76+
import {
77+
DEFAULT_SKILLS_AGENT,
78+
DEFAULT_SKILLS_DIR,
79+
EXAMPLE_OTHER_AGENT,
80+
SKILLS_INSTALL_COMMAND,
81+
skillsInstallHint,
82+
} from './skills-install.js';
7583
import { renderVersionBanner } from './banner.js';
7684

7785
const __filename = fileURLToPath(import.meta.url);
@@ -521,24 +529,26 @@ const program = new Command()
521529
}
522530

523531
// Which top-level paths belong to the skills install is measured, not
524-
// assumed: `skills add --all` fans the catalog out to every agent
525-
// runtime IT knows about (77 at the version measured), so the
526-
// destination set moves with that package's releases. Diffing the
527-
// directory across the call keeps the ⚠ marks correct without this file
528-
// carrying a list it cannot keep current.
532+
// assumed. Naming one agent makes the destination set small and
533+
// knowable (`skills-install.ts`), but it is still THEIR path to choose,
534+
// and it moves with that package's releases — so the ⚠ marks come from
535+
// diffing the directory across the call rather than from a list this
536+
// file would have to keep current.
529537
const beforeSkills = topLevelNames(targetDir);
538+
let skillsInstalled = false;
530539
if (!options.skipInstall && !options.skipSkills) {
531540
printStep('Installing AI skills for your coding agent...');
532541
try {
533-
execSync('npx -y skills add objectstack-ai/objectstack/skills --all', {
542+
execSync(SKILLS_INSTALL_COMMAND, {
534543
stdio: 'inherit',
535544
cwd: targetDir,
536545
});
546+
skillsInstalled = true;
537547
console.log('');
538548
} catch {
539549
printWarning(
540550
'Skills installation skipped. Run manually:\n' +
541-
' npx skills add objectstack-ai/objectstack/skills',
551+
` ${skillsInstallHint(DEFAULT_SKILLS_AGENT)}`,
542552
);
543553
console.log('');
544554
}
@@ -569,10 +579,29 @@ const program = new Command()
569579
console.log(chalk.dim(` ${devLabel.padEnd(labelWidth)}# Start development server`));
570580
console.log(chalk.dim(` ${validateLabel.padEnd(labelWidth)}# Verify metadata: schema + predicates + bindings`));
571581
console.log(chalk.dim(` ${' '.repeat(labelWidth)}# (run after every metadata edit — see AGENTS.md)`));
572-
if (options.skipInstall || options.skipSkills) {
573-
console.log('');
582+
// The skills block is printed on EVERY path, not only the skipped one.
583+
// The install now targets a single agent runtime, so a run that
584+
// succeeded still leaves every other runtime uninstalled — and a
585+
// capability that is absent has to say so, with the remedy, rather than
586+
// leave the reader to discover the gap. The command printed here is the
587+
// one this scaffolder ran, with a different agent name.
588+
console.log('');
589+
if (skillsInstalled) {
590+
console.log(chalk.bold(' AI Skills:'));
591+
console.log(
592+
chalk.dim(
593+
` Installed for ${DEFAULT_SKILLS_AGENT}${DEFAULT_SKILLS_DIR} — commit it with your project.`,
594+
),
595+
);
596+
console.log(chalk.dim(' Using a different agent? Add it one runtime at a time:'));
597+
console.log(chalk.dim(` ${skillsInstallHint(EXAMPLE_OTHER_AGENT)}`));
598+
console.log(chalk.dim(' (`npx skills add --help` lists every agent name)'));
599+
} else {
574600
console.log(chalk.bold(' AI Skills (recommended):'));
575-
console.log(chalk.dim(' npx skills add objectstack-ai/objectstack/skills'));
601+
console.log(chalk.dim(` ${skillsInstallHint(DEFAULT_SKILLS_AGENT)}`));
602+
console.log(
603+
chalk.dim(` Replace \`${DEFAULT_SKILLS_AGENT}\` with your own agent runtime if it differs.`),
604+
);
576605
}
577606
console.log('');
578607
} catch (error) {

0 commit comments

Comments
 (0)