pan-stack-cli 0.1.5 → 0.2.2

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/package.json CHANGED
@@ -1,19 +1,30 @@
1
1
  {
2
2
  "name": "pan-stack-cli",
3
- "version": "0.1.5",
3
+ "version": "0.2.2",
4
4
  "description": "PAN Stack CLI — multi-repo git and project commands from .project.yaml",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "pan": "bin/pan.js"
8
8
  },
9
9
  "files": [
10
- "bin",
11
- "lib",
12
- "templates"
10
+ "bin"
13
11
  ],
12
+ "scripts": {
13
+ "build": "node tools/pan-stack-cli/build.mjs",
14
+ "prepare": "node tools/pan-stack-cli/build.mjs",
15
+ "prepublishOnly": "node tools/pan-stack-cli/build.mjs",
16
+ "test": "node --test tools/pan-stack-plan/test/ tools/pan-stack-cli/test/",
17
+ "test:pan-stack-plan": "node --test tools/pan-stack-plan/test/",
18
+ "test:pan-stack-cli": "node --test tools/pan-stack-cli/test/"
19
+ },
14
20
  "engines": {
15
21
  "node": ">=18"
16
22
  },
23
+ "dependencies": {
24
+ "@inquirer/core": "^11.2.1",
25
+ "@inquirer/prompts": "^8.5.2",
26
+ "serve": "^14.2.4"
27
+ },
17
28
  "keywords": [
18
29
  "pan-stack",
19
30
  "monorepo",
@@ -24,5 +35,13 @@
24
35
  "type": "git",
25
36
  "url": "https://github.com/doiya46/pan-stack.git",
26
37
  "directory": ".pan/pan-stack-cli"
38
+ },
39
+ "devDependencies": {
40
+ "@inquirer/testing": "^3.3.9",
41
+ "csv-parse": "7.0.1",
42
+ "csv-stringify": "6.8.1",
43
+ "esbuild": "0.28.1",
44
+ "marked": "15.0.12",
45
+ "sanitize-html": "2.16.0"
27
46
  }
