Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-publish-auth.md
Original file line number Diff line number Diff line change
@@ -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).
107 changes: 97 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <name> # scaffold a new app (interactive)
linked create-package <name> # scaffold a new linkedPackage
linked create-shape <name> # add a shape file to the current package
linked create-component <name> # 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 <file> # 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 <args> # 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.
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
Expand Down
25 changes: 25 additions & 0 deletions src/cli-methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <slug>',
'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,
});
});

Expand Down
43 changes: 41 additions & 2 deletions src/commands/setup-publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

/**
Expand Down Expand Up @@ -82,8 +83,13 @@ export async function setupPublish(opts: SetupPublishOptions = {}): Promise<void
await configureGithub(repoSlug);
}

// 8. Optional: grant a GitHub team push access
if (opts.grantTeam) {
await grantTeamAccess(repoSlug, opts.grantTeam);
}

// Summary + manual steps
printNextSteps(repoSlug, npmSecretName, opts.configureGithub);
printNextSteps(repoSlug, npmSecretName, opts.configureGithub, opts.grantTeam);
}

async function resolveRepoSlug(cwd: string, pkgJson: any): Promise<string> {
Expand Down Expand Up @@ -299,7 +305,40 @@ async function configureGithub(repoSlug: string): Promise<void> {
}
}

function printNextSteps(repoSlug: string, npmSecretName: string, configuredGithub: boolean | undefined): void {
async function grantTeamAccess(repoSlug: string, teamSlug: string): Promise<void> {
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('');
Expand Down
Loading