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/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. diff --git a/package.json b/package.json index 1ae4e77..f4e0188 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@_linked/cli", - "version": "1.3.0", + "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" 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..518781b 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 on the repo. 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..695a377 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 on the repo }; /** @@ -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('');