From feec00334d19527b5ae90cdb4ab618ebcdd8bcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Thu, 23 Apr 2026 08:22:25 +0100 Subject: [PATCH 1/6] ci.yml: always emit Build & Test status (skip heavy work for Release PRs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Required status checks on main were blocking Release PRs from merging. Previous pattern: 'if:' skip of the whole job — GitHub never reports a status, required check waits forever. New pattern: job always runs, steps conditionally skip based on head ref. Release PRs merge cleanly. Also: - check-imports: REVERT warn-only; restore to fatal throw. Relative imports MUST have .js extensions for native-ESM-compatible output (per review). - setup-publish: stop gitignoring src/**/*.d.ts (hand-written type declarations like colors.d.ts are legit src files; only .js.map is strictly tsc output). --- .../dual-branch/github/workflows/ci.yml | 18 ++++++++++++++++-- defaults/setup-publish/github/workflows/ci.yml | 17 +++++++++++++++-- src/cli-methods.ts | 10 +++++----- src/commands/setup-publish.ts | 6 +++--- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/defaults/setup-publish/dual-branch/github/workflows/ci.yml b/defaults/setup-publish/dual-branch/github/workflows/ci.yml index 55b8714..711e8d1 100644 --- a/defaults/setup-publish/dual-branch/github/workflows/ci.yml +++ b/defaults/setup-publish/dual-branch/github/workflows/ci.yml @@ -9,22 +9,36 @@ on: jobs: build-and-test: name: Build & Test - if: ${{ !(github.event.pull_request.base.ref == 'main' && github.head_ref == 'changeset-release/main') }} runs-on: ubuntu-latest steps: + - name: Detect Release PR + id: rpr + run: | + if [[ "${{ github.event.pull_request.head.ref }}" == "changeset-release/main" ]]; then + echo "is_release_pr=true" >> $GITHUB_OUTPUT + echo "Release PR from changesets/action detected — skipping build/test (auto-generated version+CHANGELOG only)." + else + echo "is_release_pr=false" >> $GITHUB_OUTPUT + fi + - name: Checkout + if: steps.rpr.outputs.is_release_pr == 'false' uses: actions/checkout@v4 - name: Setup Node.js + if: steps.rpr.outputs.is_release_pr == 'false' uses: actions/setup-node@v4 with: node-version: 22.16.0 - name: Install dependencies + if: steps.rpr.outputs.is_release_pr == 'false' run: npm ci --legacy-peer-deps - name: Build + if: steps.rpr.outputs.is_release_pr == 'false' run: npm run build - name: Test - run: npm test + if: steps.rpr.outputs.is_release_pr == 'false' + run: npm test --if-present diff --git a/defaults/setup-publish/github/workflows/ci.yml b/defaults/setup-publish/github/workflows/ci.yml index 99a122b..9ed78bd 100644 --- a/defaults/setup-publish/github/workflows/ci.yml +++ b/defaults/setup-publish/github/workflows/ci.yml @@ -8,23 +8,36 @@ on: jobs: build-and-test: name: Build & Test - # Skip CI on the automated Release PR created by changesets/action. - if: ${{ !(github.event.pull_request.base.ref == 'main' && github.head_ref == 'changeset-release/main') }} runs-on: ubuntu-latest steps: + - name: Detect Release PR + id: rpr + run: | + if [[ "${{ github.event.pull_request.head.ref }}" == "changeset-release/main" ]]; then + echo "is_release_pr=true" >> $GITHUB_OUTPUT + echo "Release PR from changesets/action detected — skipping build/test (auto-generated version+CHANGELOG only)." + else + echo "is_release_pr=false" >> $GITHUB_OUTPUT + fi + - name: Checkout + if: steps.rpr.outputs.is_release_pr == 'false' uses: actions/checkout@v4 - name: Setup Node.js + if: steps.rpr.outputs.is_release_pr == 'false' uses: actions/setup-node@v4 with: node-version: 22.16.0 - name: Install dependencies + if: steps.rpr.outputs.is_release_pr == 'false' run: npm ci --legacy-peer-deps - name: Build + if: steps.rpr.outputs.is_release_pr == 'false' run: npm run build - name: Test + if: steps.rpr.outputs.is_release_pr == 'false' run: npm test --if-present diff --git a/src/cli-methods.ts b/src/cli-methods.ts index 538d59b..230b2da 100644 --- a/src/cli-methods.ts +++ b/src/cli-methods.ts @@ -1468,7 +1468,7 @@ export const checkImports = async ( let flat = [...invalidImports.values()].flat(); // All recursion must have finished, display any errors if (depth === 0 && flat.length > 0) { - res += chalk.yellow('Import warnings (non-fatal):\n'); + res += chalk.red('Invalid imports found.\n'); invalidImports.forEach((value, key) => { // res += '- '+chalk.blueBright(key.split('/').pop()) + ':\n'; @@ -1486,13 +1486,13 @@ export const checkImports = async ( message += ' which should end with a file extension. Like .js or .scss'; } - res += chalk.yellow(message + '\n'); + res += chalk.red(message + '\n'); }); }); - // Return as a string so buildStep treats this as a warning (build continues) - // rather than throwing and aborting the compile pipeline. - return res; + // Throw so buildStep aborts the compile pipeline — relative imports MUST + // have .js (or .scss) extensions for native-ESM-compatible output. + throw res; } else if (depth === 0 && invalidImports.size === 0) { // console.info('All imports OK'); // process.exit(0); diff --git a/src/commands/setup-publish.ts b/src/commands/setup-publish.ts index b8010b2..e5012da 100644 --- a/src/commands/setup-publish.ts +++ b/src/commands/setup-publish.ts @@ -156,9 +156,9 @@ async function updateGitignore(cwd: string): Promise { '*.log', '.DS_Store', '', - '# Compiled artifacts from tsc should live in lib/, never under src/.', - '# (.js alone is NOT ignored — some packages have legit .js test helpers/config.)', - 'src/**/*.d.ts', + '# tsc sourcemap output — always in lib/, never src/.', + '# (.d.ts not ignored — hand-written type declarations like colors.d.ts', + '# are legit src files.)', 'src/**/*.js.map', ]; const existing = fs.existsSync(gitignorePath) From 701e575dcf281f3fdbc6bca6bc9514d0423a828d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Thu, 23 Apr 2026 08:33:17 +0100 Subject: [PATCH 2/6] docs: rewrite README for @_linked/cli (bins, commands, templates) --- README.md | 107 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f450b45..6bb7913 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,111 @@ -#LINCD CLI -Command Line Interface for the [lincd.js](https://www.lincd.org) library +# @_linked/cli -### Installation +Command-line tools for the `@_linked/*` packages and apps. +## Install + +```bash +npm install --save-dev @_linked/cli +# or +yarn add -D @_linked/cli ``` -npm install lincd-cli + +## Binaries + +Three executables ship in this package: + +- `linked` — primary command +- `lnk` — short alias for `linked` +- `lincd` — deprecated alias; prints a warning and forwards to `linked`. Will be removed in a future major release. + +## Commands + +Run `linked --help` for the full list. The commonly used ones: + +### App scaffolding + +```bash +linked create-app # scaffold a new app (interactive) +linked create-package # scaffold a new linkedPackage +linked create-shape # add a shape file to the current package +linked create-component # add a React component file +``` + +### Building + +```bash +linked build # build the current package (tsc + checks) +linked build-app # build frontend + backend for the current app +linked build-workspace # build all linked packages in the workspace in dependency order +linked build-updated # incremental: only packages that changed since last build +linked build-package # walk up from a file path to find its package and rebuild +``` + +### Publishing / release + +```bash +linked setup-publish # install a changesets-based publish workflow in the current repo +linked setup-publish --dual-branch # use main + dev with @next prereleases +linked setup-publish --configure-github # also set branch protection via gh CLI +linked setup-publish --scope community # use NPM_AUTH_TOKEN_CM instead of NPM_AUTH_TOKEN ``` -or +`setup-publish` writes: + +- `.github/workflows/ci.yml`, `publish.yml`, `changeset-check.yml` +- `.changeset/config.json` + `README.md` +- `.gitignore` entries +- `publishConfig: {access: public}` + `@changesets/cli` devDeps in `package.json` +- `package-lock.json` (via isolated tmpdir) +### Dev workflow + +```bash +linked start # run the dev server (app) +linked dev # file-watch rebuild (package) +linked yarn # safe-yarn: run yarn at root while preserving nested yarn.lock files ``` -yarn add lincd-cli + +### Registry / dev utilities + +```bash +linked publish # publish the current package (for non-CI flows) +linked register # register the package to the linked registry +linked status # show which packages need build/publish +linked depcheck # check for missing/unused deps ``` -### Usage +## Package flags -type +The CLI recognizes two flags in `package.json`: +```json +{ + "linkedPackage": true, // marks a reusable library; build-workspace builds it + "linkedApp": true // marks a deployable app; build-workspace skips it +} ``` -npm exec lincd help + +The legacy `lincd: true` and `lincdApp: true` flags are still read for the transition period. + +## Development + +```bash +cd packages/cli +yarn build ``` -for available commands +Dual ESM + CJS build via `tsconfig-to-dual-package`. Sources in `src/`, output in `lib/esm/` and `lib/cjs/`. + +### Templates + +Templates live in `defaults/`: + +- `defaults/app-with-backend/` — used by `linked create-app` +- `defaults/app-static/` — minimal static app +- `defaults/package/` — used by `linked create-package` +- `defaults/setup-publish/` — workflow + changeset files written by `linked setup-publish` (single-branch default; `dual-branch/` subdirectory for the `--dual-branch` variant) + +## Repository + +`linked-cm/cli` on GitHub. License: MPL-2.0. From d72cd72dba77890e4d28cfca601a02b9b50e8b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Thu, 23 Apr 2026 09:25:13 +0100 Subject: [PATCH 3/6] linked build: refuse to build non-linkedPackage; setup-publish: --grant-team option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 'linked build' now early-errors if package.json lacks 'linkedPackage: true'. linkedApp gets a pointer to 'linked build-app' instead; packages with no flag get a clear message. - setup-publish: new --grant-team option. Calls gh api to PUT team push-permission on the repo. Handy for core packages so maintainers (semantu-devs) get access without per-repo invites. - Team slug is variable, so community-package promotion flows can use a different team if desired (or omit entirely — community repos are maintainer-owned). --- src/cli-methods.ts | 25 ++++++++++++++++++++ src/cli.ts | 5 ++++ src/commands/setup-publish.ts | 43 +++++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/cli-methods.ts b/src/cli-methods.ts index 230b2da..33d4a63 100644 --- a/src/cli-methods.ts +++ b/src/cli-methods.ts @@ -2405,6 +2405,31 @@ export const buildPackage = async ( // Always use the resolved absolute path packagePath = currentPath; + // Guard: `linked build` only makes sense for linkedPackage. Apps must use + // `linked build-app`. Plain packages (no linked flag) have no build pipeline + // defined here and should have their own build script. + const pkgJson = JSON.parse( + fs.readFileSync(path.join(packagePath, 'package.json'), 'utf8'), + ); + const isPackage = pkgJson.linkedPackage === true || pkgJson.lincd === true; + const isApp = pkgJson.linkedApp === true || pkgJson.lincdApp === true; + if (!isPackage) { + if (isApp) { + console.error( + chalk.red( + `'${pkgJson.name || packagePath}' is a linkedApp, not a linkedPackage. Use 'linked build-app' instead of 'linked build'.`, + ), + ); + } else { + console.error( + chalk.red( + `'${pkgJson.name || packagePath}' does not have 'linkedPackage: true' in package.json. 'linked build' only builds linked packages. Add the flag or run a different build command.`, + ), + ); + } + return false; + } + let spinner: Ora; if (logResults) { //TODO: replace with listr so we can show multiple processes at once diff --git a/src/cli.ts b/src/cli.ts index 3f60a0f..4cbeaa5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -357,12 +357,17 @@ program 'Which NPM secret to reference in the publish workflow: "core" uses NPM_AUTH_TOKEN, "community" uses NPM_AUTH_TOKEN_CM. Defaults to "core".', 'core', ) + .option( + '--grant-team ', + 'GitHub team slug to grant push access (e.g. "semantu-devs"). Requires gh CLI.', + ) .action(async (options) => { const {setupPublish} = await import('./commands/setup-publish.js'); await setupPublish({ configureGithub: !!options.configureGithub, dualBranch: !!options.dualBranch, scope: options.scope === 'community' ? 'community' : 'core', + grantTeam: options.grantTeam, }); }); diff --git a/src/commands/setup-publish.ts b/src/commands/setup-publish.ts index e5012da..a8cd724 100644 --- a/src/commands/setup-publish.ts +++ b/src/commands/setup-publish.ts @@ -18,6 +18,7 @@ export type SetupPublishOptions = { configureGithub?: boolean; scope?: 'core' | 'community'; // which NPM secret name to use dualBranch?: boolean; // main + dev with `@next` prereleases on dev + grantTeam?: string; // GitHub team slug to grant push access (e.g. 'semantu-devs') }; /** @@ -82,8 +83,13 @@ export async function setupPublish(opts: SetupPublishOptions = {}): Promise { @@ -299,7 +305,40 @@ async function configureGithub(repoSlug: string): Promise { } } -function printNextSteps(repoSlug: string, npmSecretName: string, configuredGithub: boolean | undefined): void { +async function grantTeamAccess(repoSlug: string, teamSlug: string): Promise { + console.log(''); + console.log(chalk.magenta(`Granting '${teamSlug}' team push access to ${repoSlug}...`)); + + try { + await execPromise('gh --version', false, false); + } catch { + console.warn(chalk.yellow(" ⚠ `gh` CLI not found. Install from https://cli.github.com/ and retry with --grant-team,")); + console.warn(chalk.yellow(` or add the team manually at https://github.com/${repoSlug}/settings/access`)); + return; + } + + const [owner, repo] = repoSlug.split('/'); + try { + await execPromise( + `gh api -X PUT /orgs/${owner}/teams/${teamSlug}/repos/${owner}/${repo} -f permission=push`, + false, + false, + ); + console.log(chalk.green(' ✓') + ` team '${teamSlug}' granted push access on ${repoSlug}`); + } catch (err: any) { + const msg = err?.stderr || err?.stdout || String(err); + console.warn(chalk.yellow(` ⚠ Failed to grant team access: ${msg.slice(0, 200)}`)); + console.warn(chalk.yellow(` Team may not exist in org '${owner}', or you may lack admin rights.`)); + console.warn(chalk.yellow(` Manual: https://github.com/${repoSlug}/settings/access`)); + } +} + +function printNextSteps( + repoSlug: string, + npmSecretName: string, + configuredGithub: boolean | undefined, + grantedTeam: string | undefined, +): void { console.log(''); console.log(chalk.green('Done.')); console.log(''); From 4b86e8caa88856ae20f58b93f67c3b96f76f3a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Thu, 23 Apr 2026 09:26:33 +0100 Subject: [PATCH 4/6] setup-publish: drop example team name from code comments (generic in OSS cli) --- src/commands/setup-publish.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/setup-publish.ts b/src/commands/setup-publish.ts index a8cd724..695a377 100644 --- a/src/commands/setup-publish.ts +++ b/src/commands/setup-publish.ts @@ -18,7 +18,7 @@ export type SetupPublishOptions = { configureGithub?: boolean; scope?: 'core' | 'community'; // which NPM secret name to use dualBranch?: boolean; // main + dev with `@next` prereleases on dev - grantTeam?: string; // GitHub team slug to grant push access (e.g. 'semantu-devs') + grantTeam?: string; // GitHub team slug to grant push access on the repo }; /** From 816ff1c72ae20e7932617073d721f32360171b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Thu, 23 Apr 2026 09:27:26 +0100 Subject: [PATCH 5/6] setup-publish: drop example team name from cli.ts help text too --- src/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index 4cbeaa5..518781b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -359,7 +359,7 @@ program ) .option( '--grant-team ', - 'GitHub team slug to grant push access (e.g. "semantu-devs"). Requires gh CLI.', + 'GitHub team slug to grant push access on the repo. Requires gh CLI.', ) .action(async (options) => { const {setupPublish} = await import('./commands/setup-publish.js'); From eb1224ea65ae65d7f534923b286d5daa0cdc151d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Thu, 23 Apr 2026 09:35:02 +0100 Subject: [PATCH 6/6] fix: remove prepack/postpack/postinstall scripts that broke CI publish auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prepack script 'yarn build && pinst --disable' ran during 'npm publish' and caused ENEEDAUTH — the publish couldn't authenticate against the registry. CI already runs the Build step explicitly, so prepack is redundant. Removed prepack/postpack/postinstall; bumped to 1.3.1. --- .changeset/fix-publish-auth.md | 5 +++++ package.json | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-publish-auth.md diff --git a/.changeset/fix-publish-auth.md b/.changeset/fix-publish-auth.md new file mode 100644 index 0000000..828ed5b --- /dev/null +++ b/.changeset/fix-publish-auth.md @@ -0,0 +1,5 @@ +--- +'@_linked/cli': patch +--- + +Remove `prepack: yarn build && pinst --disable` and `postpack: pinst --enable` scripts. These were conflicting with the CI publish flow (ENEEDAUTH on the actual `npm publish` call). Build now happens only in the dedicated CI "Build" step. Also remove `postinstall: husky install` (not needed for published installs). diff --git a/package.json b/package.json index 3fe4d2d..f4e0188 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@_linked/cli", - "version": "1.2.11", + "version": "1.3.1", "description": "Command line tools for the @_linked/* packages and apps", "main": "lib/cjs/index.js", "module": "lib/esm/index.js", @@ -38,9 +38,6 @@ "copy-to-lib": "echo '💫 Copying CSS assets to lib folder' && yarn copyfiles -u 1 'src/**/*.css' lib/esm && yarn copyfiles -u 1 'src/**/*.css' lib/cjs", "dev": "yarn tsc -p tsconfig-esm.json -w", "dev-cjs": "yarn tsc -p tsconfig-cjs.json -w", - "postinstall": "husky install", - "prepack": "yarn build && pinst --disable", - "postpack": "pinst --enable", "prettier": "prettier \"src/**/*.{js,jsx,ts,tsx,css,scss}\" --check", "prettier:fix": "yarn prettier --write", "format": "yarn prettier:fix && yarn lint:fix"