@clossys/launcher 0.1.1
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/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/check-cli.d.ts +14 -0
- package/dist/check-cli.d.ts.map +1 -0
- package/dist/check-cli.js +161 -0
- package/dist/check-cli.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +90 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.d.ts +29 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +500 -0
- package/dist/core.js.map +1 -0
- package/dist/host.d.ts +4 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +65 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
- package/skeleton/.clossys/inventory.json +4 -0
- package/skeleton/.clossys/workspace.json +6 -0
- package/skeleton/README.md +29 -0
- package/skeleton/package.json +13 -0
- package/src/check-cli.ts +179 -0
- package/src/cli.ts +92 -0
- package/src/core.ts +570 -0
- package/src/host.ts +63 -0
- package/src/index.ts +28 -0
- package/src/types.ts +114 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Ternary retained by the installed CLI. */
|
|
2
|
+
export type WorkspaceState = "satisfied" | "violated" | "indeterminate";
|
|
3
|
+
/** Captured stdout/stderr from a host command. */
|
|
4
|
+
export interface CommandResult {
|
|
5
|
+
readonly status: number | null;
|
|
6
|
+
readonly stdout: string;
|
|
7
|
+
readonly stderr: string;
|
|
8
|
+
}
|
|
9
|
+
/** Filesystem and process ports. Tests inject these; the CLI supplies Node. */
|
|
10
|
+
export interface WorkspaceHost {
|
|
11
|
+
readonly cwd: string;
|
|
12
|
+
readonly env: NodeJS.ProcessEnv;
|
|
13
|
+
readonly isTTY: boolean;
|
|
14
|
+
now(): string;
|
|
15
|
+
exists(path: string): boolean;
|
|
16
|
+
isDirectory(path: string): boolean;
|
|
17
|
+
readText(path: string): string | null;
|
|
18
|
+
writeText(path: string, contents: string): void;
|
|
19
|
+
mkdirp(path: string): void;
|
|
20
|
+
readDir(path: string): string[];
|
|
21
|
+
run(command: string, args: readonly string[], options?: {
|
|
22
|
+
cwd?: string;
|
|
23
|
+
}): CommandResult;
|
|
24
|
+
prompt(message: string, choices: readonly string[]): string | null;
|
|
25
|
+
}
|
|
26
|
+
/** On-disk hub marker generated on the consumer hub. Packed template: skeleton/.clossys/workspace.json. */
|
|
27
|
+
export interface HubDocument {
|
|
28
|
+
readonly schemaVersion: 1;
|
|
29
|
+
readonly kind: "account-hub";
|
|
30
|
+
readonly owner: string;
|
|
31
|
+
readonly repository: string;
|
|
32
|
+
}
|
|
33
|
+
export interface InventoryObservation {
|
|
34
|
+
readonly status: "missing" | "empty" | "populated";
|
|
35
|
+
readonly count: number;
|
|
36
|
+
}
|
|
37
|
+
/** Read-only pin and inventory report after adopt or resume. Never uninstalls. */
|
|
38
|
+
export interface HubHealthReport {
|
|
39
|
+
readonly marker: "present" | "missing";
|
|
40
|
+
readonly inventory: InventoryObservation;
|
|
41
|
+
readonly advisorPin: {
|
|
42
|
+
readonly dependencies?: string;
|
|
43
|
+
readonly devDependencies?: string;
|
|
44
|
+
readonly live?: string;
|
|
45
|
+
};
|
|
46
|
+
readonly dualPin: boolean;
|
|
47
|
+
readonly extraClossys: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
export interface CwdObservation {
|
|
50
|
+
readonly absolutePath: string;
|
|
51
|
+
readonly empty: boolean;
|
|
52
|
+
readonly git: boolean;
|
|
53
|
+
readonly githubOwner?: string;
|
|
54
|
+
readonly githubRepository?: string;
|
|
55
|
+
readonly hub?: HubDocument;
|
|
56
|
+
readonly looksLikeFoundry: boolean;
|
|
57
|
+
readonly inventory?: InventoryObservation;
|
|
58
|
+
}
|
|
59
|
+
export interface WorkspaceObservation {
|
|
60
|
+
readonly cwd: CwdObservation;
|
|
61
|
+
readonly ownerCandidates: readonly string[];
|
|
62
|
+
readonly envOwner?: string;
|
|
63
|
+
readonly remoteDefaultHub?: {
|
|
64
|
+
readonly owner: string;
|
|
65
|
+
readonly repository: string;
|
|
66
|
+
};
|
|
67
|
+
readonly advisorVersion?: string;
|
|
68
|
+
readonly ghAvailable: boolean;
|
|
69
|
+
readonly gitAvailable: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface WorkspacePlanCreate {
|
|
72
|
+
readonly action: "create";
|
|
73
|
+
readonly owner: string;
|
|
74
|
+
readonly repository: string;
|
|
75
|
+
readonly directory: string;
|
|
76
|
+
readonly advisorVersion: string;
|
|
77
|
+
}
|
|
78
|
+
export interface WorkspacePlanResume {
|
|
79
|
+
readonly action: "resume";
|
|
80
|
+
readonly owner: string;
|
|
81
|
+
readonly repository: string;
|
|
82
|
+
readonly directory: string;
|
|
83
|
+
readonly clone: boolean;
|
|
84
|
+
}
|
|
85
|
+
export interface WorkspacePlanAdopt {
|
|
86
|
+
readonly action: "adopt";
|
|
87
|
+
readonly owner: string;
|
|
88
|
+
readonly repository: string;
|
|
89
|
+
readonly directory: string;
|
|
90
|
+
readonly advisorVersion: string;
|
|
91
|
+
/** Absolute path of a populated inventory document to copy. Absent when cwd already has one. */
|
|
92
|
+
readonly inventorySource?: string;
|
|
93
|
+
}
|
|
94
|
+
export type WorkspacePlan = WorkspacePlanCreate | WorkspacePlanResume | WorkspacePlanAdopt;
|
|
95
|
+
export interface WorkspaceRefusal {
|
|
96
|
+
readonly action: "refuse";
|
|
97
|
+
readonly state: WorkspaceState;
|
|
98
|
+
readonly message: string;
|
|
99
|
+
}
|
|
100
|
+
export type WorkspaceDecision = WorkspacePlan | WorkspaceRefusal;
|
|
101
|
+
export interface WorkspaceApplyResult {
|
|
102
|
+
readonly state: "satisfied";
|
|
103
|
+
readonly message: string;
|
|
104
|
+
readonly health: HubHealthReport;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clossys/launcher",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"launcher": "dist/cli.js",
|
|
18
|
+
"launcher-check": "dist/check-cli.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"!src/**/*.test.ts",
|
|
24
|
+
"skeleton",
|
|
25
|
+
"README.md",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"description": "Foundry workspace scaffolder: create, resume, or adopt a GitHub repository as the account hub. Executable tooling, not a role.",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"workspace",
|
|
33
|
+
"github",
|
|
34
|
+
"npx",
|
|
35
|
+
"onboarding",
|
|
36
|
+
"typescript"
|
|
37
|
+
],
|
|
38
|
+
"author": "Clossys",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/clossys/foundry.git",
|
|
42
|
+
"directory": "packages/launcher"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/clossys/foundry/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/clossys/foundry/tree/main/packages/launcher#readme",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc -p tsconfig.json",
|
|
53
|
+
"prepublishOnly": "node ../../scripts/check-name-collision.mjs . && npm run build",
|
|
54
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
|
+
"test": "vitest run"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^26.5.1",
|
|
59
|
+
"typescript": "~6.0.0",
|
|
60
|
+
"vitest": "^5.0.0"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"registry": "https://registry.npmjs.org",
|
|
64
|
+
"access": "public"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Account workspace
|
|
2
|
+
|
|
3
|
+
This repository is the account hub for Foundry packages. It inventories
|
|
4
|
+
where packages are installed and coordinates engagement. It is not a
|
|
5
|
+
product application. Do not install the whole catalogue here.
|
|
6
|
+
|
|
7
|
+
## Open this folder
|
|
8
|
+
|
|
9
|
+
Open this repository in your coding agent (Cursor, Claude, Codex, or
|
|
10
|
+
another agent that can work a GitHub checkout). Advisor is read-only
|
|
11
|
+
until you approve a next action.
|
|
12
|
+
|
|
13
|
+
## Resume later
|
|
14
|
+
|
|
15
|
+
From this folder, or from any empty directory on a machine that can see
|
|
16
|
+
the same GitHub owner:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @clossys/launcher
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Public npm reads are credentialless. Do not add a token or private
|
|
23
|
+
registry mapping for `@clossys`.
|
|
24
|
+
|
|
25
|
+
## What this hub pins
|
|
26
|
+
|
|
27
|
+
Exact `@clossys/advisor` is a development dependency so the engagement
|
|
28
|
+
gate can run against evidence you own. Other roles are pinned in the
|
|
29
|
+
repositories that actually do the work, not dumped into this hub.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__REPOSITORY_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"description": "Account workspace hub for Foundry engagement. Not a product application.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@clossys/advisor": "__ADVISOR_VERSION__"
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/check-cli.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { isDirectInvocation } from "./cli.js";
|
|
5
|
+
import { planWorkspace } from "./core.js";
|
|
6
|
+
import type {
|
|
7
|
+
CommandResult,
|
|
8
|
+
CwdObservation,
|
|
9
|
+
InventoryObservation,
|
|
10
|
+
WorkspaceHost,
|
|
11
|
+
WorkspaceObservation,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
export const CHECK_USAGE = `Usage: launcher-check --input <observation.json>
|
|
15
|
+
|
|
16
|
+
Grade a captured workspace observation through planWorkspace. This bin does
|
|
17
|
+
not create, resume, or appoint a hub.
|
|
18
|
+
|
|
19
|
+
Exit codes: 0 = a create, resume, or adopt plan, 1 = a known refusal,
|
|
20
|
+
2 = indeterminate or the input could not be read.`;
|
|
21
|
+
|
|
22
|
+
export class LauncherCheckInputError extends Error {}
|
|
23
|
+
|
|
24
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isText(value: unknown): value is string {
|
|
29
|
+
return typeof value === "string" && value.trim() !== "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseInventoryObservation(value: unknown): InventoryObservation | undefined {
|
|
33
|
+
if (value === undefined) return undefined;
|
|
34
|
+
if (!isRecord(value)) {
|
|
35
|
+
throw new LauncherCheckInputError("observation.cwd.inventory must be an object");
|
|
36
|
+
}
|
|
37
|
+
if (value.status !== "missing" && value.status !== "empty" && value.status !== "populated") {
|
|
38
|
+
throw new LauncherCheckInputError("observation.cwd.inventory.status must be missing, empty, or populated");
|
|
39
|
+
}
|
|
40
|
+
if (typeof value.count !== "number" || !Number.isInteger(value.count) || value.count < 0) {
|
|
41
|
+
throw new LauncherCheckInputError("observation.cwd.inventory.count must be a nonnegative integer");
|
|
42
|
+
}
|
|
43
|
+
return { status: value.status, count: value.count };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseCwd(value: unknown): CwdObservation {
|
|
47
|
+
if (!isRecord(value) || !isText(value.absolutePath)) {
|
|
48
|
+
throw new LauncherCheckInputError("observation.cwd.absolutePath must be a nonempty string");
|
|
49
|
+
}
|
|
50
|
+
if (typeof value.empty !== "boolean" || typeof value.git !== "boolean" || typeof value.looksLikeFoundry !== "boolean") {
|
|
51
|
+
throw new LauncherCheckInputError("observation.cwd must declare empty, git, and looksLikeFoundry booleans");
|
|
52
|
+
}
|
|
53
|
+
if (value.githubOwner !== undefined && !isText(value.githubOwner)) {
|
|
54
|
+
throw new LauncherCheckInputError("observation.cwd.githubOwner must be a string");
|
|
55
|
+
}
|
|
56
|
+
if (value.githubRepository !== undefined && !isText(value.githubRepository)) {
|
|
57
|
+
throw new LauncherCheckInputError("observation.cwd.githubRepository must be a string");
|
|
58
|
+
}
|
|
59
|
+
const inventory = parseInventoryObservation(value.inventory);
|
|
60
|
+
return {
|
|
61
|
+
absolutePath: value.absolutePath,
|
|
62
|
+
empty: value.empty,
|
|
63
|
+
git: value.git,
|
|
64
|
+
looksLikeFoundry: value.looksLikeFoundry,
|
|
65
|
+
...(isText(value.githubOwner) ? { githubOwner: value.githubOwner } : {}),
|
|
66
|
+
...(isText(value.githubRepository) ? { githubRepository: value.githubRepository } : {}),
|
|
67
|
+
...(inventory === undefined ? {} : { inventory }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseObservation(value: unknown): WorkspaceObservation {
|
|
72
|
+
if (!isRecord(value)) throw new LauncherCheckInputError("observation must be a JSON object");
|
|
73
|
+
if (!Array.isArray(value.ownerCandidates) || value.ownerCandidates.some((item) => typeof item !== "string")) {
|
|
74
|
+
throw new LauncherCheckInputError("observation.ownerCandidates must be an array of strings");
|
|
75
|
+
}
|
|
76
|
+
if (typeof value.ghAvailable !== "boolean" || typeof value.gitAvailable !== "boolean") {
|
|
77
|
+
throw new LauncherCheckInputError("observation must declare ghAvailable and gitAvailable booleans");
|
|
78
|
+
}
|
|
79
|
+
if (value.envOwner !== undefined && !isText(value.envOwner)) {
|
|
80
|
+
throw new LauncherCheckInputError("observation.envOwner must be a string");
|
|
81
|
+
}
|
|
82
|
+
if (value.advisorVersion !== undefined && !isText(value.advisorVersion)) {
|
|
83
|
+
throw new LauncherCheckInputError("observation.advisorVersion must be a string");
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
cwd: parseCwd(value.cwd),
|
|
87
|
+
ownerCandidates: value.ownerCandidates,
|
|
88
|
+
ghAvailable: value.ghAvailable,
|
|
89
|
+
gitAvailable: value.gitAvailable,
|
|
90
|
+
...(isText(value.envOwner) ? { envOwner: value.envOwner } : {}),
|
|
91
|
+
...(isText(value.advisorVersion) ? { advisorVersion: value.advisorVersion } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function unused(): CommandResult {
|
|
96
|
+
return { status: 1, stdout: "", stderr: "launcher-check does not run host commands" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Host used only for owner prompting. Grading never creates a hub. */
|
|
100
|
+
export function planningHost(): WorkspaceHost {
|
|
101
|
+
return {
|
|
102
|
+
cwd: "/",
|
|
103
|
+
env: {},
|
|
104
|
+
isTTY: false,
|
|
105
|
+
now: () => "1970-01-01T00:00:00.000Z",
|
|
106
|
+
exists: () => false,
|
|
107
|
+
isDirectory: () => false,
|
|
108
|
+
readText: () => null,
|
|
109
|
+
writeText: () => {
|
|
110
|
+
throw new Error("launcher-check does not write");
|
|
111
|
+
},
|
|
112
|
+
mkdirp: () => {
|
|
113
|
+
throw new Error("launcher-check does not write");
|
|
114
|
+
},
|
|
115
|
+
readDir: () => [],
|
|
116
|
+
run: () => unused(),
|
|
117
|
+
prompt: () => null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function parseCheckArgs(argv: readonly string[]): { help: boolean; inputPath?: string } {
|
|
122
|
+
if (argv.length === 0) return { help: true };
|
|
123
|
+
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) return { help: true };
|
|
124
|
+
if (argv.length === 2 && argv[0] === "--input" && argv[1]) return { help: false, inputPath: argv[1] };
|
|
125
|
+
throw new LauncherCheckInputError("expected --input <observation.json>, or --help");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function checkMain(
|
|
129
|
+
argv: readonly string[],
|
|
130
|
+
readText: (path: string) => string,
|
|
131
|
+
writeOut: (text: string) => void,
|
|
132
|
+
writeErr: (text: string) => void,
|
|
133
|
+
): number {
|
|
134
|
+
const parsed = parseCheckArgs(argv);
|
|
135
|
+
if (parsed.help) {
|
|
136
|
+
writeOut(`${CHECK_USAGE}\n`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
const inputPath = parsed.inputPath;
|
|
140
|
+
if (inputPath === undefined) throw new LauncherCheckInputError("expected --input <observation.json>");
|
|
141
|
+
let raw: string;
|
|
142
|
+
try {
|
|
143
|
+
raw = readText(resolve(inputPath));
|
|
144
|
+
} catch {
|
|
145
|
+
throw new LauncherCheckInputError(`cannot read ${inputPath}`);
|
|
146
|
+
}
|
|
147
|
+
let parsedJson: unknown;
|
|
148
|
+
try {
|
|
149
|
+
parsedJson = JSON.parse(raw);
|
|
150
|
+
} catch {
|
|
151
|
+
throw new LauncherCheckInputError(`${inputPath} is not JSON`);
|
|
152
|
+
}
|
|
153
|
+
const observation = parseObservation(parsedJson);
|
|
154
|
+
const decision = planWorkspace(observation, planningHost());
|
|
155
|
+
if (decision.action === "refuse") {
|
|
156
|
+
writeErr(`launcher-check: ${decision.message}\n`);
|
|
157
|
+
return decision.state === "violated" ? 1 : 2;
|
|
158
|
+
}
|
|
159
|
+
writeOut(`${JSON.stringify({ action: decision.action, owner: decision.owner, repository: decision.repository })}\n`);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (isDirectInvocation(import.meta.url, process.argv[1])) {
|
|
164
|
+
try {
|
|
165
|
+
process.exitCode = checkMain(
|
|
166
|
+
process.argv.slice(2),
|
|
167
|
+
(path) => readFileSync(path, "utf8"),
|
|
168
|
+
(text) => {
|
|
169
|
+
process.stdout.write(text);
|
|
170
|
+
},
|
|
171
|
+
(text) => {
|
|
172
|
+
process.stderr.write(text);
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
} catch (cause) {
|
|
176
|
+
process.stderr.write(`launcher-check: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
177
|
+
process.exitCode = 2;
|
|
178
|
+
}
|
|
179
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { applyWorkspacePlan, observeWorkspace, planWorkspace, skeletonRootFromModule } from "./core.js";
|
|
6
|
+
import { createNodeHost } from "./host.js";
|
|
7
|
+
import type { WorkspaceHost } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export const USAGE = `Usage: launcher [--inventory <path>]
|
|
10
|
+
|
|
11
|
+
Create, resume, or appoint a GitHub repository as the account workspace hub.
|
|
12
|
+
|
|
13
|
+
Run from an empty directory to create {owner}/workspace. Run from an existing
|
|
14
|
+
hub to resume. Run from any GitHub repository you want to own the account-level
|
|
15
|
+
hub to appoint it — it does not have to be a new exclusive repo, and it keeps
|
|
16
|
+
its current name and files.
|
|
17
|
+
|
|
18
|
+
Appointing requires a populated generated hub inventory (packed template
|
|
19
|
+
skeleton/.clossys/inventory.json; the generated path does not ship), or
|
|
20
|
+
--inventory <path> pointing at one. Resume does not write. Create may write
|
|
21
|
+
an empty inventory.
|
|
22
|
+
|
|
23
|
+
GitHub-only. Owner is inferred from \`gh\` and git remotes. Public npm reads
|
|
24
|
+
need no token.
|
|
25
|
+
|
|
26
|
+
Exit codes: 0 = satisfied, 1 = a known refusal, 2 = indeterminate.`;
|
|
27
|
+
|
|
28
|
+
export class LauncherInputError extends Error {}
|
|
29
|
+
|
|
30
|
+
function exitCodeFor(state: "satisfied" | "violated" | "indeterminate"): number {
|
|
31
|
+
return state === "satisfied" ? 0 : state === "violated" ? 1 : 2;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseLauncherArgs(argv: readonly string[]): { help: boolean; inventoryPath?: string } {
|
|
35
|
+
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) return { help: true };
|
|
36
|
+
if (argv.length === 0) return { help: false };
|
|
37
|
+
if (argv.length === 2 && argv[0] === "--inventory" && argv[1]) return { help: false, inventoryPath: argv[1] };
|
|
38
|
+
throw new LauncherInputError("launcher takes no arguments except optional --inventory <path>; run it from the directory to create or appoint");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Testable CLI dispatcher. Unknown arguments throw; the executable maps them to exit 2. */
|
|
42
|
+
export function main(argv: readonly string[], host: WorkspaceHost, skeletonRoot: string): number {
|
|
43
|
+
const parsed = parseLauncherArgs(argv);
|
|
44
|
+
if (parsed.help) {
|
|
45
|
+
console.log(USAGE);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
const observation = observeWorkspace(host);
|
|
49
|
+
if (
|
|
50
|
+
!observation.cwd.empty &&
|
|
51
|
+
!observation.cwd.git &&
|
|
52
|
+
observation.cwd.hub === undefined &&
|
|
53
|
+
!observation.cwd.looksLikeFoundry
|
|
54
|
+
) {
|
|
55
|
+
console.log(USAGE);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const decision = planWorkspace(observation, host, { inventoryPath: parsed.inventoryPath });
|
|
59
|
+
if (decision.action === "refuse") {
|
|
60
|
+
console.error(`launcher: ${decision.message}`);
|
|
61
|
+
return exitCodeFor(decision.state);
|
|
62
|
+
}
|
|
63
|
+
if (parsed.inventoryPath !== undefined && decision.action !== "adopt") {
|
|
64
|
+
console.error("launcher: --inventory is only valid when appointing a GitHub repository");
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const result = applyWorkspacePlan(host, decision, skeletonRoot);
|
|
68
|
+
console.log(result.message);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function run(): void {
|
|
73
|
+
try {
|
|
74
|
+
const host = createNodeHost();
|
|
75
|
+
process.exitCode = main(process.argv.slice(2), host, skeletonRootFromModule(import.meta.url));
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
console.error(`launcher: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
78
|
+
process.exitCode = 2;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Resolves an npm/POSIX bin symlink before deciding whether this module is the entrypoint. */
|
|
83
|
+
export function isDirectInvocation(moduleUrl: string, argvPath: string | undefined): boolean {
|
|
84
|
+
if (argvPath === undefined) return false;
|
|
85
|
+
try {
|
|
86
|
+
return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (isDirectInvocation(import.meta.url, process.argv[1])) run();
|