28
- }
47
+ }
package/lib/create.js DELETED
@@ -1,87 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const readline = require("readline");
4
- const { validateFolderName } = require("./platform");
5
-
6
- const TEMPLATE_ROOT = path.join(__dirname, "..", "templates", "workspace");
7
-
8
- function promptFolderName() {
9
- return new Promise((resolve, reject) => {
10
- if (!process.stdin.isTTY) {
11
- reject(new Error("Folder name required: pan create <folder-name>"));
12
- return;
13
- }
14
-
15
- const rl = readline.createInterface({
16
- input: process.stdin,
17
- output: process.stdout,
18
- });
19
-
20
- rl.question("Workspace folder name: ", (answer) => {
21
- rl.close();
22
- const name = answer.trim();
23
- if (!name) {
24
- reject(new Error("Folder name cannot be empty."));
25
- return;
26
- }
27
- resolve(name);
28
- });
29
- });
30
- }
31
-
32
- function copyDir(src, dest) {
33
- fs.mkdirSync(dest, { recursive: true });
34
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
35
- const srcPath = path.join(src, entry.name);
36
- const destPath = path.join(dest, entry.name);
37
- if (entry.isDirectory()) {
38
- copyDir(srcPath, destPath);
39
- } else {
40
- fs.copyFileSync(srcPath, destPath);
41
- }
42
- }
43
- }
44
-
45
- function applyPlaceholders(targetDir, folderName) {
46
- const files = [".project.yaml", "README.md"];
47
- for (const relativePath of files) {
48
- const filePath = path.join(targetDir, relativePath);
49
- if (!fs.existsSync(filePath)) {
50
- continue;
51
- }
52
- const content = fs.readFileSync(filePath, "utf8");
53
- fs.writeFileSync(filePath, content.replaceAll("__PROJECT_NAME__", folderName));
54
- }
55
- }
56
-
57
- async function runCreate(args = []) {
58
- let folderName = args[0]?.trim();
59
- if (!folderName) {
60
- folderName = await promptFolderName();
61
- }
62
-
63
- validateFolderName(folderName);
64
-
65
- const targetDir = path.resolve(process.cwd(), folderName);
66
- if (fs.existsSync(targetDir)) {
67
- throw new Error(`Folder already exists: ${targetDir}`);
68
- }
69
-
70
- if (!fs.existsSync(TEMPLATE_ROOT)) {
71
- throw new Error(`Template not found: ${TEMPLATE_ROOT}`);
72
- }
73
-
74
- copyDir(TEMPLATE_ROOT, targetDir);
75
- applyPlaceholders(targetDir, folderName);
76
-
77
- console.log(`Created PAN Stack workspace: ${targetDir}`);
78
- console.log("");
79
- console.log("Next steps:");
80
- console.log(` cd ${folderName}`);
81
- console.log(" # Edit .project.yaml with your repository URLs");
82
- console.log(" pan clone");
83
- }
84
-
85
- module.exports = {
86
- runCreate,
87
- };
package/lib/discover.js DELETED
@@ -1,236 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const os = require("os");
4
- const { splitEnvList } = require("./platform");
5
- const {
6
- PAN_CONFIG_FILE,
7
- loadManifest,
8
- loadPanConfig,
9
- getCommandDefinition,
10
- commandAliases,
11
- validateRepo,
12
- cleanValue,
13
- } = require("./manifest");
14
-
15
- const BUILTIN_GIT_COMMANDS = new Set([
16
- "clone",
17
- "pull",
18
- "sync-main",
19
- "sm",
20
- "fetch",
21
- "push",
22
- "grv",
23
- "gst",
24
- ]);
25
- const BUILTIN_WORKSPACE_COMMANDS = new Set(["create", ...BUILTIN_GIT_COMMANDS]);
26
-
27
- function readProjectRoots() {
28
- const roots = [];
29
- const fromEnv = process.env.PAN_PROJECT_ROOTS || "";
30
- for (const part of splitEnvList(fromEnv)) {
31
- roots.push(path.resolve(part));
32
- }
33
-
34
- const registry = path.join(os.homedir(), ".pan", "projects");
35
- if (fs.existsSync(registry)) {
36
- const content = fs.readFileSync(registry, "utf8");
37
- for (const line of content.split(/\r?\n/)) {
38
- const trimmed = line.trim();
39
- if (!trimmed || trimmed.startsWith("#")) {
40
- continue;
41
- }
42
- roots.push(path.resolve(trimmed));
43
- }
44
- }
45
-
46
- return [...new Set(roots)];
47
- }
48
-
49
- function manifestPathInDir(dir) {
50
- const candidate = path.join(dir, ".project.yaml");
51
- return fs.existsSync(candidate) ? candidate : null;
52
- }
53
-
54
- function walkUpManifests(startDir) {
55
- const found = [];
56
- let current = path.resolve(startDir);
57
- const seen = new Set();
58
-
59
- while (true) {
60
- const manifestPath = manifestPathInDir(current);
61
- if (manifestPath && !seen.has(manifestPath)) {
62
- seen.add(manifestPath);
63
- found.push(manifestPath);
64
- }
65
- const parent = path.dirname(current);
66
- if (parent === current) {
67
- break;
68
- }
69
- current = parent;
70
- }
71
-
72
- return found;
73
- }
74
-
75
- function findNearestProjectManifest(startDir = process.cwd()) {
76
- const walked = walkUpManifests(startDir);
77
- return walked[0] || null;
78
- }
79
-
80
- function findNearestWorkspace(startDir = process.cwd()) {
81
- const manifestPath = findNearestProjectManifest(startDir);
82
- if (!manifestPath) {
83
- return null;
84
- }
85
- return loadManifest(manifestPath);
86
- }
87
-
88
- function loadCommandSourcesForWorkspace(workspace) {
89
- const sources = [];
90
- const workspaceRoot = workspace.root;
91
- const workspacePan = path.join(workspaceRoot, PAN_CONFIG_FILE);
92
-
93
- if (fs.existsSync(workspacePan)) {
94
- sources.push(
95
- loadPanConfig(workspacePan, {
96
- workspaceRoot,
97
- project: workspace.project,
98
- repoName: workspace.project,
99
- scope: "workspace",
100
- })
101
- );
102
- }
103
-
104
- for (const repo of workspace.repositories) {
105
- try {
106
- const { name, repoPath } = validateRepo(repo);
107
- const repoDir = path.resolve(workspaceRoot, repoPath);
108
- const repoPan = path.join(repoDir, PAN_CONFIG_FILE);
109
- if (!fs.existsSync(repoPan)) {
110
- continue;
111
- }
112
- sources.push(
113
- loadPanConfig(repoPan, {
114
- workspaceRoot,
115
- project: name,
116
- repoName: name,
117
- scope: "repo",
118
- })
119
- );
120
- } catch {
121
- // skip invalid repo entries
122
- }
123
- }
124
-
125
- return sources;
126
- }
127
-
128
- function collectWorkspaces(startDir = process.cwd()) {
129
- const workspaces = new Map();
130
-
131
- for (const manifestPath of walkUpManifests(startDir)) {
132
- try {
133
- const workspace = loadManifest(manifestPath);
134
- workspaces.set(workspace.root, workspace);
135
- } catch {
136
- // ignore
137
- }
138
- }
139
-
140
- for (const root of readProjectRoots()) {
141
- const manifestPath = manifestPathInDir(root);
142
- if (!manifestPath) {
143
- continue;
144
- }
145
- try {
146
- const workspace = loadManifest(manifestPath);
147
- workspaces.set(workspace.root, workspace);
148
- } catch {
149
- // ignore
150
- }
151
- }
152
-
153
- return [...workspaces.values()];
154
- }
155
-
156
- function loadAllCommandSources(startDir = process.cwd()) {
157
- const sources = [];
158
- const seen = new Set();
159
-
160
- for (const workspace of collectWorkspaces(startDir)) {
161
- for (const source of loadCommandSourcesForWorkspace(workspace)) {
162
- const key = source.path;
163
- if (seen.has(key)) {
164
- continue;
165
- }
166
- seen.add(key);
167
- sources.push(source);
168
- }
169
- }
170
-
171
- return sources;
172
- }
173
-
174
- function sourceMatchesProject(source, projectFlag) {
175
- const flag = projectFlag.trim();
176
- return (
177
- source.project === flag ||
178
- source.repoName === flag ||
179
- path.basename(source.root) === flag
180
- );
181
- }
182
-
183
- function sourceHasCommand(source, commandName) {
184
- if (source.commands[commandName]) {
185
- return true;
186
- }
187
- return Object.values(source.commands || {}).some((def) =>
188
- commandAliases(def).includes(commandName)
189
- );
190
- }
191
-
192
- function findSourcesWithCommand(commandName, startDir = process.cwd()) {
193
- return loadAllCommandSources(startDir).filter((source) =>
194
- sourceHasCommand(source, commandName)
195
- );
196
- }
197
-
198
- function formatProjectList(matches, commandName) {
199
- const lines = [`Command '${commandName}' is defined in multiple places:`];
200
- matches.forEach((source, index) => {
201
- const def = source.commands[commandName];
202
- const desc = def?.description ? ` — ${def.description}` : "";
203
- const label =
204
- source.scope === "workspace"
205
- ? `${source.project} (workspace)`
206
- : source.repoName || source.project;
207
- lines.push(` ${index + 1}. ${label} (${source.root})${desc}`);
208
- });
209
- lines.push("");
210
- lines.push("Pick a number, or rerun with --project <repo-name>.");
211
- return lines.join("\n");
212
- }
213
-
214
- function isBuiltinGitCommand(name) {
215
- return BUILTIN_GIT_COMMANDS.has(name);
216
- }
217
-
218
- function isBuiltinWorkspaceCommand(name) {
219
- return BUILTIN_WORKSPACE_COMMANDS.has(name);
220
- }
221
-
222
- module.exports = {
223
- BUILTIN_GIT_COMMANDS,
224
- BUILTIN_WORKSPACE_COMMANDS,
225
- PAN_CONFIG_FILE,
226
- findNearestProjectManifest,
227
- findNearestWorkspace,
228
- loadAllCommandSources,
229
- loadCommandSourcesForWorkspace,
230
- findSourcesWithCommand,
231
- formatProjectList,
232
- isBuiltinGitCommand,
233
- isBuiltinWorkspaceCommand,
234
- getCommandDefinition,
235
- sourceMatchesProject,
236
- };
@@ -1,71 +0,0 @@
1
- #!/usr/bin/env node
2
- /*
3
- * Clone PAN Stack project repositories from a small clone manifest.
4
- *
5
- * The parser intentionally supports only the manifest shape documented in
6
- * PAN_STACK.md. It avoids external dependencies so the script can run anywhere
7
- * Node.js and Git are available.
8
- */
9
-
10
- const fs = require("fs");
11
- const path = require("path");
12
- const { spawnSync } = require("child_process");
13
- const { spawnOptions } = require("../platform");
14
- const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
15
-
16
- function cloneRepo({ name, url, destination, dryRun }) {
17
- if (fs.existsSync(destination)) {
18
- console.log(`[skip] ${name}: ${destination} already exists`);
19
- return;
20
- }
21
-
22
- fs.mkdirSync(path.dirname(destination), { recursive: true });
23
- const command = ["git", "clone", url, destination];
24
- if (dryRun) {
25
- console.log(`[dry-run] ${command.join(" ")}`);
26
- return;
27
- }
28
-
29
- console.log(`[clone] ${name}: ${url} -> ${destination}`);
30
- const result = spawnSync(command[0], command.slice(1), spawnOptions({ stdio: "inherit" }));
31
- if (result.status !== 0) {
32
- console.log(`[warn] ${name}: clone failed, check repo access or url`);
33
- }
34
- }
35
-
36
- function printHelp() {
37
- console.log(`usage: git-clone.js [--base-dir BASE_DIR] [--dry-run] [manifest]
38
-
39
- Clone repositories from a PAN Stack clone manifest.
40
-
41
- ${printCommonOptions()}`);
42
- }
43
-
44
- function main() {
45
- const args = parseCommonArgs(process.argv.slice(2));
46
- if (args.help) {
47
- printHelp();
48
- return;
49
- }
50
-
51
- const { repos } = loadManifestRepos({
52
- manifestPath: args.manifest,
53
- baseDir: args.baseDir,
54
- });
55
-
56
- for (const repo of repos) {
57
- cloneRepo({
58
- name: repo.name,
59
- url: repo.url,
60
- destination: repo.destination,
61
- dryRun: args.dryRun,
62
- });
63
- }
64
- }
65
-
66
- try {
67
- main();
68
- } catch (error) {
69
- console.error(error.message);
70
- process.exit(1);
71
- }
@@ -1,62 +0,0 @@
1
- #!/usr/bin/env node
2
- /*
3
- * Fetch all remotes for PAN Stack project repositories from a clone manifest.
4
- *
5
- * Skips repositories that do not exist or are not git checkouts. Failures warn
6
- * and continue so one broken repo does not block the rest.
7
- */
8
-
9
- const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
10
- const { ensureRepoExists, runGit } = require("./helpers/git");
11
-
12
- function fetchRepo({ name, destination, dryRun }) {
13
- if (!ensureRepoExists({ name, destination })) {
14
- return;
15
- }
16
-
17
- const status = runGit(["fetch", "--all"], {
18
- cwd: destination,
19
- dryRun,
20
- action: "fetch",
21
- name,
22
- });
23
- if (status !== 0) {
24
- console.log(`[warn] ${name}: fetch failed, check remote access`);
25
- }
26
- }
27
-
28
- function printHelp() {
29
- console.log(`usage: git-fetch-all.js [--base-dir BASE_DIR] [--dry-run] [manifest]
30
-
31
- Fetch all remotes for repositories listed in a PAN Stack clone manifest.
32
-
33
- ${printCommonOptions()}`);
34
- }
35
-
36
- function main() {
37
- const args = parseCommonArgs(process.argv.slice(2));
38
- if (args.help) {
39
- printHelp();
40
- return;
41
- }
42
-
43
- const { repos } = loadManifestRepos({
44
- manifestPath: args.manifest,
45
- baseDir: args.baseDir,
46
- });
47
-
48
- for (const repo of repos) {
49
- fetchRepo({
50
- name: repo.name,
51
- destination: repo.destination,
52
- dryRun: args.dryRun,
53
- });
54
- }
55
- }
56
-
57
- try {
58
- main();
59
- } catch (error) {
60
- console.error(error.message);
61
- process.exit(1);
62
- }
@@ -1,94 +0,0 @@
1
- #!/usr/bin/env node
2
- /*
3
- * List git remotes (git remote -v) for PAN Stack project repositories.
4
- *
5
- * oh-my-zsh-style alias for grv — full remote URLs (fetch/push).
6
- */
7
-
8
- const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
9
- const { ensureRepoExists, isGitRepo, listRemotesVerbose } = require("./helpers/git");
10
-
11
- function printRepoRemotes({ label, destination, dryRun }) {
12
- if (dryRun) {
13
- console.log(`[dry-run] (in ${destination}) git remote -v`);
14
- return;
15
- }
16
-
17
- console.log(`== ${label} ==`);
18
- console.log(destination);
19
- const lines = listRemotesVerbose(destination);
20
- if (lines.length === 0) {
21
- console.log("(no remotes)");
22
- } else {
23
- for (const line of lines) {
24
- console.log(line);
25
- }
26
- }
27
- console.log("");
28
- }
29
-
30
- function showWorkspaceRootRemotes({ workspaceRoot, dryRun }) {
31
- if (!isGitRepo(workspaceRoot)) {
32
- return;
33
- }
34
-
35
- printRepoRemotes({
36
- label: "workspace",
37
- destination: workspaceRoot,
38
- dryRun,
39
- });
40
- }
41
-
42
- function printHelp() {
43
- console.log(`usage: git-grv.js [--base-dir BASE_DIR] [--dry-run] [--no-workspace] [manifest]
44
-
45
- List git remotes with full URLs (git remote -v) for each repository.
46
-
47
- ${printCommonOptions()}
48
- --no-workspace Skip workspace root (manifest repos only)`);
49
- }
50
-
51
- function main() {
52
- const args = parseCommonArgs(process.argv.slice(2), {
53
- defaults: { noWorkspace: false },
54
- handlers: {
55
- "--no-workspace": (parsed) => {
56
- parsed.noWorkspace = true;
57
- },
58
- },
59
- });
60
- if (args.help) {
61
- printHelp();
62
- return;
63
- }
64
-
65
- const { repos, workspaceRoot } = loadManifestRepos({
66
- manifestPath: args.manifest,
67
- baseDir: args.baseDir,
68
- });
69
-
70
- if (!args.noWorkspace) {
71
- showWorkspaceRootRemotes({
72
- workspaceRoot,
73
- dryRun: args.dryRun,
74
- });
75
- }
76
-
77
- for (const repo of repos) {
78
- if (!ensureRepoExists({ name: repo.name, destination: repo.destination })) {
79
- continue;
80
- }
81
- printRepoRemotes({
82
- label: repo.name,
83
- destination: repo.destination,
84
- dryRun: args.dryRun,
85
- });
86
- }
87
- }
88
-
89
- try {
90
- main();
91
- } catch (error) {
92
- console.error(error.message);
93
- process.exit(1);
94
- }
@@ -1,94 +0,0 @@
1
- #!/usr/bin/env node
2
- /*
3
- * Git status for PAN Stack project repositories.
4
- *
5
- * oh-my-zsh-style alias for gst — git status on workspace + child repos.
6
- */
7
-
8
- const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
9
- const { ensureRepoExists, isGitRepo, runGitStatus } = require("./helpers/git");
10
-
11
- function printRepoStatus({ label, destination, dryRun, gitArgs }) {
12
- const statusArgs = gitArgs.length > 0 ? gitArgs.join(" ") : "";
13
- if (dryRun) {
14
- console.log(`[dry-run] (in ${destination}) git status ${statusArgs}`.trim());
15
- return;
16
- }
17
-
18
- console.log(`== ${label} ==`);
19
- console.log(destination);
20
- runGitStatus(destination, gitArgs);
21
- console.log("");
22
- }
23
-
24
- function showWorkspaceRootStatus({ workspaceRoot, dryRun, gitArgs }) {
25
- if (!isGitRepo(workspaceRoot)) {
26
- return;
27
- }
28
-
29
- printRepoStatus({
30
- label: "workspace",
31
- destination: workspaceRoot,
32
- dryRun,
33
- gitArgs,
34
- });
35
- }
36
-
37
- function printHelp() {
38
- console.log(`usage: git-gst.js [--base-dir BASE_DIR] [--dry-run] [--no-workspace] [manifest] [git status args...]
39
-
40
- Run git status for workspace root and each repository in the manifest.
41
- Extra args are forwarded to git status (e.g. -s, -sb).
42
-
43
- ${printCommonOptions()}
44
- --no-workspace Skip workspace root (manifest repos only)`);
45
- }
46
-
47
- function main() {
48
- const args = parseCommonArgs(process.argv.slice(2), {
49
- defaults: { noWorkspace: false, gitArgs: [] },
50
- passthrough: true,
51
- handlers: {
52
- "--no-workspace": (parsed) => {
53
- parsed.noWorkspace = true;
54
- },
55
- },
56
- });
57
- if (args.help) {
58
- printHelp();
59
- return;
60
- }
61
-
62
- const gitArgs = args.gitArgs || [];
63
- const { repos, workspaceRoot } = loadManifestRepos({
64
- manifestPath: args.manifest,
65
- baseDir: args.baseDir,
66
- });
67
-
68
- if (!args.noWorkspace) {
69
- showWorkspaceRootStatus({
70
- workspaceRoot,
71
- dryRun: args.dryRun,
72
- gitArgs,
73
- });
74
- }
75
-
76
- for (const repo of repos) {
77
- if (!ensureRepoExists({ name: repo.name, destination: repo.destination })) {
78
- continue;
79
- }
80
- printRepoStatus({
81
- label: repo.name,
82
- destination: repo.destination,
83
- dryRun: args.dryRun,
84
- gitArgs,
85
- });
86
- }
87
- }
88
-
89
- try {
90
- main();
91
- } catch (error) {
92
- console.error(error.message);
93
- process.exit(1);
94
- }