@ivotoby/postgram-cli 1.38.3 → 1.39.0

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/README.md CHANGED
@@ -121,6 +121,32 @@ provenance, decisions, dependencies, blockers, ownership, involvement,
121
121
  discussion participants, connected context, or ambiguous search hits. Do not
122
122
  expand when the compact result already answers a direct fact.
123
123
 
124
+ ## Workspace tracking
125
+
126
+ Track features, machines, workspaces and agent sessions across worktrees:
127
+
128
+ ```bash
129
+ pgm here # report the current workspace and session
130
+ pgm hooks install # install the Claude Code hooks (--pi installs the pi extension)
131
+ pgm hooks uninstall # remove the installed hooks
132
+ pgm where # features and workspaces grouped by stage
133
+ pgm feature add "my feature" # create a feature (also: stage, pr, link, unlink)
134
+ pgm machine set --alias codebox # set this machine's alias or connect target (also: pgm machine list)
135
+ pgm repo set <remote> --release-on-merge true|false # record the repository for this worktree
136
+ pgm worktree new 1234abcd # create a worktree for a feature (UUID, 8-hex short id or title; --create creates the feature)
137
+ pgm scan --root ~/src # discover worktrees and record their features (needs --root <dir> or scan_roots; --yes-exact, --dry-run)
138
+ ```
139
+
140
+ New `~/.pgmrc` keys (read alongside `api_url`/`api_key`):
141
+
142
+ | Key | Default |
143
+ |-----|---------|
144
+ | `machine` | `{ "alias": <hostname>, "connect_target": <alias> }` |
145
+ | `scan_roots` | `[]` |
146
+ | `worktree_root` | `null` (meaning `<repo_root>.worktrees`) |
147
+ | `here_timeout_ms` | `2000` |
148
+ | `resume` | per-agent-kind `with_id`/`without_id` command templates |
149
+
124
150
  ## Memory Roles
125
151
 
126
152
  Store durable memory:
package/dist/client.js CHANGED
@@ -12,7 +12,7 @@ function normalizeErrorBody(body) {
12
12
  }
13
13
  return body;
14
14
  }
15
- async function request(options, path, requestOptions = {}) {
15
+ export async function request(options, path, requestOptions = {}) {
16
16
  const headers = {
17
17
  Authorization: `Bearer ${options.apiKey}`,
18
18
  ...(requestOptions.body === undefined
@@ -28,6 +28,9 @@ async function request(options, path, requestOptions = {}) {
28
28
  if (requestOptions.body !== undefined) {
29
29
  init.body = JSON.stringify(requestOptions.body);
30
30
  }
31
+ if (requestOptions.signal !== undefined) {
32
+ init.signal = requestOptions.signal;
33
+ }
31
34
  const response = await fetch(`${options.apiUrl}${path}`, init);
32
35
  if (!response.ok) {
33
36
  const body = normalizeErrorBody(await parseResponse(response));
package/dist/pgm.js CHANGED
@@ -8,6 +8,12 @@ import { pipeline } from 'node:stream/promises';
8
8
  import { createPgmClient } from './client.js';
9
9
  import { handleCliFailure, isJsonMode, parseCommaList, parseJsonObject, printHuman, printJson, readStdinText, resolvePgmConfig, shortId } from './shared.js';
10
10
  import { AppError, ErrorCode } from './errors.js';
11
+ import { registerFeatureCommands } from './tracking/feature-commands.js';
12
+ import { registerHereCommand } from './tracking/here.js';
13
+ import { registerHooksCommands } from './tracking/hooks.js';
14
+ import { registerScanCommand } from './tracking/scan.js';
15
+ import { registerWhereCommand } from './tracking/where.js';
16
+ import { registerWorktreeCommand } from './tracking/worktree.js';
11
17
  import { buildSyncManifest } from './sync-walk.js';
12
18
  import { compactEdgeResponse, compactEntityListResponse, compactGraphResponse, compactSearchResponse, compactStoredEntityResponse, entityListResponseToToon, graphResponseToToon, searchResponseToToon } from './search-output.js';
13
19
  function readCliVersion() {
@@ -908,4 +914,10 @@ program
908
914
  return lines;
909
915
  });
910
916
  });
917
+ registerHereCommand(program);
918
+ registerHooksCommands(program);
919
+ registerWhereCommand(program);
920
+ registerFeatureCommands(program);
921
+ registerWorktreeCommand(program);
922
+ registerScanCommand(program);
911
923
  await program.parseAsync(process.argv);
@@ -0,0 +1,139 @@
1
+ // cli/src/tracking/api.ts
2
+ import os from 'node:os';
3
+ import { createPgmClient, request } from '../client.js';
4
+ import { handleCliFailure, isJsonMode, printHuman, printJson, resolvePgmConfig } from '../shared.js';
5
+ import { UsageError, loadTrackingConfig } from './config.js';
6
+ function toQuery(params) {
7
+ const query = new URLSearchParams();
8
+ for (const [key, value] of Object.entries(params)) {
9
+ query.set(key, value);
10
+ }
11
+ const text = query.toString();
12
+ return text ? `?${text}` : '';
13
+ }
14
+ export function createTrackingClient(options) {
15
+ return {
16
+ report(body, reportOptions) {
17
+ return request(options, '/api/workspaces/report', {
18
+ method: 'POST',
19
+ body,
20
+ ...(reportOptions?.timeoutMs !== undefined
21
+ ? { signal: AbortSignal.timeout(reportOptions.timeoutMs) }
22
+ : {})
23
+ });
24
+ },
25
+ patchWorkspace(id, body) {
26
+ return request(options, `/api/workspaces/${encodeURIComponent(id)}`, { method: 'PATCH', body });
27
+ },
28
+ putMachine(alias, connectTarget) {
29
+ return request(options, `/api/machines/${encodeURIComponent(alias)}`, { method: 'PUT', body: { connect_target: connectTarget } });
30
+ },
31
+ listMachines() {
32
+ return request(options, '/api/machines');
33
+ },
34
+ putRepository(remote, releaseOnMerge) {
35
+ return request(options, '/api/repositories', {
36
+ method: 'PUT',
37
+ body: { remote, release_on_merge: releaseOnMerge }
38
+ });
39
+ },
40
+ createFeature(input) {
41
+ return request(options, '/api/features', {
42
+ method: 'POST',
43
+ body: input
44
+ });
45
+ },
46
+ setStage(id, stage) {
47
+ return request(options, `/api/features/${encodeURIComponent(id)}/stage`, { method: 'POST', body: { stage } });
48
+ },
49
+ addPullRequest(id, input) {
50
+ return request(options, `/api/features/${encodeURIComponent(id)}/pull-requests`, {
51
+ method: 'POST',
52
+ body: input
53
+ });
54
+ },
55
+ lookup(machine, path) {
56
+ return request(options, `/api/workspaces/lookup${toQuery({ machine, path })}`);
57
+ },
58
+ listWorkspaces(query) {
59
+ const params = {};
60
+ if (query.machine !== undefined) {
61
+ params.machine = query.machine;
62
+ }
63
+ if (query.repo !== undefined) {
64
+ params.repo = query.repo;
65
+ }
66
+ if (query.featureId !== undefined) {
67
+ params.feature_id = query.featureId;
68
+ }
69
+ if (query.unassigned) {
70
+ params.unassigned = 'true';
71
+ }
72
+ if (query.includeEnded) {
73
+ params.include_ended = 'true';
74
+ }
75
+ return request(options, `/api/workspaces${toQuery(params)}`);
76
+ },
77
+ where(query) {
78
+ const params = {};
79
+ if (query.view !== undefined) {
80
+ params.view = query.view;
81
+ }
82
+ if (query.machine !== undefined) {
83
+ params.machine = query.machine;
84
+ }
85
+ if (query.repo !== undefined) {
86
+ params.repo = query.repo;
87
+ }
88
+ if (query.includeReleased) {
89
+ params.include_released = 'true';
90
+ }
91
+ if (query.includeDone) {
92
+ params.include_done = 'true';
93
+ }
94
+ if (query.includeEnded) {
95
+ params.include_ended = 'true';
96
+ }
97
+ return request(options, `/api/where${toQuery(params)}`);
98
+ }
99
+ };
100
+ }
101
+ export async function runTrackingCommand(command, handler) {
102
+ const json = isJsonMode(command);
103
+ let result;
104
+ try {
105
+ const homeDir = os.homedir();
106
+ const config = await loadTrackingConfig();
107
+ const credentials = await resolvePgmConfig();
108
+ result = await handler({
109
+ json,
110
+ config,
111
+ client: createTrackingClient(credentials),
112
+ pgm: createPgmClient(credentials),
113
+ homeDir
114
+ });
115
+ }
116
+ catch (error) {
117
+ if (error instanceof UsageError) {
118
+ process.stderr.write(`${error.message}\n`);
119
+ process.exitCode = 2;
120
+ return;
121
+ }
122
+ await handleCliFailure(error, json);
123
+ return;
124
+ }
125
+ if (result === undefined) {
126
+ return;
127
+ }
128
+ if (Array.isArray(result)) {
129
+ if (json) {
130
+ printJson(result);
131
+ }
132
+ else {
133
+ printHuman(result.map(String));
134
+ }
135
+ }
136
+ else {
137
+ printJson(result);
138
+ }
139
+ }
@@ -0,0 +1,148 @@
1
+ // cli/src/tracking/config.ts
2
+ import { chmod, readFile, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { AGENT_KINDS } from './types.js';
6
+ export const DEFAULT_RESUME = {
7
+ 'claude-code': { with_id: 'claude --resume {session_id}', without_id: 'claude' },
8
+ pi: { with_id: 'pi --session {session_id}', without_id: 'pi' },
9
+ talon: { with_id: null, without_id: null },
10
+ other: { with_id: null, without_id: null }
11
+ };
12
+ export class UsageError extends Error {
13
+ }
14
+ export function shq(value) {
15
+ return `'${value.replaceAll("'", `'\\''`)}'`;
16
+ }
17
+ export function slug(title) {
18
+ return (title
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9]+/g, '-')
21
+ .replace(/^-+|-+$/g, '')
22
+ .slice(0, 40)
23
+ .replace(/-+$/, '') || 'feature');
24
+ }
25
+ export function defaultAlias(hostname) {
26
+ const head = hostname.toLowerCase().split('.')[0] ?? '';
27
+ return head.replace(/[^a-z0-9._-]/g, '-') || 'machine';
28
+ }
29
+ export function expandHome(value, homeDir) {
30
+ if (value === '~') {
31
+ return homeDir;
32
+ }
33
+ if (value.startsWith('~/')) {
34
+ return path.join(homeDir, value.slice(2));
35
+ }
36
+ return value;
37
+ }
38
+ function isRecord(value) {
39
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
40
+ }
41
+ function nonEmptyString(value) {
42
+ return typeof value === 'string' && value !== '' ? value : null;
43
+ }
44
+ function cloneResume(templates) {
45
+ const result = {};
46
+ for (const kind of AGENT_KINDS) {
47
+ const base = templates[kind];
48
+ result[kind] = { with_id: base.with_id, without_id: base.without_id };
49
+ }
50
+ return result;
51
+ }
52
+ export function parseTrackingConfig(raw, input) {
53
+ const machine = isRecord(raw) ? (isRecord(raw.machine) ? raw.machine : {}) : {};
54
+ const alias = nonEmptyString(machine.alias) ?? defaultAlias(input.hostname);
55
+ const connectTarget = nonEmptyString(machine.connect_target) ?? alias;
56
+ const scanRoots = [];
57
+ if (isRecord(raw) && Array.isArray(raw.scan_roots)) {
58
+ for (const entry of raw.scan_roots) {
59
+ if (typeof entry === 'string') {
60
+ scanRoots.push(expandHome(entry, input.homeDir));
61
+ }
62
+ }
63
+ }
64
+ const worktreeRoot = isRecord(raw) && typeof raw.worktree_root === 'string'
65
+ ? expandHome(raw.worktree_root, input.homeDir)
66
+ : null;
67
+ const timeout = isRecord(raw) ? raw.here_timeout_ms : undefined;
68
+ const hereTimeoutMs = typeof timeout === 'number' && Number.isFinite(timeout) && timeout > 0
69
+ ? timeout
70
+ : 2000;
71
+ const resume = cloneResume(DEFAULT_RESUME);
72
+ const rawResume = isRecord(raw) && isRecord(raw.resume) ? raw.resume : {};
73
+ for (const kind of AGENT_KINDS) {
74
+ const kindConfig = rawResume[kind];
75
+ if (!isRecord(kindConfig)) {
76
+ continue;
77
+ }
78
+ if (typeof kindConfig.with_id === 'string' || kindConfig.with_id === null) {
79
+ resume[kind].with_id = kindConfig.with_id;
80
+ }
81
+ if (typeof kindConfig.without_id === 'string' ||
82
+ kindConfig.without_id === null) {
83
+ resume[kind].without_id = kindConfig.without_id;
84
+ }
85
+ }
86
+ return { alias, connectTarget, scanRoots, worktreeRoot, hereTimeoutMs, resume };
87
+ }
88
+ export async function loadTrackingConfig(input) {
89
+ const homeDir = input?.homeDir ?? os.homedir();
90
+ const hostname = input?.hostname ?? os.hostname();
91
+ let raw = {};
92
+ try {
93
+ const text = await readFile(path.join(homeDir, '.pgmrc'), 'utf8');
94
+ raw = JSON.parse(text);
95
+ }
96
+ catch {
97
+ raw = {};
98
+ }
99
+ return parseTrackingConfig(raw, { homeDir, hostname });
100
+ }
101
+ export async function writeMachineConfig(input) {
102
+ const homeDir = input.homeDir ?? os.homedir();
103
+ const rcPath = path.join(homeDir, '.pgmrc');
104
+ let doc = {};
105
+ let text = null;
106
+ try {
107
+ text = await readFile(rcPath, 'utf8');
108
+ }
109
+ catch (error) {
110
+ const code = error instanceof Error && 'code' in error
111
+ ? error.code
112
+ : undefined;
113
+ if (code !== 'ENOENT') {
114
+ throw error;
115
+ }
116
+ }
117
+ if (text !== null) {
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(text);
121
+ }
122
+ catch (error) {
123
+ throw new UsageError(`~/.pgmrc contains invalid JSON: ${error instanceof Error ? error.message : 'parse failed'}`);
124
+ }
125
+ if (!isRecord(parsed)) {
126
+ throw new UsageError('~/.pgmrc must be a JSON object');
127
+ }
128
+ doc = parsed;
129
+ }
130
+ const machine = isRecord(doc.machine) ? doc.machine : {};
131
+ const previousAlias = nonEmptyString(machine.alias);
132
+ const alias = nonEmptyString(input.alias) ??
133
+ previousAlias ??
134
+ defaultAlias(os.hostname());
135
+ const connectTarget = nonEmptyString(input.connectTarget) ??
136
+ nonEmptyString(machine.connect_target) ??
137
+ alias;
138
+ doc.machine = { ...machine, alias, connect_target: connectTarget };
139
+ await writeFile(rcPath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
140
+ await chmod(rcPath, 0o600);
141
+ return { previousAlias, alias, connectTarget };
142
+ }
143
+ export function usageExit(command) {
144
+ command.exitOverride((error) => {
145
+ process.exit(error.exitCode === 0 ? 0 : 2);
146
+ });
147
+ return command;
148
+ }
@@ -0,0 +1,228 @@
1
+ // cli/src/tracking/feature-commands.ts: pgm feature, pgm machine, pgm repo.
2
+ import { existsSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import { AppError, ErrorCode } from '../errors.js';
5
+ import { parseCommaList, shortId } from '../shared.js';
6
+ import { runTrackingCommand } from './api.js';
7
+ import { UsageError, usageExit, writeMachineConfig } from './config.js';
8
+ import { readBootId, removeMarker, resolveLocalWorkspace, writeMarker } from './local.js';
9
+ import { FEATURE_STAGES } from './types.js';
10
+ export async function runFeatureAdd(title, options, deps) {
11
+ const tags = parseCommaList(options.tags);
12
+ const response = await deps.client.createFeature({
13
+ content: title,
14
+ ...(tags !== undefined ? { tags } : {}),
15
+ ...(options.visibility !== undefined ? { visibility: options.visibility } : {})
16
+ });
17
+ return deps.json ? response : [response.entity.id];
18
+ }
19
+ export async function runFeatureStage(id, stage, deps) {
20
+ if (!FEATURE_STAGES.includes(stage)) {
21
+ throw new UsageError(`invalid stage '${stage}'. Use one of: ${FEATURE_STAGES.join(', ')}`);
22
+ }
23
+ const response = await deps.client.setStage(id, stage);
24
+ return deps.json
25
+ ? response
26
+ : [`${shortId(response.entity.id)} ${response.previous_stage ?? '-'} → ${stage}`];
27
+ }
28
+ const PR_STATES = ['open', 'merged', 'closed'];
29
+ export async function runFeaturePr(id, url, options, deps) {
30
+ const state = options.state ?? 'open';
31
+ if (!PR_STATES.includes(state)) {
32
+ throw new UsageError(`invalid --state '${state}'. Use one of: ${PR_STATES.join(', ')}`);
33
+ }
34
+ const response = await deps.client.addPullRequest(id, {
35
+ url,
36
+ state: state
37
+ });
38
+ return deps.json
39
+ ? response
40
+ : [`${shortId(response.entity.id)} PR ${state}, stage ${response.to}`];
41
+ }
42
+ export async function runFeatureLink(id, options, deps) {
43
+ if (options.workspace !== undefined) {
44
+ const patched = await deps.client.patchWorkspace(options.workspace, {
45
+ feature_id: id
46
+ });
47
+ const workspace = patched.workspace;
48
+ if (workspace.machine === deps.config.alias && deps.exists(workspace.path)) {
49
+ if (id !== null) {
50
+ await deps.writeMarker(workspace.path, workspace.feature_id ?? id);
51
+ }
52
+ else {
53
+ await deps.removeMarker(workspace.path);
54
+ }
55
+ }
56
+ else {
57
+ deps.warn(`warning: ${workspace.machine} may have a feature marker; its next registration will restore the marker's link`);
58
+ }
59
+ return deps.json
60
+ ? patched
61
+ : [
62
+ id !== null
63
+ ? `linked ${workspace.path} → ${shortId(id)}`
64
+ : `unlinked ${workspace.path}`
65
+ ];
66
+ }
67
+ const target = options.path ?? deps.cwd;
68
+ const local = await deps.resolveWorkspace(target);
69
+ if (local.kind === 'timeout') {
70
+ throw new AppError(ErrorCode.INTERNAL, 'git timeout');
71
+ }
72
+ const looked = await deps.client.lookup(deps.config.alias, local.path);
73
+ let workspaceId;
74
+ if (looked.workspace !== null && looked.workspace.path === local.path) {
75
+ workspaceId = looked.workspace.id;
76
+ }
77
+ else {
78
+ const report = await deps.client.report({
79
+ source: 'hook',
80
+ machine: {
81
+ alias: deps.config.alias,
82
+ hostname: deps.hostname,
83
+ boot_id: await deps.bootId()
84
+ },
85
+ workspace: local.workspace
86
+ });
87
+ if (report.workspace === null) {
88
+ throw new AppError(ErrorCode.INTERNAL, 'workspace registration returned no workspace');
89
+ }
90
+ workspaceId = report.workspace.id;
91
+ }
92
+ const patched = await deps.client.patchWorkspace(workspaceId, {
93
+ feature_id: id
94
+ });
95
+ if (id !== null) {
96
+ await deps.writeMarker(local.path, patched.workspace.feature_id ?? id);
97
+ }
98
+ else {
99
+ await deps.removeMarker(local.path);
100
+ }
101
+ return deps.json
102
+ ? patched
103
+ : [
104
+ id !== null
105
+ ? `linked ${local.path} → ${shortId(id)}`
106
+ : `unlinked ${local.path}`
107
+ ];
108
+ }
109
+ export async function runMachineSet(options, deps) {
110
+ const written = await deps.writeConfig({
111
+ ...(options.alias !== undefined ? { alias: options.alias } : {}),
112
+ ...(options.connect !== undefined ? { connectTarget: options.connect } : {}),
113
+ homeDir: deps.homeDir
114
+ });
115
+ if (written.previousAlias !== null && written.previousAlias !== written.alias) {
116
+ deps.warn(`warning: history stays under the old alias '${written.previousAlias}'`);
117
+ }
118
+ await deps.client.report({
119
+ source: 'hook',
120
+ machine: {
121
+ alias: written.alias,
122
+ hostname: deps.hostname,
123
+ boot_id: await deps.bootId()
124
+ }
125
+ });
126
+ const result = await deps.client.putMachine(written.alias, written.connectTarget);
127
+ return deps.json
128
+ ? result
129
+ : [`machine ${written.alias} → ${written.connectTarget}`];
130
+ }
131
+ export async function runMachineList(deps) {
132
+ const result = await deps.client.listMachines();
133
+ return deps.json
134
+ ? result
135
+ : result.machines.map((machine) => `${machine.alias} ${machine.connect_target ?? machine.alias} ${machine.last_seen_at}`);
136
+ }
137
+ export async function runRepoSet(remote, options, deps) {
138
+ if (options.releaseOnMerge !== 'true' && options.releaseOnMerge !== 'false') {
139
+ throw new UsageError(`--release-on-merge must be 'true' or 'false'`);
140
+ }
141
+ const releaseOnMerge = options.releaseOnMerge === 'true';
142
+ const result = await deps.client.putRepository(remote, releaseOnMerge);
143
+ return deps.json ? result : [`${remote} release_on_merge=${releaseOnMerge}`];
144
+ }
145
+ function featureDeps(context) {
146
+ return {
147
+ client: context.client,
148
+ config: context.config,
149
+ json: context.json,
150
+ cwd: process.cwd(),
151
+ hostname: os.hostname(),
152
+ homeDir: context.homeDir,
153
+ bootId: readBootId,
154
+ resolveWorkspace: (cwd) => resolveLocalWorkspace(cwd),
155
+ writeMarker: (worktree, featureId) => writeMarker(worktree, featureId, new Date()),
156
+ removeMarker,
157
+ exists: existsSync,
158
+ warn: (line) => process.stderr.write(`${line}\n`)
159
+ };
160
+ }
161
+ export function registerFeatureCommands(program) {
162
+ const feature = usageExit(program
163
+ .command('feature')
164
+ .description('Manage features'));
165
+ usageExit(feature
166
+ .command('add')
167
+ .description('Create a feature')
168
+ .argument('<title>', 'feature title')
169
+ .option('--tags <tags>', 'comma-separated tags')
170
+ .option('--visibility <visibility>', 'personal, work or shared')).action(async (title, options, command) => {
171
+ await runTrackingCommand(command, (context) => runFeatureAdd(title, options, featureDeps(context)));
172
+ });
173
+ usageExit(feature
174
+ .command('stage')
175
+ .description('Set a feature stage')
176
+ .argument('<id>', 'feature id (UUID or 8-hex)')
177
+ .argument('<stage>', `one of: ${FEATURE_STAGES.join(', ')}`)).action(async (id, stage, _options, command) => {
178
+ await runTrackingCommand(command, (context) => runFeatureStage(id, stage, featureDeps(context)));
179
+ });
180
+ usageExit(feature
181
+ .command('pr')
182
+ .description('Attach a pull request to a feature')
183
+ .argument('<id>', 'feature id (UUID or 8-hex)')
184
+ .argument('<url>', 'pull request URL')
185
+ .option('--state <state>', 'open, merged or closed', 'open')).action(async (id, url, options, command) => {
186
+ await runTrackingCommand(command, (context) => runFeaturePr(id, url, options, featureDeps(context)));
187
+ });
188
+ usageExit(feature
189
+ .command('link')
190
+ .description('Link a workspace to a feature')
191
+ .argument('<id>', 'feature id (UUID or 8-hex)')
192
+ .option('--path <dir>', 'workspace directory (default: cwd)')
193
+ .option('--workspace <workspace-uuid>', 'link by workspace id instead')).action(async (id, options, command) => {
194
+ await runTrackingCommand(command, (context) => runFeatureLink(id, options, featureDeps(context)));
195
+ });
196
+ usageExit(feature
197
+ .command('unlink')
198
+ .description('Unlink a workspace from its feature')
199
+ .option('--path <dir>', 'workspace directory (default: cwd)')
200
+ .option('--workspace <workspace-uuid>', 'unlink by workspace id instead')).action(async (options, command) => {
201
+ await runTrackingCommand(command, (context) => runFeatureLink(null, options, featureDeps(context)));
202
+ });
203
+ const machine = usageExit(program
204
+ .command('machine')
205
+ .description('Machine settings'));
206
+ usageExit(machine
207
+ .command('set')
208
+ .description('Write ~/.pgmrc and register the machine')
209
+ .option('--alias <alias>', 'machine alias')
210
+ .option('--connect <target>', 'connect target')).action(async (options, command) => {
211
+ await runTrackingCommand(command, (context) => runMachineSet(options, { ...featureDeps(context), writeConfig: writeMachineConfig }));
212
+ });
213
+ usageExit(machine
214
+ .command('list')
215
+ .description('List machines')).action(async (_options, command) => {
216
+ await runTrackingCommand(command, (context) => runMachineList(featureDeps(context)));
217
+ });
218
+ const repo = usageExit(program
219
+ .command('repo')
220
+ .description('Repository settings'));
221
+ usageExit(repo
222
+ .command('set')
223
+ .description('Set repository settings')
224
+ .argument('<remote>', 'repository remote')
225
+ .option('--release-on-merge <bool>', 'true or false')).action(async (remote, options, command) => {
226
+ await runTrackingCommand(command, (context) => runRepoSet(remote, options, featureDeps(context)));
227
+ });
228
+ }